Skip to content

Commit da55d62

Browse files
authored
Merge pull request #18 from zhujian0805/main
Add Codex multi-provider profile config
2 parents 6e08da1 + ed6d05e commit da55d62

File tree

10 files changed

+580
-78
lines changed

10 files changed

+580
-78
lines changed

code_assistant_manager/cli/app.py

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,3 +389,178 @@ def list_config():
389389
# Add list as shorthand commands
390390
config_app.command(name="ls", hidden=True)(list_config)
391391
config_app.command(name="l", hidden=True)(list_config)
392+
393+
394+
@config_app.command("codex-profile", short_help="Create/update a Codex profile in ~/.codex/config.toml")
395+
def codex_profile(
396+
config: Optional[str] = CONFIG_FILE_OPTION,
397+
name: Optional[str] = typer.Option(
398+
None, "--name", "-n", help="Profile name to create/update (default: model name)"
399+
),
400+
reasoning_effort: str = typer.Option(
401+
"low", "--reasoning-effort", help="profiles.<name>.model_reasoning_effort"
402+
),
403+
):
404+
"""Interactively select a provider endpoint + model, then write a Codex profile."""
405+
from pathlib import Path
406+
407+
from code_assistant_manager.config import ConfigManager
408+
from code_assistant_manager.endpoints import EndpointManager
409+
from code_assistant_manager.menu.base import Colors
410+
from code_assistant_manager.menu.model_selector import ModelSelector
411+
412+
try:
413+
cm = ConfigManager(config)
414+
cm.load_env_file()
415+
is_valid, errors = cm.validate_config()
416+
if not is_valid:
417+
typer.echo(f"{Colors.RED}✗ Configuration validation failed:{Colors.RESET}")
418+
for err in errors:
419+
typer.echo(f" - {err}")
420+
raise typer.Exit(1)
421+
422+
em = EndpointManager(cm)
423+
424+
ok, endpoint_name = em.select_endpoint("codex")
425+
if not ok or not endpoint_name:
426+
raise typer.Exit(0)
427+
428+
ok, endpoint_config = em.get_endpoint_config(endpoint_name)
429+
if not ok or not endpoint_config:
430+
raise typer.Exit(1)
431+
432+
ok, models = em.fetch_models(endpoint_name, endpoint_config)
433+
if not ok or not models:
434+
raise typer.Exit(1)
435+
436+
ok, model = ModelSelector.select_model_with_endpoint_info(
437+
models, endpoint_name, endpoint_config, "model", "codex"
438+
)
439+
if not ok or not model:
440+
raise typer.Exit(0)
441+
442+
profile_name = name or model
443+
provider_key = endpoint_name
444+
env_key = cm.get_endpoint_config(endpoint_name).get("api_key_env") or "OPENAI_API_KEY"
445+
446+
from code_assistant_manager.tools.config_writers.codex import upsert_codex_profile
447+
448+
config_path = Path.home() / ".codex" / "config.toml"
449+
try:
450+
result = upsert_codex_profile(
451+
config_path=config_path,
452+
provider=provider_key,
453+
base_url=endpoint_config.get("endpoint", ""),
454+
env_key=env_key,
455+
profile=profile_name,
456+
model=model,
457+
reasoning_effort=reasoning_effort,
458+
project_path=Path.cwd().resolve(),
459+
)
460+
except Exception as e:
461+
typer.echo(f"{Colors.RED}✗ Failed to write {config_path}: {e}{Colors.RESET}")
462+
raise typer.Exit(1)
463+
464+
if result.get("changed"):
465+
typer.echo(f"{Colors.GREEN}✓ Wrote Codex profile '{profile_name}'{Colors.RESET}")
466+
else:
467+
typer.echo(f"{Colors.GREEN}✓ Codex profile already up to date: '{profile_name}'{Colors.RESET}")
468+
typer.echo(f" Config: {config_path}")
469+
typer.echo(f" Run: codex -p {profile_name}")
470+
471+
except typer.Exit:
472+
raise
473+
except Exception as e:
474+
typer.echo(f"{Colors.RED}✗ Unexpected error: {e}{Colors.RESET}")
475+
raise typer.Exit(1)
476+
477+
478+
@config_app.command(
479+
"codex-profiles",
480+
short_help="Create/update Codex profiles for multiple providers from providers.json",
481+
)
482+
def codex_profiles(
483+
config: Optional[str] = CONFIG_FILE_OPTION,
484+
reasoning_effort: str = typer.Option(
485+
"low", "--reasoning-effort", help="profiles.<name>.model_reasoning_effort"
486+
),
487+
):
488+
"""Prompt repeatedly to configure provider+model pairs for Codex.
489+
490+
This is Droid-like: keep selecting a provider and a model (or skip), until you cancel.
491+
"""
492+
from pathlib import Path
493+
494+
from code_assistant_manager.tools.config_writers.codex import upsert_codex_profile
495+
from code_assistant_manager.config import ConfigManager
496+
from code_assistant_manager.endpoints import EndpointManager
497+
from code_assistant_manager.menu.base import Colors
498+
from code_assistant_manager.menu.model_selector import ModelSelector
499+
500+
cm = ConfigManager(config)
501+
cm.load_env_file()
502+
is_valid, errors = cm.validate_config()
503+
if not is_valid:
504+
typer.echo(f"{Colors.RED}✗ Configuration validation failed:{Colors.RESET}")
505+
for err in errors:
506+
typer.echo(f" - {err}")
507+
raise typer.Exit(1)
508+
509+
em = EndpointManager(cm)
510+
511+
endpoints = cm.get_sections(exclude_common=True)
512+
endpoints = [ep for ep in endpoints if em._is_client_supported(ep, "codex")]
513+
if not endpoints:
514+
typer.echo(f"{Colors.RED}✗ No endpoints configured for codex{Colors.RESET}")
515+
raise typer.Exit(1)
516+
517+
config_path = Path.home() / ".codex" / "config.toml"
518+
changed_any = False
519+
configured = 0
520+
521+
# Prompt providers one by one (like Droid): pick one model per provider (or skip).
522+
for endpoint_name in endpoints:
523+
ok, endpoint_config = em.get_endpoint_config(endpoint_name)
524+
if not ok or not endpoint_config:
525+
continue
526+
527+
ok, models = em.fetch_models(
528+
endpoint_name, endpoint_config, use_cache_if_available=False
529+
)
530+
if not ok or not models:
531+
continue
532+
533+
ok, model = ModelSelector.select_model_with_endpoint_info(
534+
models, endpoint_name, endpoint_config, "model", "codex"
535+
)
536+
if not ok or not model:
537+
typer.echo(f"Skipped {endpoint_name}")
538+
continue
539+
540+
profile_name = model
541+
env_key = cm.get_endpoint_config(endpoint_name).get("api_key_env") or "OPENAI_API_KEY"
542+
543+
result = upsert_codex_profile(
544+
config_path=config_path,
545+
provider=endpoint_name,
546+
base_url=endpoint_config.get("endpoint", ""),
547+
env_key=env_key,
548+
profile=profile_name,
549+
model=model,
550+
reasoning_effort=reasoning_effort,
551+
project_path=Path.cwd().resolve(),
552+
)
553+
554+
changed_any = changed_any or bool(result.get("changed"))
555+
configured += 1
556+
557+
if configured == 0:
558+
typer.echo(f"{Colors.YELLOW}! No profiles configured{Colors.RESET}")
559+
raise typer.Exit(0)
560+
561+
if changed_any:
562+
typer.echo(f"{Colors.GREEN}✓ Updated Codex profiles ({configured}){Colors.RESET}")
563+
else:
564+
typer.echo(f"{Colors.GREEN}✓ Codex config already up to date ({configured}){Colors.RESET}")
565+
566+
typer.echo(f" Config: {config_path}")

