Skip to content

Studio: allow --secure with --api-only (headless secure API server) and add --api-only to unsloth studio run - #6591

Merged
danielhanchen merged 6 commits into
mainfrom
fix-secure-api-only-tunnel
Jun 23, 2026
Merged

danielhanchen merged 6 commits into
mainfrom
fix-secure-api-only-tunnel

Conversation

@danielhanchen

Copy link
Copy Markdown
Member

Summary

--secure is 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-only started no tunnel and then fell on its own fail-closed check:

A secure Cloudflare link is not allowed, use --no-secure which provides a 0.0.0.0 link

That blocked the natural headless use case: serve just the API (no web UI) over the secure tunnel for remote API calling.

Root cause

def _cloudflare_tunnel_should_start(*, cloudflare, host, secure, api_only, is_colab):
    return cloudflare and (host == "0.0.0.0" or secure) and not api_only and not is_colab

The not api_only term 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_start now starts the tunnel whenever --secure is set, even api-only. The non-secure path is unchanged: tunnel only a 0.0.0.0 bind, never api-only (Tauri) or Colab.
  • unsloth_cli/commands/studio.py: add --api-only to unsloth studio run and forward it through both the re-exec args and the in-venv run_server(api_only=...) call. unsloth studio run --secure --api-only --model ... is now a one-liner headless secure API server.

Behaviour

invocation before after
run.py --secure (UI) tunnel up tunnel up (unchanged)
run.py --secure --api-only fail closed tunnel up, API only
run.py --api-only (Tauri) no tunnel no tunnel (unchanged)
run.py -H 0.0.0.0 tunnel up tunnel up (unchanged)
--secure on Colab no tunnel no tunnel (unchanged)

Testing

python -m pytest studio/backend/tests/test_secure_tunnel_gate.py \
                 unsloth_cli/tests/test_studio_run_parallel_flag.py -q
56 passed
  • Updated the tunnel-gate truth table: secure + api-only now tunnels; secure + Colab still does not.
  • Added --api-only registration plus re-exec and in-venv run_server forwarding coverage to the run CLI tests.

Verified end to end on a host where the quick tunnel can establish: run.py --secure --api-only brings up the tunnel and serves /api/health over it (200), with / returning 404 (no UI).

…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.
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.

@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +1034 to +1038
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.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread studio/backend/run.py
if is_colab or not cloudflare:
return False
if secure:
return True

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.

@danielhanchen

Copy link
Copy Markdown
Member Author

@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.
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread studio/backend/run.py
Comment on lines +901 to +902
if secure:
return True

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.

@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Bravo.

Reviewed commit: ce11c5df12

ℹ️ 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".

@danielhanchen
danielhanchen merged commit 76cbddb into main Jun 23, 2026
50 checks passed
@danielhanchen
danielhanchen deleted the fix-secure-api-only-tunnel branch June 23, 2026 12:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant