Files
Bryan Qiu ff20407a2d fix(routing): count managed-settings AIGW backing for claude-native (#4491)
* fix(routing): count managed-settings AIGW backing for claude-native

claude_gateway_inference_backed() returned False whenever
resolve_native_claude_config yielded no config — the case for a
subscription (Claude Code login) provider. But Claude Code itself still
routes all inference through an AI Gateway when an enterprise managed
settings file pins ANTHROPIC_BASE_URL, so Smart Routing was being gated
off for a genuinely gateway-backed launch. Codex already reads its own
config.toml base_url; this brings Claude to parity.

Add a fallback: read Claude Code managed settings and treat the launch as
gateway-backed when env.ANTHROPIC_BASE_URL is a Databricks AI Gateway URL
(validated with is_databricks_ai_gateway_url) and a credential is
delivered via top-level apiKeyHelper or a truthy env.CLAUDE_CODE_USE_GATEWAY.
Managed settings win at the real launch, so this signal can flip the
answer to True even when the omnigent provider is subscription.

Co-authored-by: Isaac
Signed-off-by: Bryan Qiu <bryan.qiu@databricks.com>

* fix(routing): validate the resolve-path base URL as a Databricks AIGW

The resolve-based branch of claude_gateway_inference_backed() returned
True on just ANTHROPIC_BASE_URL + api_key_helper being present, without
checking the URL is actually a Databricks AI Gateway. A bare
api.anthropic.com (or any non-Databricks Anthropic-compatible endpoint)
would qualify — but the external task_v1 router's picks are Databricks
catalog ids that endpoint cannot serve. Require
is_databricks_ai_gateway_url() on the resolved base URL too, matching the
managed-settings fallback and the Codex check.

Co-authored-by: Isaac
Signed-off-by: Bryan Qiu <bryan.qiu@databricks.com>

* fix(routing): resolve cli-config codex base URL from the shared config.toml

native_codex_launch_base_url() returned None for a cli-config launch,
because such a launch pins only a model_provider name — the provider
table (with base_url) lives in the user's shared ~/.codex/config.toml,
which the launch never inlines. So codex_gateway_inference_backed()
reported a genuinely AIGW-routed cli-config provider as not backed,
gating Smart Routing off. This is the Codex analogue of the Claude
managed-settings gap.

Read the shared config.toml in the final branch: extract the pinned
provider name (codex_session_meta_model_provider), locate the user's
CODEX_HOME config via _codex_home_config_source_from_env, and return
model_providers.<name>.base_url with tomllib. openai (Codex's own login)
and omnigent_databricks (the profile branch's generated id) have no
user-config table, so they stay None. Any read/parse failure returns
None — an unreadable config is unknown, not backed. codex_gateway_
inference_backed() is unchanged; it validates the URL as before.

Co-authored-by: Isaac
Signed-off-by: Bryan Qiu <bryan.qiu@databricks.com>

* fix(routing): resolve codex config-default base URL for the empty-override launch

The prior commit covered a cli-config launch that pins a model_provider
name, but the user's Databricks-wide setup hits a different path: when no
omnigent provider resolves and the config default is not dismissed,
resolve_native_codex_launch leaves config_overrides empty on purpose so
Codex uses its own config.toml top-level model_provider default. On such
a machine that default is a Databricks AIGW provider, yet the probe saw
empty overrides and reported not-backed.

Extend native_codex_launch_base_url: when a launch pins no model_provider
override and no profile, resolve the config.toml top-level model_provider
default's base_url (unless the user dismissed the default, which pins
Codex's built-in openai). An explicit model_provider="openai" override
(subscription / dismissed paths) still returns None — only a truly
unpinned launch reads the config default. Factor the shared table lookup
into _config_toml_provider_base_url, used by both the cli-config and
config-default paths.

Co-authored-by: Isaac
Signed-off-by: Bryan Qiu <bryan.qiu@databricks.com>

* fix(routing): count a resolvable launch base URL as codex readiness

_codex_auth_unavailable_reason() detected a provider-routed launch only
via a profile or a non-openai model_provider override. On a Databricks-
wide machine the launch pins neither — omnigent defers to Codex's own
config.toml top-level model_provider default — so readiness fell through
to the auth.json check, found no openai credential, and falsely reported
needs-auth even though bare `codex` works. That gated the Smart Routing
harness row off in New Chat (it needs both claude-native and codex-native
ready).

Broaden the predicate to also count a resolvable launch base URL
(native_codex_launch_base_url(launch) is not None), which now resolves
the config.toml provider default. This only adds a ready case: an
explicit model_provider="openai" pin still returns None from that helper,
so a genuinely logged-out openai user still reports needs-auth. Readiness
now agrees with the launch resolver and the gateway-inference check.

Co-authored-by: Isaac
Signed-off-by: Bryan Qiu <bryan.qiu@databricks.com>

* test: wrap the codex config.toml fixture under the line limit

Split the three identical model_providers config-toml f-strings across two
adjacent literals so each line stays under 99 chars, clearing the ruff E501
that failed pre-commit.

Co-authored-by: Isaac
Signed-off-by: Bryan Qiu <bryan.qiu@databricks.com>

---------

Signed-off-by: Bryan Qiu <bryan.qiu@databricks.com>
2026-08-11 03:19:09 +00:00

179 lines
7.0 KiB
Python

"""Host-side checks for whether a harness family's inference is AI-Gateway-backed.
Smart Routing's apply layer can only rewrite a launch's model when the launch
resolves through the Databricks AI Gateway — that is where the routable model
catalog lives. These checks answer that question per harness family from config
resolution alone: no process launch, no network round-trip, so the host can
report the answer alongside harness readiness on every registration.
"""
from __future__ import annotations
import logging
from collections.abc import Iterable, Mapping
from typing import Final
_logger = logging.getLogger(__name__)
# Every spelling the Claude family travels under on the wire.
CLAUDE_GATEWAY_HARNESSES: Final[tuple[str, ...]] = ("claude-native", "native-claude")
# Every spelling the Codex family travels under on the wire.
CODEX_GATEWAY_HARNESSES: Final[tuple[str, ...]] = ("codex", "codex-native", "native-codex")
# The AI Gateway serves Codex/OpenAI-Responses under this path suffix; both
# gateway URL shapes (dedicated subdomain and workspace-hosted) end with it.
_CODEX_GATEWAY_PATH_SUFFIX = "/codex/v1"
def claude_gateway_inference_backed() -> bool:
"""Whether a claude-native launch on this host resolves gateway-backed inference.
A gateway-backed launch pins a Databricks AI Gateway ``ANTHROPIC_BASE_URL``
and delivers its bearer token through Claude Code's ``apiKeyHelper``. The
base URL must be a genuine Databricks AI Gateway (validated with
:func:`is_databricks_ai_gateway_url`, parity with the Codex check), since
the external router's picks are Databricks catalog ids only that endpoint
serves. The Bedrock path sets ``ANTHROPIC_BEDROCK_BASE_URL`` with no
helper — not routable.
A subscription / CLI login resolves no omnigent config, yet Claude Code
still routes all inference through an AI Gateway when an enterprise managed
settings file pins it. Managed settings win at the actual launch, so that
signal counts too: it flips the answer to ``True`` even when resolution
yields nothing.
:returns: ``True`` iff a claude-native launch resolves AI-Gateway-backed
inference, from omnigent config or managed settings.
"""
from omnigent.claude_native import (
managed_claude_gateway_signal,
resolve_native_claude_config,
)
from omnigent.databricks_ai_gateway import is_databricks_ai_gateway_url
config = resolve_native_claude_config(spec=None, refresh_models=False)
if config is not None:
base_url = config.env.get("ANTHROPIC_BASE_URL")
if base_url and config.api_key_helper and is_databricks_ai_gateway_url(base_url):
return True
managed_base_url, managed_has_credential = managed_claude_gateway_signal()
if (
managed_base_url is not None
and managed_has_credential
and is_databricks_ai_gateway_url(managed_base_url)
):
return True
return False
def codex_gateway_inference_backed() -> bool:
"""Whether a codex-native launch on this host resolves gateway-backed inference.
:returns: ``True`` iff the resolved launch routes through an AI Gateway
Codex base URL.
"""
from omnigent.codex_native_app_server import (
native_codex_launch_base_url,
resolve_native_codex_launch,
)
from omnigent.databricks_ai_gateway import is_databricks_ai_gateway_url
base_url = native_codex_launch_base_url(resolve_native_codex_launch(model=None))
if not base_url:
return False
if not is_databricks_ai_gateway_url(base_url):
return False
return base_url.rstrip("/").endswith(_CODEX_GATEWAY_PATH_SUFFIX)
def gateway_inference_map() -> dict[str, bool]:
"""Per-harness map of whether this host's inference for that family is gateway-backed.
Each family is evaluated once and the result fanned out over every spelling
that family travels under. A family whose check raises is omitted rather
than reported as ``False``, so the server can tell "not gateway-backed"
apart from "could not tell".
:returns: Harness spelling → gateway-backed flag, omitting unevaluable
families.
"""
result: dict[str, bool] = {}
for family, spellings, check in (
("claude", CLAUDE_GATEWAY_HARNESSES, claude_gateway_inference_backed),
("codex", CODEX_GATEWAY_HARNESSES, codex_gateway_inference_backed),
):
try:
backed = check()
except Exception: # noqa: BLE001 — an unevaluable family is omitted, not False
_logger.warning(
"gateway-inference check for the %s family failed; omitting it",
family,
exc_info=True,
)
continue
for spelling in spellings:
result[spelling] = backed
return result
def gateway_inference_state(
gateway: Mapping[str, object] | None,
harness: str,
) -> bool | None:
"""Read *harness*'s gateway-backed flag out of a reported map.
:param gateway: A host's ``gateway_inference`` map, or ``None``.
:param harness: Harness id in any spelling, e.g. ``"native-codex"``.
:returns: The reported flag, or ``None`` when the map says nothing about
this harness — an older host, a family whose check could not run, or a
host that has not registered yet. Unknown is not "unavailable".
"""
if not gateway:
return None
for key in _family_spellings(harness):
value = gateway.get(key)
if isinstance(value, bool):
return value
return None
def _family_spellings(harness: str) -> tuple[str, ...]:
"""Every key a host may have reported *harness*'s family under.
:func:`gateway_inference_map` fans one family verdict out over all of its
spellings, but a caller holds only one — and the reversed aliases
(``native-codex``) never canonicalize back. Look the family up instead, so
any spelling finds the entry.
:param harness: Harness id in any spelling, e.g. ``"native-codex"``.
:returns: The family's spellings, or just *harness* when it is in neither.
"""
from omnigent.harness_aliases import canonicalize_harness
canonical = canonicalize_harness(harness) or harness
for spellings in (CLAUDE_GATEWAY_HARNESSES, CODEX_GATEWAY_HARNESSES):
if canonical in spellings or harness in spellings:
return spellings
return (canonical, harness)
def not_gateway_backed(
gateway: Mapping[str, object] | None,
harnesses: Iterable[str],
) -> list[str]:
"""Which of *harnesses* the map explicitly reports as not gateway-backed.
Smart Routing's apply layer rewrites the launch model through the AI
Gateway, so these are the harnesses a routed pick could not reach. Only an
explicit ``False`` counts: unknown keeps every option.
:param gateway: A host's ``gateway_inference`` map, or ``None``.
:param harnesses: Harness ids to check, e.g.
``("claude-native", "codex-native")``.
:returns: The not-backed ids, in the order given.
"""
return [harness for harness in harnesses if gateway_inference_state(gateway, harness) is False]