code_assistant_manager/endpoints.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -505,22 +505,32 @@ def _resolve_api_key(
505505
# 1. Check api_key_env variable
506506
api_key_env = endpoint_config.get("api_key_env", "")
507507
if api_key_env and api_key_env in os.environ:
508-
return os.environ[api_key_env]
508+
val = os.environ.get(api_key_env, "")
509+
if val:
510+
return val
509511

510512
# 2. Check dynamic env var API_KEY_<ENDPOINT_NAME>
511513
dynamic_var = f"API_KEY_{endpoint_name.upper().replace('-', '_')}"
512514
if dynamic_var in os.environ:
513-
return os.environ[dynamic_var]
515+
val = os.environ.get(dynamic_var, "")
516+
if val:
517+
return val
514518

515519
# 3. Check special cases
516520
if endpoint_name == "copilot-api" and "API_KEY_COPILOT" in os.environ:
517-
return os.environ["API_KEY_COPILOT"]
521+
val = os.environ.get("API_KEY_COPILOT", "")
522+
if val:
523+
return val
518524
if endpoint_name == "litellm" and "API_KEY_LITELLM" in os.environ:
519-
return os.environ["API_KEY_LITELLM"]
525+
val = os.environ.get("API_KEY_LITELLM", "")
526+
if val:
527+
return val
520528

521529
# 4. Check generic API_KEY
522530
if "API_KEY" in os.environ:
523-
return os.environ["API_KEY"]
531+
val = os.environ.get("API_KEY", "")
532+
if val:
533+
return val
524534

525535
# 5. Check config file value
526536
return endpoint_config.get("api_key", "")

0 commit comments

Comments
 (0)