Repository navigation
feat(discovery)!: tagged discovery config with one runtime conversion - #2833
Conversation
`RouterConfig.discovery` becomes a tagged provider configuration, and the CLI and the Python binding now reach the runtime through one conversion. - `DiscoveryConfig` is an enum tagged by `provider`, with one variant, `Kubernetes(KubernetesDiscoveryConfig)`: the old fields minus `enabled`. Presence means enabled. The legacy flat object still reads: no `provider` means Kubernetes, and `enabled: false` becomes `None` at deserialization, so writing the config back cannot turn discovery on. Canonical output writes `provider` and no `enabled`. - `RuntimeDiscoveryConfig::from_config` is the one config-to-runtime conversion. `main.rs` and the Python binding used to hand-build the runtime config beside the serialized one, the duplication that once let the KV annotation flags reach only one of the two. Mesh-router config is likewise derived once, by `MeshDiscoveryConfig::from_discovery`. - `--discovery-provider kubernetes` selects the provider, with `--service-discovery` kept as its legacy spelling; giving both is a usage error. IGW auto-enable and the worker-auto-recovery default follow the selected provider, whichever spelling chose it. The Python CLI mirrors this, and the binding gains a keyword-only `discovery=` mapping read by the same rules as `RouterConfig.discovery`. - Validation dispatches per provider; the Kubernetes checks are unchanged. - The in-cluster kind gateway now starts through `--discovery-provider kubernetes`; the host-process journey keeps `--service-discovery`. Breaking for Rust callers: construct `DiscoveryConfig::Kubernetes(..)` (or `.into()` a `KubernetesDiscoveryConfig`) instead of a struct literal; `ServiceDiscoveryConfig` loses `enabled`; `ServerConfig::service_discovery_config` is now `Option<RuntimeDiscoveryConfig>`. YAML read into `RouterConfig` must quote label values that look like numbers or booleans (`version: "1"`), as a Kubernetes manifest already must. Signed-off-by: XinyueZhang369 <zoeyzhang369@gmail.com>
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configuration
📒 Files selected for processing (2)
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (19)
💤 Files with no reviewable changes (1)
Included review availability: This review used your included allowance. 9 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 10 reviews per hour. 📝 SummarySummary by CodeRabbit
WalkthroughThe change introduces provider-tagged Kubernetes discovery configuration and adds provider selection to the Python and CLI interfaces. Startup derives worker and mesh discovery settings from the selected router configuration. ChangesKubernetes discovery provider
Priority: ➖ Normal Estimated code review effort: 4 (Complex) | ~45 minutes Change: Feature Sequence Diagram(s)sequenceDiagram
participant RouterArgs
participant RouterFromArgs
participant RustRouter
participant RuntimeDiscoveryConfig
participant KubernetesDiscovery
RouterArgs->>RouterFromArgs: resolve selected discovery provider
RouterFromArgs->>RustRouter: pass Kubernetes discovery configuration
RustRouter->>RuntimeDiscoveryConfig: convert settings for routing mode
RuntimeDiscoveryConfig->>KubernetesDiscovery: start provider discovery
Suggested reviewers: Merge Risk: ⚪ Minimal · up to No actionable issue remains from these findings; the change is mergeable after normal checks. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 63.27% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 98 functions across 17 files. (1 skipped: 1 unsupported.)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
`Router.from_args` mapped the selected provider to `service_discovery` with `== "kubernetes"`, so any other provider became `service_discovery=False`: discovery would not start and nothing would say so. It can't happen today, since only `kubernetes` is accepted, but adding a provider to the CLI choices without wiring it here would hit it. Raise instead. The new test extends the choices without wiring `from_args`, the case it guards; it fails with the old mapping. Signed-off-by: XinyueZhang369 <zoeyzhang369@gmail.com>
Brings the three commits main gained since eda1134 (#2835 Cargo.lock's toml entry, #2833 the tagged discovery config with one runtime conversion, #2838 Qwen3's tagged call syntax) under the leap branch. Resolutions, both import lists: - model_gateway/src/config/builder.rs: the `crate::config` import takes main's `KubernetesDiscoveryConfig` and the leap's `KvIndexKind`, in rustfmt order. - model_gateway/src/main.rs: the same pair in the same list. Cargo.lock, bindings/python/src/lib.rs, config/types.rs and config/validation.rs merged on their own; the bindings build the discovery config in main's enum form and the runtime conversion is main's. Signed-off-by: Simo Lin <25425177+slin1237@users.noreply.github.com>
Description
Problem
RouterConfig.discoveryis the validated, serializable discovery configuration, but discovery doesn't run from it. The Rust CLI and the Python binding each build a second, runtimeServiceDiscoveryConfigbeside it, mostly straight from the flags again, andserver::startupstarts discovery from that copy:Nothing ties the two together, so a flag wired into only one is silently lost on the other: #2359 had to wire the KV annotation flags into both. The mesh router config is assembled separately on each path too.
There is also no way to name a provider.
--service-discoveryis a Kubernetes boolean andDiscoveryConfigis a flat Kubernetes struct with anenabledflag, so the file provider that comes next has nowhere to plug in.Solution
RouterConfig.discoverybecomes one tagged provider configuration, and every input surface reaches the runtime through one conversion,RuntimeDiscoveryConfig::from_config. Kubernetes is still the only provider.--discovery-provider kubernetesselects the provider on the command line, with--service-discoverykept as its legacy spelling.Changes
Configuration (
config/types.rs).DiscoveryConfigis an enum tagged byproviderwith one variant,Kubernetes(KubernetesDiscoveryConfig): the old fields minusenabled.RouterConfig.discoveryreads throughdeserialize_discovery, which accepts the flat legacy object: a missingprovidermeans Kubernetes, andenabled: falsebecomesNoneat deserialization, so writing the config back can't turn discovery on. Validation dispatches per provider; the Kubernetes checks are unchanged.One conversion (
service_discovery/runtime.rs).RuntimeDiscoveryConfig::from_config(&DiscoveryConfig, &RoutingMode)builds the runtime config. It derives what the runtime needs beyond the configuration: the interval as aDuration, the disaggregated flag from the routing mode, and the parsedModelIdSource.main.rsand the Python binding both call it in place of their hand-built copies, andMeshDiscoveryConfig::from_discoverylikewise replaces the two hand-built mesh configs.start_service_discoverydispatches on the provider.CLI.
--discovery-provider <kubernetes>conflicts with--service-discovery; giving both is a usage error rather than a precedence rule. IGW auto-enable and the worker-auto-recovery default follow the selected provider, whichever spelling chose it.Python.
--discovery-providerjoins the Python CLI in a mutually exclusive group with--service-discovery.RouterArgs.discovery_provideris appended at the tail, since the field order is frozen for positional callers.Router.from_argspasses Kubernetes on asservice_discovery=Trueand raises for a provider it can't pass on, so a provider added to the CLI choices without wiring fails loudly instead of starting the router without discovery. The binding gains a keyword-onlydiscovery=mapping, read bydeserialize_discoverylikeRouterConfig.discovery; passing it together withservice_discovery=TrueraisesValueError.kind E2E. The in-cluster gateway now starts through
--discovery-provider kubernetes, and the host-process journey keeps--service-discovery, so every run covers both spellings.Compatibility
CLI flags, Python arguments and serialized configs all keep working. Rust callers need a small migration:
DiscoveryConfig { enabled: true, .. }DiscoveryConfig::Kubernetes(KubernetesDiscoveryConfig { .. }), or.into()DiscoveryConfig::default()KubernetesDiscoveryConfig::default()ServiceDiscoveryConfig { enabled, .. }enabledremoved;Nonearound it means offServerConfig.service_discovery_config: Option<ServiceDiscoveryConfig>Option<RuntimeDiscoveryConfig>DiscoveryConfigdeliberately has noDefault: the old default was disabled, and an enum default would silently mean enabled Kubernetes.One serialized edge: YAML read into
RouterConfigmust quote label values that look like numbers or booleans (version: "1"), as a Kubernetes manifest already must. The flat struct let serde_yaml readversion: 1into aString. A tagged union is buffered before its fields are read, so a YAML scalar keeps the type YAML gave it. No in-tree path readsRouterConfigfrom YAML.Deviations from the plan
kubernetes_legacy()constructor.From<KubernetesDiscoveryConfig>covers the migration.discovery.providerselector with RBAC gated on the providers in use, and a router-discovery surface that runs without worker discovery. Router discovery is still configured inside Kubernetesdiscovery, as today.Test Plan
New tests:
config/types.rs:legacy_flat_discovery_reads_as_kubernetes,untagged_discovery_without_enabled_reads_as_kubernetes,tagged_kubernetes_discovery_reads_as_kubernetes,disabled_legacy_discovery_reads_as_none_and_stays_none,discovery_serializes_tagged_without_enabled,legacy_flat_discovery_reads_from_yaml,unknown_discovery_provider_is_rejected,non_boolean_discovery_enabled_is_rejected.service_discovery/runtime.rs:kubernetes_settings_reach_the_runtime(every field, the KV annotations included),disaggregated_modes_select_role_selectors,invalid_model_id_source_is_a_config_error.main.rs:discovery_provider_kubernetes_matches_service_discovery(both spellings build identical router, worker-discovery and mesh configs),service_discovery_and_discovery_provider_conflict,selecting_a_discovery_provider_enables_igw;worker_auto_recovery_follows_service_discovery_by_defaultnow covers the new spelling.test_parse_discovery_provider_kubernetes,test_service_discovery_and_discovery_provider_are_exclusive,test_unknown_discovery_provider_is_rejected,test_selected_discovery_provider_programmatic,test_discovery_provider_kubernetes_enables_igw,test_from_args_passes_discovery_provider_as_service_discovery,test_from_args_rejects_a_provider_it_cannot_pass(fails against a plain== "kubernetes"mapping), andTestDiscoveryMapping(tagged and untagged mappings accepted, conflict withservice_discovery=True, unknown provider rejected).cargo +nightly fmt --all -- --checkcargo clippy --workspace --all-targets --all-features -- -D warningssmg,amg)k8s_discovery_testbindings/python/testsagainst a wheel built from this branch¹ Run on Rust 1.95, which also flags
nonminimal_boolatrouters/common/kv_transfer.rs:342onmain; that lint was allowed for the run. CI's pinned 1.98 doesn't flag it.Checklist
cargo +nightly fmtpassescargo clippy --all-targets --all-features -- -D warningspasses