Skip to content

Commit 2ef7d98

Browse files
committed
ucode: add --no-preflight to skip per-launch auth/gateway re-validation
Every `ucode <agent>` launch re-runs configure_shared_state, which does an auth login check, a token fetch, an AI Gateway v2 probe, and per-launch model discovery — ~5-10s of network round-trips — even when a prior `ucode configure` already validated all of it. Managed/headless launchers (e.g. omnigent) that run configure once and then launch repeatedly pay this cost on every turn. Add a launch-only `--no-preflight` flag (distinct from the configure-only `--skip-validate`, which skips the post-configure model smoke test). It threads through _launch_tool as configure_shared_state(no_preflight=True), which skips the auth+gateway block and model discovery entirely — the PAT/bearer is already exported by apply_pat_environment and the gateway was verified by the earlier configure. The local profile resolution, base-URL rebuild, and state persistence still run, and previously-discovered model lists are preserved rather than clobbered with empties. Wired into every launch command (codex/claude/gemini/opencode/copilot/pi). Co-authored-by: Isaac
1 parent a0a0d1c commit 2ef7d98

2 files changed

Lines changed: 228 additions & 75 deletions

File tree

src/ucode/cli.py

Lines changed: 124 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ def configure_shared_state(
216216
force_login: bool = False,
217217
use_pat: bool | None = None,
218218
skip_model_discovery: bool = False,
219+
no_preflight: bool = False,
219220
) -> dict:
220221
"""Log into Databricks, enforce AI Gateway v2, fetch model lists, persist state.
221222
@@ -229,44 +230,60 @@ def configure_shared_state(
229230
``--profile`` to every CLI invocation so ambiguous `~/.databrickscfg`
230231
entries (e.g. DEFAULT and a named profile both pointing at the same host)
231232
don't error out. If ``None``, we resolve it from the host after login.
233+
If no_preflight is True, trust a prior ``ucode configure`` and skip the
234+
per-launch network round-trips entirely: no auth login, no token fetch, no
235+
AI Gateway probe, and no model discovery. The PAT/bearer is already exported
236+
(``apply_pat_environment`` in ``_launch_tool``) and the gateway was verified
237+
by that earlier configure, so a managed/headless re-launch needs none of it.
238+
The local profile/base-url derivation and state persistence still run.
232239
"""
233240
workspace = normalize_workspace_url(workspace)
234241
prior_state = load_state()
235242
previous_workspace = prior_state.get("workspace")
236243
if use_pat is None:
237244
use_pat = bool(prior_state.get("use_pat")) and previous_workspace == workspace
238245
fetch_all = tools is None
239-
if use_pat:
240-
if not profile:
241-
raise RuntimeError(
242-
"--use-pat requires a Databricks CLI profile. Pass one via `--profiles <name>`."
243-
)
244-
pat = resolve_pat_token(profile)
245-
if not pat:
246-
raise RuntimeError(
247-
f"--use-pat: profile '{profile}' has no personal access token in "
248-
"~/.databrickscfg (its auth_type must be `pat`). Add a `token = <PAT>` "
249-
f"entry under [{profile}], or re-run without --use-pat to use OAuth."
250-
)
251-
# Export the PAT for this process and launched agent subprocesses so
252-
# every token fetch takes the static-bearer path. ensure_pat_bearer
253-
# keeps a non-empty pre-set bearer (CI escape hatch) but treats an
254-
# empty one as absent, so it never shadows the PAT. Pass the validated
255-
# token to avoid re-reading ~/.databrickscfg.
256-
ensure_pat_bearer(profile, pat)
257-
ensure_databricks_auth(workspace, profile)
258-
elif force_login:
259-
run_databricks_login(workspace, profile)
246+
token: str | None = None
247+
if no_preflight:
248+
# A prior `ucode configure` already logged in and verified the AI Gateway,
249+
# and the PAT/bearer is already exported (apply_pat_environment), so skip
250+
# the auth login, token fetch, and gateway probe. Still resolve the
251+
# profile locally from ~/.databrickscfg so downstream CLI calls
252+
# disambiguate; no token is needed since model discovery is skipped too.
253+
if profile is None:
254+
profile = find_profile_name_for_host(workspace)
260255
else:
261-
ensure_databricks_auth(workspace, profile)
262-
# After login the profile exists in ~/.databrickscfg, so a host->profile
263-
# lookup is reliable. Persist it so subsequent CLI calls disambiguate.
264-
if profile is None:
265-
profile = find_profile_name_for_host(workspace)
266-
with spinner("Verifying Unity AI Gateway..."):
267-
token = get_databricks_token(workspace, profile)
268-
ensure_ai_gateway_v2(workspace, token)
269-
print_success("Unity AI Gateway detected")
256+
if use_pat:
257+
if not profile:
258+
raise RuntimeError(
259+
"--use-pat requires a Databricks CLI profile. Pass one via `--profiles <name>`."
260+
)
261+
pat = resolve_pat_token(profile)
262+
if not pat:
263+
raise RuntimeError(
264+
f"--use-pat: profile '{profile}' has no personal access token in "
265+
"~/.databrickscfg (its auth_type must be `pat`). Add a `token = <PAT>` "
266+
f"entry under [{profile}], or re-run without --use-pat to use OAuth."
267+
)
268+
# Export the PAT for this process and launched agent subprocesses so
269+
# every token fetch takes the static-bearer path. ensure_pat_bearer
270+
# keeps a non-empty pre-set bearer (CI escape hatch) but treats an
271+
# empty one as absent, so it never shadows the PAT. Pass the validated
272+
# token to avoid re-reading ~/.databrickscfg.
273+
ensure_pat_bearer(profile, pat)
274+
ensure_databricks_auth(workspace, profile)
275+
elif force_login:
276+
run_databricks_login(workspace, profile)
277+
else:
278+
ensure_databricks_auth(workspace, profile)
279+
# After login the profile exists in ~/.databrickscfg, so a host->profile
280+
# lookup is reliable. Persist it so subsequent CLI calls disambiguate.
281+
if profile is None:
282+
profile = find_profile_name_for_host(workspace)
283+
with spinner("Verifying Unity AI Gateway..."):
284+
token = get_databricks_token(workspace, profile)
285+
ensure_ai_gateway_v2(workspace, token)
286+
print_success("Unity AI Gateway detected")
270287

