Studio: shareable per-checkpoint preview links (#6486)
* checkpoint preview endpoint * harden new preview endpoints * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * address review * Studio preview: pin adapter, guard streaming submit, robust copy-link Harden the public per-checkpoint preview surface: - Pin use_adapter=True in the preview payload sanitizer. Otherwise an unauthenticated /p caller can POST use_adapter=false, which calls disable_adapter_layers() on the shared in-memory model without restoring it; since load_model skips reloads for the same checkpoint, every later visitor (the page never sends the field) keeps getting base-model output instead of the fine-tuned checkpoint. Forcing it on also re-enables a previously disabled adapter and no-ops on merged checkpoints. - Ignore preview-page submits while a response is streaming. The send button was disabled but the Enter handler still called requestSubmit(), so a second request could start before the first reply landed in msgs and reorder the chat history. Both the keydown and submit handlers now honor the disabled button. - Keep the cloudflare-URL polling loop alive across transient startup fetch errors instead of letting one rejection halt it. - Build the copy-link from a backend preview_ref (output dir relative to outputs_root, gated on previewability and the two-segment /p route limit) so a nested output dir no longer copies a basename-only link that 404s. Expose preview_ref on training run summaries. Add route-level security tests (path traversal, payload sanitization, asset containment, CSP header, HTML title escaping, streaming lock held until drained) and preview_ref unit tests. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Studio preview: Safari-safe submit and adapter pin only for LoRA Follow-ups from cross-browser and route simulations: - Preview page: send the message from a shared send() helper called by both the form submit and the Enter key, instead of form.requestSubmit(). The latter throws on Safari < 16 and older iOS, which broke Enter-to-send there. Verified across Chromium, Firefox and WebKit with Playwright. - Only pin use_adapter=True when the resolved checkpoint is a LoRA adapter (adapter_config.json present); for a merged checkpoint strip it to None. A merged model has no adapter to toggle, so forcing it on only produced a per-request "not a PeftModel" warning. The cross-request base-model contamination fix still holds for LoRA previews. Add a merged-checkpoint test asserting use_adapter is stripped to None. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Studio preview: trim verbose comments Tighten comments across the preview routes, page, checkpoint helpers, and tests to short single-line notes; drop ones that just restate the code. No behavior change (verified comment/docstring-only with comment_tools.py check). * Harden preview routes for PR #6486 - Return a generic 400 detail on a rejected preview path so the public /p route never echoes the absolute install path (the real reason is logged server-side instead). - Strip confirm_tool_calls, session_id and rag_scope in the preview payload sanitizer so the public surface stays inert regardless of the tool gate. - Use Path.is_relative_to for the asset containment check, matching the rest of the codebase. - Add img-src 'self' and font-src 'self' to the preview page CSP. - Preview page: on a mid-stream error keep the streamed text, flag the break, and restore the prompt so the user can retry; drop the unused --font-sans var. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Lee Jackson <130007945+Imagineer99@users.noreply.github.com> Co-authored-by: Daniel Han <danielhanchen@gmail.com>
This commit is contained in:
parent
c7c353d740
commit
e5cf956601
15 changed files with 1206 additions and 10 deletions
|
|
@ -125,6 +125,12 @@ def is_anthropic_path(path: str) -> bool:
|
|||
return path.startswith("/v1/messages")
|
||||
|
||||
|
||||
def wants_api_error_envelope(path: str) -> bool:
|
||||
"""True for the OpenAI/Anthropic-compatible surfaces: the ``/v1/*`` mount and
|
||||
the preview ``/p/<run>[/<ckpt>]/v1/*`` mount."""
|
||||
return path.startswith("/v1/") or (path.startswith("/p/") and "/v1/" in path)
|
||||
|
||||
|
||||
def error_body_for_path(
|
||||
path,
|
||||
message,
|
||||
|
|
@ -183,15 +189,16 @@ def _summarize_validation_errors(errors) -> tuple:
|
|||
def install_api_error_handlers(app) -> None:
|
||||
"""Register validation + HTTPException handlers that emit ``/v1/*`` envelopes.
|
||||
|
||||
Both handlers are global but only transform responses for paths starting with
|
||||
``/v1/``. Non-``/v1/`` paths reproduce FastAPI's default ``{"detail": ...}``
|
||||
behavior exactly so the Studio frontend keeps working.
|
||||
Both handlers are global but only transform responses for OpenAI/Anthropic-
|
||||
compatible surfaces (see :func:`wants_api_error_envelope`: the ``/v1/*`` mount
|
||||
and the preview ``/p/.../v1/*`` mount). Every other path reproduces FastAPI's
|
||||
default ``{"detail": ...}`` behavior exactly so the Studio frontend keeps working.
|
||||
"""
|
||||
|
||||
@app.exception_handler(RequestValidationError)
|
||||
async def _handle_validation_error(request, exc):
|
||||
path = request.url.path
|
||||
if path.startswith("/v1/"):
|
||||
if wants_api_error_envelope(path):
|
||||
summary, param = _summarize_validation_errors(exc.errors())
|
||||
return JSONResponse(
|
||||
status_code = 400,
|
||||
|
|
@ -211,7 +218,7 @@ def install_api_error_handlers(app) -> None:
|
|||
# default http_exception_handler, which returns a bodiless Response.
|
||||
if not is_body_allowed_for_status_code(exc.status_code):
|
||||
return Response(status_code = exc.status_code, headers = headers)
|
||||
if path.startswith("/v1/"):
|
||||
if wants_api_error_envelope(path):
|
||||
detail = exc.detail
|
||||
# Already a fully-formed envelope: pass through untouched.
|
||||
if isinstance(detail, dict) and ("error" in detail or detail.get("type") == "error"):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue