ff20407a2d
* 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>
179 lines
7.0 KiB
Python
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]
|