271288
want_claude = (
272289
fetch_all or "claude" in tools or "opencode" in tools or "copilot" in tools or "pi" in tools
@@ -284,39 +301,45 @@ def configure_shared_state(
284301
codex_models = []
285302
oss_models = []
286303
web_search_model: str | None = None
287-
if skip_model_discovery:
288-
# Provider mode: the agent routes through a Model Provider Service and
289-
# pins no Databricks model, so the full family discovery is unused. Web
290-
# search (claude only) still needs one Responses-capable model, so fetch
291-
# just that with a single call.
292-
if want_claude:
293-
with spinner("Fetching web search model..."):
294-
ws_models, _ = discover_codex_models(workspace, token)
295-
if ws_models:
296-
web_search_model = ws_models[0]
297-
else:
298-
# UC-first, best-effort: one UC model-services call yields all families as
299-
# `system.ai.<model-name>` ids, bucketed by name. If a family comes back
300-
# empty (workspace without UC model-services, or the listing failed), fall
301-
# back to the per-family AI Gateway listing for that family only.
302-
with spinner("Fetching available models..."):
303-
ms_claude, ms_codex, ms_gemini, ms_oss, ms_reason = discover_model_services(
304-
workspace, token
305-
)
304+
# Under no_preflight no discovery runs at all — the prior configure's
305+
# model lists are trusted (and preserved below, not overwritten).
306+
if not no_preflight:
307+
# token is always set by the auth+gateway block above when validating.
308+
assert token is not None
309+
if skip_model_discovery:
310+
# Provider mode: the agent routes through a Model Provider Service and
311+
# pins no Databricks model, so the full family discovery is unused. Web
312+
# search (claude only) still needs one Responses-capable model, so fetch
313+
# just that with a single call.
306314
if want_claude:
307-
claude_models, claude_reason = ms_claude, ms_reason
308-
if not claude_models:
309-
claude_models, claude_reason = discover_claude_models(workspace, token)
310-
if want_gemini:
311-
gemini_models, gemini_reason = ms_gemini, ms_reason
312-
if not gemini_models:
313-
gemini_models, gemini_reason = discover_gemini_models(workspace, token)
314-
if want_codex:
315-
codex_models, codex_reason = ms_codex, ms_reason
316-
if not codex_models:
317-
codex_models, codex_reason = discover_codex_models(workspace, token)
318-
if want_oss:
319-
oss_models, oss_reason = ms_oss, ms_reason
315+
with spinner("Fetching web search model..."):
316+
ws_models, _ = discover_codex_models(workspace, token)
317+
if ws_models:
318+
web_search_model = ws_models[0]
319+
else:
320+
# UC-first, best-effort: one UC model-services call yields all families
321+
# as `system.ai.<model-name>` ids, bucketed by name. If a family comes
322+
# back empty (workspace without UC model-services, or the listing
323+
# failed), fall back to the per-family AI Gateway listing for that
324+
# family only.
325+
with spinner("Fetching available models..."):
326+
ms_claude, ms_codex, ms_gemini, ms_oss, ms_reason = discover_model_services(
327+
workspace, token
328+
)
329+
if want_claude:
330+
claude_models, claude_reason = ms_claude, ms_reason
331+
if not claude_models:
332+
claude_models, claude_reason = discover_claude_models(workspace, token)
333+
if want_gemini:
334+
gemini_models, gemini_reason = ms_gemini, ms_reason
335+
if not gemini_models:
336+
gemini_models, gemini_reason = discover_gemini_models(workspace, token)
337+
if want_codex:
338+
codex_models, codex_reason = ms_codex, ms_reason
339+
if not codex_models:
340+
codex_models, codex_reason = discover_codex_models(workspace, token)
341+
if want_oss:
342+
oss_models, oss_reason = ms_oss, ms_reason
320343
opencode_models: dict[str, list[str]] = {}
321344
if claude_models:
322345
opencode_models["anthropic"] = list(claude_models.values())
@@ -341,7 +364,11 @@ def configure_shared_state(
341364
else:
342365
state.pop("use_pat", None)
343366
state["base_urls"] = build_shared_base_urls(workspace)
344-
if skip_model_discovery:
367+
if no_preflight:
368+
# No discovery ran; preserve the model lists the prior configure wrote
369+
# (they'd otherwise be clobbered with empties below).
370+
pass
371+
elif skip_model_discovery:
345372
# Don't clobber any previously-discovered Databricks model lists; provider
346373
# mode just doesn't refresh or use them. Persist the web-search model so
347374
# claude's web_search MCP keeps working through the normal gateway.
@@ -822,7 +849,12 @@ def _auto_configure_tool(tool: str) -> None:
822849
raise RuntimeError(f"{spec['display']} validation failed — config reverted.")
823850

824851

825-
def _launch_tool(tool_name: str, ctx: typer.Context, provider: str | None = None) -> None:
852+
def _launch_tool(
853+
tool_name: str,
854+
ctx: typer.Context,
855+
provider: str | None = None,
856+
no_preflight: bool = False,
857+
) -> None:
826858
try:
827859
tool = normalize_tool(tool_name)
828860
existing = load_state()
@@ -860,6 +892,7 @@ def _launch_tool(tool_name: str, ctx: typer.Context, provider: str | None = None
860892
profile=state.get("profile"),
861893
tools=[tool],
862894
skip_model_discovery=bool(provider),
895+
no_preflight=no_preflight,
863896
)
864897
if provider:
865898
# Routing through a Model Provider Service pins no Databricks model;
@@ -892,6 +925,21 @@ def _launch_tool(tool_name: str, ctx: typer.Context, provider: str | None = None
892925
raise typer.Exit(130) from None
893926

894927

928+
# Launch-only escape hatch for managed/headless launchers (e.g. omnigent) that
929+
# have already run `ucode configure`: skip the ~5-10s per-launch auth + AI
930+
# Gateway re-validation. Distinct from the configure-only `--skip-validate`,
931+
# which skips the model smoke test.
932+
NoPreflightOption = Annotated[
933+
bool,
934+
typer.Option(
935+
"--no-preflight",
936+
help="Skip the per-launch Databricks auth + AI Gateway re-validation, trusting a "
937+
"prior `ucode configure` (used by managed/headless launchers where configure "
938+
"already ran).",
939+
),
940+
]
941+
942+
895943
@app.command("codex", context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
896944
def codex_cmd(
897945
ctx: typer.Context,
@@ -904,9 +952,10 @@ def codex_cmd(
904952
"before any `--` separator.",
905953
),
906954
] = None,
955+
no_preflight: NoPreflightOption = False,
907956
) -> None:
908957
"""Launch Codex via Databricks."""
909-
_launch_tool("codex", ctx, provider=provider)
958+
_launch_tool("codex", ctx, provider=provider, no_preflight=no_preflight)
910959

911960

912961
@app.command("claude", context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
@@ -921,35 +970,36 @@ def claude_cmd(
921970
"before any `--` separator.",
922971
),
923972
] = None,
973+
no_preflight: NoPreflightOption = False,
924974
) -> None:
925975
"""Launch Claude Code via Databricks."""
926-
_launch_tool("claude", ctx, provider=provider)
976+
_launch_tool("claude", ctx, provider=provider, no_preflight=no_preflight)
927977

928978

929979
@app.command("gemini", context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
930-
def gemini_cmd(ctx: typer.Context) -> None:
980+
def gemini_cmd(ctx: typer.Context, no_preflight: NoPreflightOption = False) -> None:
931981
"""Launch Gemini CLI via Databricks."""
932-
_launch_tool("gemini", ctx)
982+
_launch_tool("gemini", ctx, no_preflight=no_preflight)
933983

934984

935985
@app.command(
936986
"opencode", context_settings={"allow_extra_args": True, "ignore_unknown_options": True}
937987
)
938-
def opencode_cmd(ctx: typer.Context) -> None:
988+
def opencode_cmd(ctx: typer.Context, no_preflight: NoPreflightOption = False) -> None:
939989
"""Launch OpenCode via Databricks."""
940-
_launch_tool("opencode", ctx)
990+
_launch_tool("opencode", ctx, no_preflight=no_preflight)
941991

942992

943993
@app.command("copilot", context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
944-
def copilot_cmd(ctx: typer.Context) -> None:
994+
def copilot_cmd(ctx: typer.Context, no_preflight: NoPreflightOption = False) -> None:
945995
"""Launch GitHub Copilot CLI via Databricks."""
946-
_launch_tool("copilot", ctx)
996+
_launch_tool("copilot", ctx, no_preflight=no_preflight)
947997

948998

949999
@app.command("pi", context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
950-
def pi_cmd(ctx: typer.Context) -> None:
1000+
def pi_cmd(ctx: typer.Context, no_preflight: NoPreflightOption = False) -> None:
9511001
"""Launch Pi coding agent via Databricks."""
952-
_launch_tool("pi", ctx)
1002+
_launch_tool("pi", ctx, no_preflight=no_preflight)
9531003

9541004

9551005
@configure_app.callback(invoke_without_command=True)

0 commit comments

Comments
 (0)