Repository navigation
feat(llm-routing): route the LLM endpoint by model, with API keys - #672
Conversation
The LLM route's backend is the model router (AgentgatewayModel "*"): the request's model picks an AgentgatewayModel attached to the route. llmRouting.models renders one per provider model; the default is anthropic (provider Anthropic, match claude-*), credential-free as the AgentgatewayBackend it replaces. The first entry's provider is the one whose ModelConfigs ride the listener. With the serving slice and model-manager on, the discovery ConfigMap publishes spec.llmEndpoint (the route and the listener URL), model-manager's ServiceAccount gets a Role on agentgatewaymodels in the release namespace, and the data plane may reach the served models' workload port, so model-manager can put served models on the endpoint (giantswarm/model-manager#145). llmRouting.backend is refused by the schema; UPGRADE.md says what to set instead. Refs #603.
Published Helm chart
|
| Chart | agent-platform-connectivity |
| Version | 4.70.2-rb83fe755t20260924220746h2eda23a |
| OCI reference | oci://gsoci.azurecr.io/charts/giantswarm/agent-platform-connectivity:4.70.2-rb83fe755t20260924220746h2eda23a |
| Digest | sha256:7b2b98910053b2150841b9c6f3da8e20e796128f224220826ad993c27e54bbab |
| Registry | public — gsoci.azurecr.io |
| Git catalog | giantswarm-test-catalog (index) |
Pull this chart
helm pull oci://gsoci.azurecr.io/charts/giantswarm/agent-platform-connectivity --version 4.70.2-rb83fe755t20260924220746h2eda23aPosted by architect-orb · build 13547 · commit 2eda23a · updated in place on every push
Published Helm chart
|
| Chart | agent-platform |
| Version | 4.70.2-rb83fe755t20260924220746h2eda23a |
| OCI reference | oci://gsoci.azurecr.io/charts/giantswarm/agent-platform:4.70.2-rb83fe755t20260924220746h2eda23a |
| Digest | sha256:064cea11f827259e310a214e14328a9d69308ee2366e5f635e4b820ff7c1e879 |
| Registry | public — gsoci.azurecr.io |
| Git catalog | giantswarm-test-catalog (index) |
Pull this chart
helm pull oci://gsoci.azurecr.io/charts/giantswarm/agent-platform --version 4.70.2-rb83fe755t20260924220746h2eda23aPosted by architect-orb · build 13548 · commit 2eda23a · updated in place on every push
…I keys llmRouting.external (off by default) serves the listener's models at https://<hostPrefix>.<global.domain> behind the installation's API keys: a route of its own whose backend is the model router, with an AgentgatewayPolicy whose apiKeyAuthentication is Strict over exactly one Secret or ConfigMap source. With the chart's edge the route is on the data plane's HTTPS listener; behind a public Gateway that Gateway forwards the hostname to the LLM listener, where the route takes it by hostname and the in-cluster route stays credential-free. Every model attaches to the external route, the discovery block names it and its URL (externalEndpoint), and gateway.metricLabels.api_key carries a key's metadata.name, held until the route renders. The README names the allowedModels gap. Refs #603.
…ion path agentgateway 2.1 builds a model's upstream path from its baseURL's path plus the format's suffix; an Anthropic model without a baseURL sent POST /messages and Anthropic answered 404 (found in agentlab). Every llmRouting.models entry now carries baseURL, the default https://api.anthropic.com/v1, and the render refuses an entry without.
A GPU node pool brings the serving slice as a second release of the chart, where model-manager is off, while the platform release keeps modelServing off. model-manager's egress rule into the serving namespace followed that release's own modelServing, so it never rendered and every model served on the pool reported no interfaces. The rule now follows model-manager's kserve backend: the slice on, a static kserve backend or cluster-manager on, into model-manager's kserve namespace when the slice is elsewhere.
kserveOn, serves and servingNamespace carry the conditions the egress rule uses, for the LLM endpoint's rules to share.
…w/.local/state/klaus-lab/worktrees/ap-602b into llm-endpoint-model-router-603 * 'model-manager-egress-follows-kserve-602' of /home/teemow/.local/state/klaus-lab/worktrees/ap-602b: refactor(model-manager): name where model-manager serves in helpers fix(model-manager): read served models wherever model-manager serves
agentgateway strips the matched PathPrefix of an HTTPRoute rule whose backend is the model router, so the in-cluster route at /v1 handed it /messages, which it proxied as Passthrough with no GenAI token or cost metric, and the Gateway-scoped routes map overrode its classification. - Every llmRouting.models AgentgatewayModel attaches directly to the data plane's LLM listener (Gateway parentRef, sectionName). The in-cluster HTTPRoute, the routes-map policy and llmRouting.pathPrefixes and llmRouting.routes are gone; the schema refuses both keys. ModelConfig base URLs are unchanged. - The external route matches PathPrefix / (a root model route). With the chart's edge it stays on the https listener; behind a public Gateway it gets a listener of its own, llmRouting.external.listener (llm-external, 8082), which the forwarder targets and both data-plane network policy flavours admit. The port guard covers it. - The LLMEndpoint document moves out of the serving slice's discovery ConfigMap into <name>-llm-endpoint in the release namespace. It, the model-manager Role on agentgatewaymodels and the data plane's egress to the served workloads render with llmRouting and a serving model-manager, so a node pool's topology (cluster-manager on, slice off) gets them too. verify-llm-routing asserts the new shape; verify-target holds the forwarded removed keys.
The agentgateway controller refuses a model attached to a listener whose allowedRoutes do not name the kind (NotAllowedByListeners). The LLM listener now admits AgentgatewayModels only, which also keeps every HTTPRoute, the MCP catch-all included, off it.
|
Lab proof: a fresh local agentlab (kind) with this branch's chart (b7fa12b), Found and fixed in the lab (b7fa12b): the controller refused the listener-attached model with In-cluster listener (
External ( Metering.
|
…uter-603 * origin/main: fix(model-manager): read served models wherever model-manager serves (#681) feat(model-serving): check every shipped preset's parsers by model family (#674) fix(backstage): drop the portal's dead agentPlatform.modelManager block (#677) fix(agent-manager): cap the Generic agent chart below 1.5.0 (#668) feat(observability): collect Substrate's and the kagent controller's metrics (#619) fix(connectivity): limit the substrate-bootstrap hook to 256Mi (#666) fix: drift detection ignores the serving configs' defaulted spec (#680) # Conflicts: # CHANGELOG.md # UPGRADE.md
…-oauth * origin/main: feat(model-serving): stamp the cache claim's tier as annotations (#676) fix(substrate): the capz snapshot-store identity holds Storage Blob Delegator on the account (#686) fix(connectivity): the egress gateway reaches the Substrate credential provider (#685) feat(llm-routing): route the LLM endpoint by model, with API keys (#672) # Conflicts: # CHANGELOG.md
…ardown-527 * origin/main: feat(model-serving): stamp the cache claim's tier as annotations (#676) fix(substrate): the capz snapshot-store identity holds Storage Blob Delegator on the account (#686) fix(connectivity): the egress gateway reaches the Substrate credential provider (#685) feat(llm-routing): route the LLM endpoint by model, with API keys (#672) # Conflicts: # CHANGELOG.md
…i-580 * origin/main: fix(serving): tear the serving slice down in order, configs freed (#679) fix: derive the managers' OAuth login from muster's OAuth server (#678) feat(model-serving): stamp the cache claim's tier as annotations (#676) fix(substrate): the capz snapshot-store identity holds Storage Blob Delegator on the account (#686) fix(connectivity): the egress gateway reaches the Substrate credential provider (#685) feat(llm-routing): route the LLM endpoint by model, with API keys (#672) feat: the kagent line and the Substrate line at 1.1.0 (#665) fix(model-manager): read served models wherever model-manager serves (#681) feat(model-serving): check every shipped preset's parsers by model family (#674) fix(backstage): drop the portal's dead agentPlatform.modelManager block (#677) fix(agent-manager): cap the Generic agent chart below 1.5.0 (#668) feat(observability): collect Substrate's and the kagent controller's metrics (#619) fix(connectivity): limit the substrate-bootstrap hook to 256Mi (#666) fix: drift detection ignores the serving configs' defaulted spec (#680) # Conflicts: # CHANGELOG.md # tests/verify-target.py
…en-606 * origin/main: (28 commits) feat(dashboards): turn done (final flush) p50 and p95 on Klaus Gateway (#701) feat: the managers', the portal's and mcp-kubernetes' traces to the platform's OTLP gateway (#700) feat(kagent): compact agent histories only past 600000 tokens (#699) fix: keep the single-replica components drainable (maxUnavailable 1) (#697) feat(model-serving): export the model pods' traces to the platform's OTLP gateway (#695) fix(substrate): keep Substrate's metrics on the scrape path (#694) feat: one OTLP collector for every exporter (global.observability.traces.otlp) (#696) fix(netpol): admit the scrape of the kagent controller and atenet-egress ext-proc (#691) fix(serving): make the models Gateway an internet-facing NLB on AWS (#692) fix(model-manager): reach the workload clusters' API servers (#689) feat(components): the CloudNativePG operator chart and image from gsoci (#671) fix(serving): tear the serving slice down in order, configs freed (#679) fix: derive the managers' OAuth login from muster's OAuth server (#678) feat(model-serving): stamp the cache claim's tier as annotations (#676) fix(substrate): the capz snapshot-store identity holds Storage Blob Delegator on the account (#686) fix(connectivity): the egress gateway reaches the Substrate credential provider (#685) feat(llm-routing): route the LLM endpoint by model, with API keys (#672) feat: the kagent line and the Substrate line at 1.1.0 (#665) fix(model-manager): read served models wherever model-manager serves (#681) feat(model-serving): check every shipped preset's parsers by model family (#674) ... # Conflicts: # CHANGELOG.md # tests/verify-gpu-pool.py
…reset-597 * origin/main: (28 commits) feat(dashboards): turn done (final flush) p50 and p95 on Klaus Gateway (#701) feat: the managers', the portal's and mcp-kubernetes' traces to the platform's OTLP gateway (#700) feat(kagent): compact agent histories only past 600000 tokens (#699) fix: keep the single-replica components drainable (maxUnavailable 1) (#697) feat(model-serving): export the model pods' traces to the platform's OTLP gateway (#695) fix(substrate): keep Substrate's metrics on the scrape path (#694) feat: one OTLP collector for every exporter (global.observability.traces.otlp) (#696) fix(netpol): admit the scrape of the kagent controller and atenet-egress ext-proc (#691) fix(serving): make the models Gateway an internet-facing NLB on AWS (#692) fix(model-manager): reach the workload clusters' API servers (#689) feat(components): the CloudNativePG operator chart and image from gsoci (#671) fix(serving): tear the serving slice down in order, configs freed (#679) fix: derive the managers' OAuth login from muster's OAuth server (#678) feat(model-serving): stamp the cache claim's tier as annotations (#676) fix(substrate): the capz snapshot-store identity holds Storage Blob Delegator on the account (#686) fix(connectivity): the egress gateway reaches the Substrate credential provider (#685) feat(llm-routing): route the LLM endpoint by model, with API keys (#672) feat: the kagent line and the Substrate line at 1.1.0 (#665) fix(model-manager): read served models wherever model-manager serves (#681) feat(model-serving): check every shipped preset's parsers by model family (#674) ... # Conflicts: # CHANGELOG.md # tests/verify-gpu-pool.py
What does this PR do?
#603: the LLM listener routes by model, served models get a way onto it, and the endpoint can be published outside the cluster behind API keys, with every request metered.
Models on the listener. Every
AgentgatewayModelattaches to the data plane Gateway's LLM listener directly: parentRefGateway,sectionName: llm.AgentgatewayModels only. The controller refuses a model on a listener whoseallowedRoutes.kindsdo not name the kind, and no HTTPRoute, the MCP catch-all included, attaches there./v1/chat/completions,/v1/responses,/v1/messages,/v1/messages/count_tokens,/v1/models, …). The request'smodelpicks the model;GET /v1/modelslists the public ones, and a name no model takes answers404 model_not_found./v1that left the router paths it could not classify: requests passed through unparsed and recorded no token or cost metrics. The Gateway-scopedbackend.ai.routesmap, whose"*": Passthroughoverrode the router's classification, is removed too, since no AI backend remains on the LLM path.http://<gateway>.<namespace>.svc:<port>as their baseUrl, so no ModelConfig needs migrating.llmRouting.modelsreplacesllmRouting.backend: oneAgentgatewayModelper entry (name,provider— an agentgateway managed provider —,match,visibility,baseURL).anthropic: provider Anthropic,baseURL: https://api.anthropic.com/v1,match: claude-*, credential-free like theAgentgatewayBackendit replaces./messages, which answers 404.*.Served models (kserve: one AgentgatewayModel per served model on the LLM endpoint model-manager#145): the platform release renders the LLM endpoint document, a ConfigMap
<release>-llm-endpointlabelledagent-platform.giantswarm.io/llm-endpoint: "true", keyllm-endpoint.yaml, kindLLMEndpoint:spec.parentRefs: the listener, plus the external route;spec.endpoint: the listener's in-cluster URL;spec.externalEndpoint: withllmRouting.external.With it come model-manager's Role on
agentgatewaymodelsin the release namespace (nothing else) and the data plane's egress to the served workloads (both flavours). All three render withllmRoutingon and model-manager serving: this release's serving slice, a static kserve backend, or cluster-manager on (a GPU node pool's slice is a second release of this chart with model-manager off, fix(model-manager): read served models wherever model-manager serves #681). The discovery ConfigMap carries nospec.llmEndpoint.llmRouting.external(off by default): the endpoint onhttps://<hostPrefix>.<global.domain>behind the installation's API keys. The route<release>-llm-externalhas the model router as backend and matches/under its hostname; the hostname outranks the catch-all. TheAgentgatewayPolicyusesapiKeyAuthenticationStrictover exactly one key source in the release namespace (apiKeys.secretRef.name,.secretSelector.matchLabelsor.configMapSelector.matchLabels, the last with hashed keys only).gateway.metricLabels.api_key(apiKey.name, a key entry'smetadata.name) renders with it. The route's placement depends on the edge:llmRouting.external.listener(llm-external, 8082). The controller refuses a root model route on a listener with directly attached models. The public Gateway forwards the hostname there, and both policy flavours admit the port.The README says every key reaches every model until agentgateway enforces
allowedModelson the Kubernetes API-key contract (giantswarm/giantswarm#37742).Guards and docs. Render guards: an empty model list, a duplicate name, a model without a baseURL, a wildcard the CRD refuses, the external endpoint with zero or two key sources, a
hostPrefixthat is no DNS label, and an external listener port taken by another listener. The schema refusesllmRouting.backend,.pathPrefixesand.routes. The connectivity README's LLM routing section, UPGRADE.md, CHANGELOG, both schemas and READMEs are updated.What is the effect of this change to users?
An installation with
llmRouting.enabledkeeps routingclaude-*models to Anthropic with the client's own key, at the same URL, and every request through the router is metered:agentgateway_gen_ai_client_token_usageand_cost_usd_totalwith the request and response model, the agent and the person. The series'routelabel becomesinternal/llm:request. Served models join the listener by their preset name once model-manager#145 ships. An installation that set nothing renders nothing new. Breaking for an installation that setllmRouting.backend,pathPrefixesorroutes: see UPGRADE.md.How does it look like?
make verify-llm-routingcovers:llm:8081:HTTP:Same:AgentgatewayModel), the models' parentRefs, and no in-cluster HTTPRoute or routes map;/in both edge shapes (with the dedicated listener, forwarder port and policy ports), the key policy with one source, and theapi_keylabel.make verify-all: 56 of 56.Proven in a local agentlab (kind) with this chart and model-manager#145's build; the comment below has the output:
api_key.