fix(codex): pin shadowed config provider on resume (#4818)
## Related issue
N/A — reported and reproduced locally.
## Summary
- Pin Codex's detected `config.toml` provider when an explicit, non-default same-name Omnigent entry shadows ambient default synthesis.
- Resolve the provider once during native launch so rollout metadata, app-server, and remote TUI use the same immutable selection.
- Preserve spec, explicit-default, global-auth, subscription, and dismissed-provider precedence.
ELI5: if Codex is configured to use a gateway but Omnigent's matching provider entry is not marked default, a resumed conversation now follows Codex's actual gateway instead of falling back to unauthenticated OpenAI.
```text
Codex config detection ──► resolved native launch ──► resume rollout/TUI
Databricks Databricks Databricks
```
## Test Plan
- `uv run --frozen pytest tests/test_native_codex_provider.py -k 'config_provider_shadowed_by_nondefault_explicit_entry_still_pins or shadowed_config_detection_uses_active_profile_provider or resolve_native_codex_launch_undismissed_config_provider_routes_via_pin or resolve_native_codex_launch_dismissed_config_provider_pins_openai'`
- `uv run --frozen pytest tests/test_codex_native.py -k 'resolve_native_codex_launch_no_provider_sets_login_fallback_summary or resolve_native_codex_launch_databricks_provider_sets_summary'`
- `uv run --frozen ruff check omnigent/codex_native_app_server.py tests/test_native_codex_provider.py tests/test_codex_native.py`
- `uv run --frozen ruff format --check omnigent/codex_native_app_server.py tests/test_native_codex_provider.py tests/test_codex_native.py`
- `git diff --check`
## Demo
N/A — non-visual backend fix.
## Type of change
- [x] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [ ] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change
## Test coverage
- [x] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [ ] Manual verification completed
- [x] Existing tests cover this change
- [ ] Not applicable
## Coverage notes
The new tests reproduce the shadowed non-default provider state and verify active Codex profile selection. Existing tests cover dismissed providers, ordinary detected providers, explicit defaults, and no-provider summaries.
## Changelog
Resumed Codex conversations now keep using the provider selected in Codex configuration.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
This commit is contained in:
@@ -2283,8 +2283,9 @@ def resolve_native_codex_launch(
|
||||
config (issue #2744 — parity with the in-process codex harness).
|
||||
:returns: The resolved :class:`NativeCodexLaunch`.
|
||||
"""
|
||||
from omnigent.onboarding.ambient import codex_config_detection
|
||||
from omnigent.onboarding.detected import (
|
||||
codex_config_provider_dismissed,
|
||||
dismissed_detection_names,
|
||||
effective_config_with_detected,
|
||||
)
|
||||
from omnigent.onboarding.provider_config import (
|
||||
@@ -2296,14 +2297,17 @@ def resolve_native_codex_launch(
|
||||
from omnigent.spec.types import DatabricksAuth
|
||||
|
||||
explicit = load_config()
|
||||
config_detection = codex_config_detection()
|
||||
config_provider_dismissed = (
|
||||
config_detection is not None
|
||||
and config_detection.name in dismissed_detection_names(explicit)
|
||||
)
|
||||
# When the launch ends up on codex's own login with NO provider routing,
|
||||
# the bridged config.toml's custom default model_provider would still
|
||||
# apply — including one the user explicitly Removed (dismissed). Pin
|
||||
# codex's built-in provider in that case so the dismissal holds at run
|
||||
# time. An undetectable/undismissed custom provider keeps its routing.
|
||||
no_provider_overrides = (
|
||||
['model_provider="openai"'] if codex_config_provider_dismissed(explicit) else []
|
||||
)
|
||||
no_provider_overrides = ['model_provider="openai"'] if config_provider_dismissed else []
|
||||
if spec is not None and (
|
||||
spec.executor.auth is not None
|
||||
or spec.executor.profile
|
||||
@@ -2384,6 +2388,31 @@ def resolve_native_codex_launch(
|
||||
)
|
||||
entry = default_provider_for_harness(effective_config_with_detected(explicit), "codex")
|
||||
|
||||
if (
|
||||
entry is None
|
||||
and config_detection is not None
|
||||
and config_detection.model_provider is not None
|
||||
and not config_provider_dismissed
|
||||
):
|
||||
# An adopted cli-config entry can explicitly shadow the same ambient
|
||||
# detection without being marked the Omnigent default. Codex still
|
||||
# selects that provider from config.toml, so pin the already-resolved
|
||||
# detection instead of describing this as an OpenAI-login launch.
|
||||
# This keeps rollout metadata, app-server, and remote TUI routing on
|
||||
# one immutable provider selection during cold resume.
|
||||
provider_id = config_detection.model_provider
|
||||
_logger.info(
|
||||
"native-codex routing: config.toml provider %r (ambient fallback, model=%s)",
|
||||
provider_id,
|
||||
model,
|
||||
)
|
||||
return NativeCodexLaunch(
|
||||
config_overrides=[f"model_provider={json.dumps(provider_id)}"],
|
||||
model=model,
|
||||
profile=None,
|
||||
summary=f"Codex config.toml provider {provider_id!r} (ambient fallback)",
|
||||
)
|
||||
|
||||
if entry is None:
|
||||
_logger.info(
|
||||
"native-codex routing: Codex CLI login (no provider configured for the Codex "
|
||||
|
||||
@@ -10272,11 +10272,12 @@ def test_resolve_native_codex_launch_no_provider_sets_login_fallback_summary(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""No configured provider -> summary names the login fallback (#2745)."""
|
||||
from omnigent.onboarding import detected, provider_config
|
||||
from omnigent.onboarding import ambient, detected, provider_config
|
||||
from omnigent.runtime import workflow
|
||||
|
||||
monkeypatch.setattr(provider_config, "load_config", dict)
|
||||
monkeypatch.setattr(detected, "codex_config_provider_dismissed", lambda cfg: False)
|
||||
monkeypatch.setattr(ambient, "codex_config_detection", lambda: None)
|
||||
monkeypatch.setattr(detected, "dismissed_detection_names", lambda cfg: frozenset())
|
||||
monkeypatch.setattr(detected, "effective_config_with_detected", lambda cfg: {})
|
||||
monkeypatch.setattr(provider_config, "default_provider_for_harness", lambda cfg, harness: None)
|
||||
monkeypatch.setattr(workflow, "_load_global_auth", lambda: None)
|
||||
@@ -10292,11 +10293,12 @@ def test_resolve_native_codex_launch_databricks_provider_sets_summary(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""A Databricks provider default -> summary names the ucode profile (#2745)."""
|
||||
from omnigent.onboarding import detected, provider_config
|
||||
from omnigent.onboarding import ambient, detected, provider_config
|
||||
|
||||
entry = SimpleNamespace(kind=provider_config.DATABRICKS_KIND, profile="my-profile")
|
||||
monkeypatch.setattr(provider_config, "load_config", dict)
|
||||
monkeypatch.setattr(detected, "codex_config_provider_dismissed", lambda cfg: False)
|
||||
monkeypatch.setattr(ambient, "codex_config_detection", lambda: None)
|
||||
monkeypatch.setattr(detected, "dismissed_detection_names", lambda cfg: frozenset())
|
||||
monkeypatch.setattr(
|
||||
provider_config, "default_provider_for_harness", lambda cfg, harness: entry
|
||||
)
|
||||
|
||||
@@ -440,6 +440,74 @@ def test_resolve_native_codex_launch_undismissed_config_provider_routes_via_pin(
|
||||
assert launch.profile is None
|
||||
|
||||
|
||||
def test_config_provider_shadowed_by_nondefault_explicit_entry_still_pins(
|
||||
_isolated: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""A nondefault adopted entry cannot hide Codex's active config provider.
|
||||
|
||||
The explicit entry shadows ambient synthesis by name, but Codex itself
|
||||
still selects the provider from config.toml. An empty launch would make a
|
||||
synthesized resume rollout record OpenAI and lose this provider's auth.
|
||||
"""
|
||||
monkeypatch.setattr("omnigent.onboarding.ambient._ollama_reachable", lambda: False)
|
||||
codex_dir = _isolated / ".codex"
|
||||
codex_dir.mkdir()
|
||||
(codex_dir / "config.toml").write_text(_DISMISSIBLE_CODEX_CONFIG)
|
||||
_seed(
|
||||
_isolated,
|
||||
{
|
||||
"codex-databricks": {
|
||||
"kind": "cli-config",
|
||||
"cli": "codex",
|
||||
"model_provider": "Databricks",
|
||||
"display_name": "Databricks AI Gateway",
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
launch = resolve_native_codex_launch(model="test-model")
|
||||
|
||||
assert launch.config_overrides == ['model_provider="Databricks"']
|
||||
assert launch.model == "test-model"
|
||||
assert launch.profile is None
|
||||
|
||||
|
||||
def test_shadowed_config_detection_uses_active_profile_provider(
|
||||
_isolated: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""The fallback pins the provider selected by Codex's active profile."""
|
||||
monkeypatch.setattr("omnigent.onboarding.ambient._ollama_reachable", lambda: False)
|
||||
codex_dir = _isolated / ".codex"
|
||||
codex_dir.mkdir()
|
||||
(codex_dir / "config.toml").write_text(
|
||||
'profile = "work"\n'
|
||||
'model_provider = "UnusedTopLevel"\n'
|
||||
"[profiles.work]\n"
|
||||
'model_provider = "Databricks"\n'
|
||||
"[model_providers.Databricks]\n"
|
||||
'name = "Databricks AI Gateway"\n'
|
||||
'base_url = "https://example.ai-gateway.cloud.databricks.com/codex/v1"\n'
|
||||
"[model_providers.Databricks.auth]\n"
|
||||
'command = "jq"\n'
|
||||
)
|
||||
_seed(
|
||||
_isolated,
|
||||
{
|
||||
"codex-databricks": {
|
||||
"kind": "cli-config",
|
||||
"cli": "codex",
|
||||
"model_provider": "Databricks",
|
||||
"display_name": "Databricks AI Gateway",
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
launch = resolve_native_codex_launch(model=None)
|
||||
|
||||
assert launch.config_overrides == ['model_provider="Databricks"']
|
||||
assert launch.profile is None
|
||||
|
||||
|
||||
# ── Spec-level credentials (issue #2744) ────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user