Studio: allow --secure with --api-only (headless secure API server) and add --api-only to unsloth studio run - #6591
Conversation
…and add --api-only to `unsloth studio run` --secure exposes ONLY the Cloudflare link (it forces a loopback bind), but _cloudflare_tunnel_should_start gated the tunnel on `not api_only`, so `run.py --secure --api-only` started no tunnel and then fail-closed with "A secure Cloudflare link is not allowed". That blocked the natural headless use: serve just the API (no web UI) over the authenticated tunnel. Make --secure start the tunnel regardless of api_only (the non-secure path is unchanged: tunnel only a 0.0.0.0 bind, never api-only Tauri or Colab). Then expose --api-only on `unsloth studio run` and forward it through both the re-exec args and the in-venv run_server call, so `unsloth studio run --secure --api-only --model ...` is a one-liner secure API server. Verified end to end: `run.py --secure --api-only` now brings up the tunnel and serves /api/health over it (200), with / returning 404 (no UI). Tests: update the tunnel-gate truth table (secure+api-only now tunnels; secure+colab still does not) and add --api-only registration + re-exec/in-venv forwarding coverage to the run CLI tests.
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 40f49256e7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| api_only: bool = typer.Option( | ||
| False, | ||
| "--api-only", | ||
| help = "Serve only the API (no web UI), for a headless model server. " | ||
| "Pairs with --secure to expose the API over the Cloudflare link alone.", |
There was a problem hiding this comment.
Reject parent-level --api-only before run
When the user places the new flag before the subcommand, e.g. unsloth studio --api-only run --secure --model ..., Typer consumes --api-only in the parent studio_default callback, whose subcommand guard currently rejects --parallel, --secure, etc. but not --api-only; the run command therefore receives api_only=False and serves the web UI, potentially over the secure tunnel, despite the user requesting a headless API server. Please add the same parent-level rejection/redirect guidance for --api-only that the surrounding callback uses for the other run-specific flags.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 25f6bc9. The parent studio_default callback now rejects --api-only when a subcommand is invoked, matching the existing guidance for --parallel/--secure/--verbose: it exits 2 and tells the user to put the flag after the subcommand (unsloth studio run --api-only ...). Added test_studio_default_rejects_api_only_when_subcommand_invoked.
| if is_colab or not cloudflare: | ||
| return False | ||
| if secure: | ||
| return True |
There was a problem hiding this comment.
Keep secure API-only CORS usable remotely
With --secure --api-only, this branch now publishes an API-only server through Cloudflare, but run_server(api_only=True) sets UNSLOTH_API_ONLY=1 before importing main, and studio/backend/main.py:858-867 switches CORS from * to a Tauri/localhost-only allowlist. Browser-based remote API clients calling the new Cloudflare URL from any other origin will fail preflight even with a valid API key, whereas --secure without --api-only works. If this mode is meant to support remote API callers, keep the UI disabled without enabling the Tauri-only CORS profile or allow the tunnel origin.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 25f6bc9. Secure api-only is a remote server behind Cloudflare, so it now keeps the any-origin CORS profile; the Tauri-only lockdown still applies to plain local api-only. The decision is factored into cors_origins_for_mode(api_only, secure) (returns the Tauri allowlist only when api_only and not secure, else [""]), and run_server exports UNSLOTH_SECURE before importing main so main.py can tell the two apart. With allow_credentials=True and [""], Starlette reflects the request Origin, so remote browser preflight succeeds. Added test_cors_origins_for_mode and test_run_server_exports_secure_env_for_cors.
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
|
@codex review |
- Reject --api-only on the parent `unsloth studio` group when a subcommand is invoked, with the same redirect guidance used for --parallel/--secure; otherwise the flag was silently dropped and the UI served anyway. - Keep CORS any-origin for secure api-only serving: that mode publishes the API over Cloudflare for remote browser clients, so the Tauri-only lockdown (still applied to plain local api-only) would break preflight. Factored the decision into cors_origins_for_mode() and gate it on api_only and not secure; run_server exports UNSLOTH_SECURE before importing main.
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
for more information, see https://pre-commit.ci
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3c38e30628
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if secure: | ||
| return True |
There was a problem hiding this comment.
Allow remote CORS origins for secure API-only tunnels
When --secure --api-only now reaches this branch, run_server sets UNSLOTH_API_ONLY=1 before importing main.py; main.py then restricts CORS to Tauri/localhost origins only. Browser-based clients using the advertised Cloudflare URL (for example an Origin on https://*.trycloudflare.com or another web app) will fail the authenticated API preflight even though this mode is intended to expose the headless API remotely. Please use the non-Tauri CORS policy for secure headless serving or add appropriate remote/tunnel origins.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
This review ran against 3c38e30, which is the commit just before the fix. It is already addressed in 25f6bc9 (now 25f6bc9..a652338 after a pre-commit.ci reformat): secure api-only uses the non-Tauri policy. cors_origins_for_mode(api_only, secure) returns the Tauri allowlist only when api_only and not secure, otherwise ["*"], and run_server exports UNSLOTH_SECURE before importing main so --secure --api-only resolves to the any-origin profile (same one --secure without --api-only already uses). Covered by test_cors_origins_for_mode and test_run_server_exports_secure_env_for_cors.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a652338a32
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
|
|
||
| @pytest.mark.parametrize("extra,present", [(["--api-only"], True), ([], False)]) | ||
| def test_reexec_forwards_api_only(monkeypatch, extra, present): |
There was a problem hiding this comment.
Use a unique name for the api-only re-exec test
When pytest imports this module, this second test_reexec_forwards_api_only definition replaces the earlier one added above, so the explicit --secure --api-only re-exec case is never collected. Rename or merge one of these tests so the headline secure headless path is actually exercised.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in ce11c5d. That duplicate definition was shadowing the parametrized test_reexec_forwards_api_only below it, so the secure case was never collected. I removed the duplicate (and the redundant omit-by-default test, already covered by the parametrized ([], False) case) and folded the secure headless path in as a param: (['--secure', '--api-only'], True). One canonical test now, and the secure case is collected.
| host = host, | ||
| port = port, | ||
| silent = True, | ||
| api_only = api_only, |
There was a problem hiding this comment.
Do not leak TAURI_PORT from headless run output
When unsloth studio run --api-only reaches this in-venv path, it passes api_only=True into run_server, whose api-only branch unconditionally prints the Tauri-only TAURI_PORT=... line even though this caller uses silent=True and then prints the run subcommand's URL:/API Key: banner. This makes the new headless run --api-only mode emit an extra unrelated line before the documented output, including under --silent, which can break scripts that parse the one-liner server output; gate that port print to the desktop/Tauri launch path instead.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in ce11c5d. run_server now takes emit_tauri_port (default True, so the Tauri/desktop and studio_default paths are unchanged), and the TAURI_PORT= line is gated on api_only and emit_tauri_port. The headless run --api-only path passes emit_tauri_port=False, so it no longer prepends that machine line before the URL:/API Key: banner, including under --silent. Covered by test_run_server_emit_tauri_port_defaults_on, test_tauri_port_print_is_gated_in_source, and an emit_tauri_port=False assertion in test_in_venv_path_passes_api_only_to_run_server.
- run_server gains emit_tauri_port (default True, unchanged for the Tauri/ desktop path). The new headless `run --api-only` path passes False so the Tauri-only TAURI_PORT= line no longer prepends the documented URL/API key banner (it ran even under --silent and could break one-liner parsers). - Remove a duplicate test_reexec_forwards_api_only that shadowed the parametrized one; fold the --secure --api-only case into it so the secure headless path is actually collected.
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
|
@codex review |
|
Codex Review: Didn't find any major issues. Bravo. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Summary
--secureis meant to expose Studio ONLY through the authenticated Cloudflare HTTPS link (it forces a loopback bind and fails closed if the tunnel can't start). But the tunnel start gate excluded api-only mode, so--secure --api-onlystarted no tunnel and then fell on its own fail-closed check:That blocked the natural headless use case: serve just the API (no web UI) over the secure tunnel for remote API calling.
Root cause
The
not api_onlyterm was there to keep the Tauri desktop app (which loads the API on loopback) from tunnelling. But it also suppressed the tunnel under--secure, where the tunnel is the only way in, so secure + api-only could never start.Changes
studio/backend/run.py:_cloudflare_tunnel_should_startnow starts the tunnel whenever--secureis set, even api-only. The non-secure path is unchanged: tunnel only a0.0.0.0bind, never api-only (Tauri) or Colab.unsloth_cli/commands/studio.py: add--api-onlytounsloth studio runand forward it through both the re-exec args and the in-venvrun_server(api_only=...)call.unsloth studio run --secure --api-only --model ...is now a one-liner headless secure API server.Behaviour
run.py --secure(UI)run.py --secure --api-onlyrun.py --api-only(Tauri)run.py -H 0.0.0.0--secureon ColabTesting
--api-onlyregistration plus re-exec and in-venvrun_serverforwarding coverage to the run CLI tests.Verified end to end on a host where the quick tunnel can establish:
run.py --secure --api-onlybrings up the tunnel and serves/api/healthover it (200), with/returning 404 (no UI).