Files
molecule-ai-workspace-templ…/render_provider_toml.py
infra-runtime-be 80237bcabc
CI / Adapter unit tests (push) Successful in 35s
CI / Template validation (static) (push) Successful in 40s
CI / Adapter unit tests (pull_request) Successful in 32s
CI / Template validation (static) (pull_request) Successful in 1m25s
CI / Template validation (runtime) (push) Successful in 2m43s
CI / T4 tier-4 conformance (live) (push) Successful in 2m40s
CI / T4 tier-4 conformance (live) (pull_request) Successful in 50s
CI / validate (push) Successful in 9s
CI / Template validation (runtime) (pull_request) Successful in 3m13s
CI / validate (pull_request) Successful in 4s
feat(codex): YAML-driven provider abstraction — replace hardcoded minimax script
Replace the hardcoded codex_minimax_config.sh single-provider routing
with a generic providers: registry in config.yaml and a Python adapter
layer (provider_config.py) that resolves the picked provider against
the env + writes ~/.codex/config.toml accordingly. Shape mirrors the
claude-code template's provider registry / _resolve_provider /
_project_vendor_auth pattern (PR template-claude-code#24), adapted to
codex's file-based config (config.toml + auth.json) rather than the
Anthropic SDK's env-var contract.

Three auth modes supported in the registry:
  - chatgpt_subscription  (CODEX_AUTH_JSON / CODEX_CHATGPT_AUTH_JSON)
  - openai_api            (OPENAI_API_KEY)
  - openai_compat_responses (third-party Responses-API endpoints +
                             vendor env key, e.g. MiniMax token-plan)

Resolution honors the verified prod precedence from PR#11: when a
subscription credential is present it wins over a model-prefix match,
so prod-Reviewer / prod-Researcher workspaces with both CODEX_AUTH_JSON
and MINIMAX_API_KEY set continue to route through the subscription
(NO model_provider override → built-in OpenAI/Responses path). The
verified gpt-5.5 + 5.4 + 5.3-codex roster is preserved unchanged.

Backward compat: codex_minimax_config.sh stays in the image as a
fallback for one release so external ops scripts + the existing test
fixtures that exec it directly keep working; start.sh prefers the new
python helper when available.

Adding a new codex-compatible provider is now a one-entry config.yaml
edit instead of a code change in the boot scripts.

Tests:
  - tests/test_provider_abstraction.py (new, 16 cases) — registry load,
    resolution precedence (subscription / explicit / model-prefix /
    credential-aware), render output for each auth mode, idempotent
    write + stale-override cleanup, fail-closed on misconfigured entry.
  - tests/test_modernization_pr1.py updated: roster assertion relaxed
    from equality to "verified OpenAI set is a subset of the model
    list" (the multi-provider abstraction adds third-party entries
    alongside the OpenAI roster); per-model required_env validated
    against the providers registry rather than hardcoded to
    OPENAI_API_KEY.

Prior art:
  - template-claude-code config.yaml providers registry +
    adapter.py _load_providers / _resolve_provider / _project_vendor_auth
  - OpenClaw multi-provider routing (internal#440)
  - PR#11 subscription-wins-over-minimax precedence (internal#513)

Tracks internal#514. NOT a fix for the MiniMax Chat-vs-Responses
incompatibility on CLI 0.130 — that remains a downstream-vendor gap
and is now data in the YAML registry (wire_api: responses) so a future
shim flips a YAML field rather than touching boot scripts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:18:50 -07:00

105 lines
3.5 KiB
Python

"""CLI entry point — render ``~/.codex/config.toml`` from the providers registry.
Invoked from ``start.sh`` (replacing the old hardcoded
``codex_minimax_config.sh`` invocation). The actual logic lives in
``provider_config.py`` so the adapter and the boot script share one
implementation; this file is just an ``argparse``-free wrapper that
loads the YAML registry, resolves the provider against the current
env, and writes the config.toml.
Exit codes:
0 — wrote a config.toml (compat provider with model_provider override).
0 — wrote nothing (built-in OpenAI mode; codex uses its native default).
2 — registry / env misconfig (raised ValueError); we print the message
so ``start.sh`` can surface it to ``docker logs`` and the operator.
Usage:
python3 render_provider_toml.py # auto-resolve
python3 render_provider_toml.py --provider X # explicit provider
python3 render_provider_toml.py --model Y # explicit model
"""
from __future__ import annotations
import argparse
import logging
import os
import sys
from pathlib import Path
# Allow running from /usr/local/bin or /app via either copy.
_HERE = Path(__file__).resolve().parent
if str(_HERE) not in sys.path:
sys.path.insert(0, str(_HERE))
def main() -> int:
logging.basicConfig(
level=os.environ.get("CODEX_PROVIDER_LOG_LEVEL", "INFO"),
format="[codex-provider] %(message)s",
)
parser = argparse.ArgumentParser(
description="Render ~/.codex/config.toml from the providers registry"
)
parser.add_argument("--provider", default="", help="explicit provider name")
parser.add_argument("--model", default="", help="explicit model id")
parser.add_argument(
"--codex-home",
default=os.environ.get("CODEX_HOME") or "",
help="override $CODEX_HOME (default: $CODEX_HOME or ~/.codex)",
)
parser.add_argument(
"--workspace-config",
default=os.environ.get("WORKSPACE_CONFIG_PATH", "/configs"),
help="workspace config dir (for the per-workspace YAML fallback)",
)
args = parser.parse_args()
try:
from provider_config import (
load_providers, resolve_provider, write_config_toml,
)
except ImportError as exc:
print(f"[codex-provider] FATAL: provider_config import failed: {exc}",
file=sys.stderr)
return 2
providers = load_providers(workspace_config_path=args.workspace_config)
explicit_provider = args.provider or os.environ.get("MODEL_PROVIDER", "")
model = args.model or os.environ.get("MODEL", "")
try:
picked = resolve_provider(
model or None, providers,
explicit_provider=explicit_provider or None,
)
except ValueError as exc:
print(f"[codex-provider] {exc}", file=sys.stderr)
return 2
try:
written = write_config_toml(
picked,
model=model or None,
codex_home=args.codex_home or None,
)
except ValueError as exc:
print(f"[codex-provider] {exc}", file=sys.stderr)
return 2
if written:
print(
f"[codex-provider] wrote {written} provider={picked['name']} "
f"auth_mode={picked['auth_mode']}"
)
else:
print(
f"[codex-provider] no config.toml override needed "
f"(provider={picked['name']} auth_mode={picked['auth_mode']}); "
"codex will use its built-in OpenAI/Responses path"
)
return 0
if __name__ == "__main__":
sys.exit(main())