|
16 | 16 | configure_tool, |
17 | 17 | ensure_bootstrap_dependencies, |
18 | 18 | ensure_provider_state, |
| 19 | + harness_for_model, |
19 | 20 | install_tool_binary, |
20 | 21 | normalize_tool, |
21 | 22 | provider_permission_error, |
|
48 | 49 | list_profile_entries, |
49 | 50 | list_tool_provider_services, |
50 | 51 | normalize_workspace_url, |
| 52 | + recommend_coding_agent_models, |
51 | 53 | resolve_pat_token, |
52 | 54 | run_databricks_login, |
53 | 55 | ) |
@@ -1034,6 +1036,124 @@ def pi_cmd(ctx: typer.Context, skip_preflight: SkipPreflightOption = False) -> N |
1034 | 1036 | _launch_tool("pi", ctx, skip_preflight=skip_preflight) |
1035 | 1037 |
|
1036 | 1038 |
|
| 1039 | +def _available_models(state: dict) -> list[str]: |
| 1040 | + """Collect every Databricks model id discovered for this workspace. |
| 1041 | +
|
| 1042 | + Flattens the per-family state buckets (claude by family alias, plus the flat |
| 1043 | + codex/gemini/oss lists) into a single de-duplicated, order-preserving list to |
| 1044 | + send to the recommendModel endpoint. |
| 1045 | + """ |
| 1046 | + models: list[str] = [] |
| 1047 | + models.extend((state.get("claude_models") or {}).values()) |
| 1048 | + for key in ("codex_models", "gemini_models", "oss_models"): |
| 1049 | + models.extend(state.get(key) or []) |
| 1050 | + seen: set[str] = set() |
| 1051 | + unique: list[str] = [] |
| 1052 | + for model in models: |
| 1053 | + if model and model not in seen: |
| 1054 | + seen.add(model) |
| 1055 | + unique.append(model) |
| 1056 | + return unique |
| 1057 | + |
| 1058 | + |
| 1059 | +def _run_session(ctx: typer.Context, skip_preflight: bool = False) -> None: |
| 1060 | + """Recommend a model for the user's tier, then launch the matching harness. |
| 1061 | +
|
| 1062 | + Discovers the workspace's models, asks the AI Gateway's recommendModel |
| 1063 | + endpoint which ones the caller's tier allows, lets the user pick one, maps it |
| 1064 | + to a harness (claude/codex/gemini) and launches that tool pinned to the model. |
| 1065 | + """ |
| 1066 | + try: |
| 1067 | + existing = load_state() |
| 1068 | + apply_pat_environment(existing) |
| 1069 | + # Ensure a workspace is configured. Without one, fall back to the same |
| 1070 | + # first-run configuration prompt the per-tool launchers use, defaulting to |
| 1071 | + # the codex harness for the bootstrap install. |
| 1072 | + if not existing.get("workspace"): |
| 1073 | + ensure_bootstrap_dependencies("codex", update_existing=True) |
| 1074 | + _auto_configure_tool("codex") |
| 1075 | + # Refresh model discovery for every family so recommendModel sees the full |
| 1076 | + # set the workspace exposes (tools=None => fetch_all). |
| 1077 | + state = configure_shared_state( |
| 1078 | + load_state()["workspace"], |
| 1079 | + profile=load_state().get("profile"), |
| 1080 | + skip_preflight=skip_preflight, |
| 1081 | + ) |
| 1082 | + available = _available_models(state) |
| 1083 | + if not available: |
| 1084 | + raise RuntimeError( |
| 1085 | + "No models available for this workspace. Run `ucode configure` to set it up." |
| 1086 | + ) |
| 1087 | + with spinner("Requesting recommended models..."): |
| 1088 | + token = get_databricks_token(state["workspace"], state.get("profile")) |
| 1089 | + recommended, reason = recommend_coding_agent_models( |
| 1090 | + state["workspace"], token, available |
| 1091 | + ) |
| 1092 | + if reason is not None: |
| 1093 | + raise RuntimeError(f"Could not fetch recommended models: {reason}") |
| 1094 | + if not recommended: |
| 1095 | + print_warning( |
| 1096 | + "No recommended models — use `ucode <harness>` " |
| 1097 | + "(e.g. `ucode claude` or `ucode codex`) to boot up your preferred harness." |
| 1098 | + ) |
| 1099 | + raise typer.Exit(0) |
| 1100 | + |
| 1101 | + print_section("ucode") |
| 1102 | + if len(recommended) == 1: |
| 1103 | + # Only one model recommended — no point prompting; launch it directly. |
| 1104 | + chosen = recommended[0] |
| 1105 | + print_note(f"Only one recommended model — launching {chosen}.") |
| 1106 | + else: |
| 1107 | + chosen = prompt_for_selection( |
| 1108 | + "Select a model", [(model, model) for model in recommended] |
| 1109 | + ) |
| 1110 | + if not chosen: |
| 1111 | + print_err("No model selected.") |
| 1112 | + raise typer.Exit(130) |
| 1113 | + |
| 1114 | + tool = harness_for_model(chosen) |
| 1115 | + if not tool: |
| 1116 | + raise RuntimeError( |
| 1117 | + f"No coding-agent harness maps to model '{chosen}'. " |
| 1118 | + "Supported families: claude, gpt, kimi/glm, gemini." |
| 1119 | + ) |
| 1120 | + |
| 1121 | + # A newly-picked harness may not have been configured/installed yet. |
| 1122 | + ensure_bootstrap_dependencies(tool, update_existing=True) |
| 1123 | + if tool not in (state.get("available_tools") or []): |
| 1124 | + _auto_configure_tool(tool) |
| 1125 | + state = load_state() |
| 1126 | + |
| 1127 | + state, resolved_model = resolve_launch_model(tool, state, chosen) |
| 1128 | + state = configure_tool(tool, state, resolved_model) |
| 1129 | + print_kv("Model", resolved_model or chosen) |
| 1130 | + print_kv("Harness", TOOL_SPECS[tool]["display"]) |
| 1131 | + if tool in ("gemini", "opencode", "copilot", "pi"): |
| 1132 | + print_note( |
| 1133 | + f"{TOOL_SPECS[tool]['display']} token refresh is managed automatically " |
| 1134 | + f"every 30 minutes while the session is running." |
| 1135 | + ) |
| 1136 | + print_success(f"Starting {TOOL_SPECS[tool]['display']}") |
| 1137 | + launch_agent(tool, state, ctx.args) |
| 1138 | + except typer.Exit: |
| 1139 | + # Intentional exits (empty recommendation, cancelled selection) subclass |
| 1140 | + # RuntimeError, so re-raise them before the error handler below rewrites |
| 1141 | + # them to a generic exit code 1. |
| 1142 | + raise |
| 1143 | + except RuntimeError as exc: |
| 1144 | + print_err(str(exc)) |
| 1145 | + raise typer.Exit(1) from None |
| 1146 | + except KeyboardInterrupt: |
| 1147 | + print_err("Interrupted.") |
| 1148 | + raise typer.Exit(130) from None |
| 1149 | + |
| 1150 | + |
| 1151 | +@app.command("run", context_settings={"allow_extra_args": True, "ignore_unknown_options": True}) |
| 1152 | +def run_cmd(ctx: typer.Context, skip_preflight: SkipPreflightOption = False) -> None: |
| 1153 | + """Recommend a model for your usage tier, then launch the matching harness.""" |
| 1154 | + _run_session(ctx, skip_preflight=skip_preflight) |
| 1155 | + |
| 1156 | + |
1037 | 1157 | @configure_app.callback(invoke_without_command=True) |
1038 | 1158 | def configure( |
1039 | 1159 | ctx: typer.Context, |
|
0 commit comments