diff --git a/.github/scripts/run-studio-permission-browser.sh b/.github/scripts/run-studio-permission-browser.sh index 2007789035..e5a9a4c135 100755 --- a/.github/scripts/run-studio-permission-browser.sh +++ b/.github/scripts/run-studio-permission-browser.sh @@ -17,7 +17,8 @@ if [ -n "${STUDIO_PERMISSION_FRONTEND:-}" ]; then fi mkdir -p "$artifact_dir" -unsloth studio reset-password +# Wipe (not reset-password): the boot below must re-seed a fresh .bootstrap_password. +rm -rf "$studio_home/auth" UNSLOTH_API_ONLY=1 unsloth studio -H 127.0.0.1 -p "$port" "$@" \ >"$server_log" 2>&1 & studio_pid=$! diff --git a/.github/workflows/local-agent-guides-ci.yml b/.github/workflows/local-agent-guides-ci.yml index c48328e90f..0dc0cc66d7 100644 --- a/.github/workflows/local-agent-guides-ci.yml +++ b/.github/workflows/local-agent-guides-ci.yml @@ -167,7 +167,9 @@ jobs: # ── boot the server under test (factored helper) ────────────────── - name: Serve unsloth run --disable-tools (gemma-4-E4B) run: | - unsloth studio reset-password + # Wipe, not reset-password: since #7573 the reset rotates in place and + # prints the new passphrase, which would land unmasked in the job log. + rm -rf ~/.unsloth/studio/auth bash .github/scripts/serve-unsloth-run.sh \ --gguf-file "$GITHUB_WORKSPACE/gguf-cache/${GGUF_FILE}" \ --port "$STUDIO_PORT" --log-dir logs \ @@ -371,7 +373,7 @@ jobs: - name: Serve unsloth run --disable-tools (gemma-4-E4B) run: | - unsloth studio reset-password + rm -rf ~/.unsloth/studio/auth bash .github/scripts/serve-unsloth-run.sh \ --gguf-file "$GITHUB_WORKSPACE/gguf-cache/${GGUF_FILE}" \ --port "$STUDIO_PORT" --log-dir logs \ @@ -554,7 +556,7 @@ jobs: - name: Serve unsloth run --disable-tools (gemma-4-E4B) run: | - unsloth studio reset-password + rm -rf ~/.unsloth/studio/auth bash .github/scripts/serve-unsloth-run.sh \ --gguf-file "$GITHUB_WORKSPACE/gguf-cache/${GGUF_FILE}" \ --port "$STUDIO_PORT" --log-dir logs \ @@ -718,7 +720,7 @@ jobs: - name: Serve unsloth run --disable-tools (gemma-3-270m) run: | - unsloth studio reset-password + rm -rf ~/.unsloth/studio/auth bash .github/scripts/serve-unsloth-run.sh \ --model "$GGUF_REPO" --gguf-variant "$GGUF_VARIANT" \ --port "$STUDIO_PORT" --log-dir logs \ diff --git a/.github/workflows/studio-api-smoke.yml b/.github/workflows/studio-api-smoke.yml index cdf1f6bf12..1cfa66fea4 100644 --- a/.github/workflows/studio-api-smoke.yml +++ b/.github/workflows/studio-api-smoke.yml @@ -113,7 +113,8 @@ jobs: - name: Reset auth + boot Unsloth (API-only) run: | - unsloth studio reset-password + # Wipe (not reset-password): the boot below must re-seed a fresh .bootstrap_password. + rm -rf ~/.unsloth/studio/auth mkdir -p logs UNSLOTH_API_ONLY=1 unsloth studio -H 127.0.0.1 -p "$STUDIO_PORT" \ > logs/studio.log 2>&1 & diff --git a/.github/workflows/studio-backend-ci.yml b/.github/workflows/studio-backend-ci.yml index ec437e0c32..dd5efbb299 100644 --- a/.github/workflows/studio-backend-ci.yml +++ b/.github/workflows/studio-backend-ci.yml @@ -223,6 +223,16 @@ jobs: tests/studio/test_is_mlx_dispatch_gate.py \ tests/studio/test_xpu_spoof_pipeline.py + - name: CLI tests (unsloth_cli) + # unsloth_cli/tests had no CI at all: `unsloth_cli/**` was only a paths + # trigger and a ruff target, so 673 tests covering the studio launcher, + # the pre-exposure gate and the auth secret writers ran nowhere, and + # four of them had been failing on main unnoticed. + # Own step, not folded into the tests/ discovery above: pyproject's + # testpaths is tests/, and this suite needs no PYTHONPATH or CUDA spoof + # (it self-bootstraps sys.path and imports neither unsloth nor torch). + run: python -m pytest unsloth_cli/tests -q --tb=short + - name: Shell installer tests # Auto-discovered rather than allowlisted. The old hardcoded list had # silently fallen seven files behind tests/run_all.sh, including diff --git a/.github/workflows/studio-inference-smoke.yml b/.github/workflows/studio-inference-smoke.yml index c2d52eac22..c37c9555bf 100644 --- a/.github/workflows/studio-inference-smoke.yml +++ b/.github/workflows/studio-inference-smoke.yml @@ -127,7 +127,8 @@ jobs: - name: Reset auth + boot Unsloth (API-only) run: | - unsloth studio reset-password + # Wipe (not reset-password): the boot below must re-seed a fresh .bootstrap_password. + rm -rf ~/.unsloth/studio/auth mkdir -p logs UNSLOTH_API_ONLY=1 unsloth studio -H 127.0.0.1 -p "$STUDIO_PORT" \ > logs/studio.log 2>&1 & @@ -400,7 +401,7 @@ jobs: # tool_policy=None so each request's `enable_tools` field is # honoured. run: | - unsloth studio reset-password + rm -rf ~/.unsloth/studio/auth mkdir -p logs UNSLOTH_API_ONLY=1 unsloth studio -H 127.0.0.1 -p "$STUDIO_PORT" \ > logs/studio.log 2>&1 & @@ -978,7 +979,7 @@ jobs: # response_format requests aren't routed through the agentic # tool loop. run: | - unsloth studio reset-password + rm -rf ~/.unsloth/studio/auth mkdir -p logs UNSLOTH_API_ONLY=1 unsloth studio -H 127.0.0.1 -p "$STUDIO_PORT" \ > logs/studio.log 2>&1 & diff --git a/.github/workflows/studio-mac-api-smoke.yml b/.github/workflows/studio-mac-api-smoke.yml index 1968885a1d..c2307f17a1 100644 --- a/.github/workflows/studio-mac-api-smoke.yml +++ b/.github/workflows/studio-mac-api-smoke.yml @@ -101,7 +101,8 @@ jobs: - name: Reset auth + boot Unsloth (API-only) run: | - unsloth studio reset-password + # Wipe (not reset-password): the boot below must re-seed a fresh .bootstrap_password. + rm -rf ~/.unsloth/studio/auth mkdir -p logs UNSLOTH_API_ONLY=1 unsloth studio -H 127.0.0.1 -p "$STUDIO_PORT" \ > logs/studio.log 2>&1 & diff --git a/.github/workflows/studio-mac-inference-smoke.yml b/.github/workflows/studio-mac-inference-smoke.yml index ce15eed5c8..1dbf86ae98 100644 --- a/.github/workflows/studio-mac-inference-smoke.yml +++ b/.github/workflows/studio-mac-inference-smoke.yml @@ -126,7 +126,8 @@ jobs: - name: Reset auth + boot Unsloth (API-only) run: | - unsloth studio reset-password + # Wipe (not reset-password): the boot below must re-seed a fresh .bootstrap_password. + rm -rf ~/.unsloth/studio/auth mkdir -p logs UNSLOTH_API_ONLY=1 unsloth studio -H 127.0.0.1 -p "$STUDIO_PORT" \ > logs/studio.log 2>&1 & @@ -386,7 +387,7 @@ jobs: # tool_policy=None so each request's `enable_tools` field is # honoured. run: | - unsloth studio reset-password + rm -rf ~/.unsloth/studio/auth mkdir -p logs UNSLOTH_API_ONLY=1 unsloth studio -H 127.0.0.1 -p "$STUDIO_PORT" \ > logs/studio.log 2>&1 & @@ -831,7 +832,7 @@ jobs: # response_format requests aren't routed through the agentic # tool loop. run: | - unsloth studio reset-password + rm -rf ~/.unsloth/studio/auth mkdir -p logs UNSLOTH_API_ONLY=1 unsloth studio -H 127.0.0.1 -p "$STUDIO_PORT" \ > logs/studio.log 2>&1 & diff --git a/.github/workflows/studio-mac-ui-smoke.yml b/.github/workflows/studio-mac-ui-smoke.yml index 7375e9bcbf..3bed2fcdff 100644 --- a/.github/workflows/studio-mac-ui-smoke.yml +++ b/.github/workflows/studio-mac-ui-smoke.yml @@ -146,7 +146,8 @@ jobs: - name: Reset auth + boot Unsloth run: | - unsloth studio reset-password + # Wipe (not reset-password): the boot below must re-seed a fresh .bootstrap_password. + rm -rf ~/.unsloth/studio/auth mkdir -p logs UNSLOTH_API_ONLY=1 unsloth studio -H 127.0.0.1 -p "$STUDIO_PORT" \ > logs/studio.log 2>&1 & @@ -190,7 +191,7 @@ jobs: # runner's kernel briefly runs out of socket buffers, and (3) a # goto 'interrupted by another navigation' when the SPA auth # guard redirects mid-navigation. The retry FULLY resets Unsloth - # (kill, reset-password, reboot, wait /api/health, re-export + # (kill, wipe auth, reboot, wait /api/health, re-export # bootstrap pw) before re-running the script. A real test failure # (assertion / timeout) does NOT match any pattern so it bypasses # retry and surfaces immediately. @@ -213,7 +214,7 @@ jobs: echo "::warning::Playwright flake on attempt ${attempt}; resetting Unsloth and retrying..." kill "${STUDIO_PID}" 2>/dev/null || true sleep 2 - unsloth studio reset-password + rm -rf ~/.unsloth/studio/auth UNSLOTH_API_ONLY=1 unsloth studio -H 127.0.0.1 -p "$STUDIO_PORT" \ > "logs/studio_retry_${attempt}.log" 2>&1 & STUDIO_PID=$! @@ -251,7 +252,7 @@ jobs: - name: Reset auth + boot Unsloth for extra UI tests (port 18897) run: | - unsloth studio reset-password + rm -rf ~/.unsloth/studio/auth mkdir -p logs UNSLOTH_API_ONLY=1 unsloth studio -H 127.0.0.1 -p 18897 \ > logs/studio_extra.log 2>&1 & @@ -308,7 +309,7 @@ jobs: echo "::warning::Playwright flake on attempt ${attempt}; resetting Unsloth and retrying..." kill "${STUDIO_EXTRA_PID}" 2>/dev/null || true sleep 2 - unsloth studio reset-password + rm -rf ~/.unsloth/studio/auth UNSLOTH_API_ONLY=1 unsloth studio -H 127.0.0.1 -p 18897 \ > "logs/studio_extra_retry_${attempt}.log" 2>&1 & STUDIO_EXTRA_PID=$! diff --git a/.github/workflows/studio-ui-smoke.yml b/.github/workflows/studio-ui-smoke.yml index 97eb07b2d8..3a0713f301 100644 --- a/.github/workflows/studio-ui-smoke.yml +++ b/.github/workflows/studio-ui-smoke.yml @@ -115,7 +115,8 @@ jobs: - name: Reset auth + boot Unsloth run: | - unsloth studio reset-password + # Wipe (not reset-password): the boot below must re-seed a fresh .bootstrap_password. + rm -rf ~/.unsloth/studio/auth mkdir -p logs UNSLOTH_API_ONLY=1 unsloth studio -H 127.0.0.1 -p "$STUDIO_PORT" \ > logs/studio.log 2>&1 & @@ -193,7 +194,7 @@ jobs: # warm install we already did) so this adds little wall time. - name: Reset auth + boot Unsloth for extra UI tests (port 18894) run: | - unsloth studio reset-password + rm -rf ~/.unsloth/studio/auth mkdir -p logs UNSLOTH_API_ONLY=1 unsloth studio -H 127.0.0.1 -p 18894 \ > logs/studio_extra.log 2>&1 & @@ -253,7 +254,7 @@ jobs: # (RAG embedder + llama.cpp probe) stay hidden from the picker. - name: Reset auth + boot Unsloth for model-config tests (port 18898) run: | - unsloth studio reset-password + rm -rf ~/.unsloth/studio/auth mkdir -p logs UNSLOTH_API_ONLY=1 unsloth studio -H 127.0.0.1 -p 18898 \ > logs/studio_modelcfg.log 2>&1 & @@ -299,7 +300,7 @@ jobs: # earlier UI tests. No GGUF -- the bug surface is the composer. - name: Reset auth + boot Unsloth for IME / i18n tests (port 18896) run: | - unsloth studio reset-password + rm -rf ~/.unsloth/studio/auth mkdir -p logs UNSLOTH_API_ONLY=1 unsloth studio -H 127.0.0.1 -p 18896 \ > logs/studio_ime.log 2>&1 & diff --git a/.github/workflows/studio-windows-api-smoke.yml b/.github/workflows/studio-windows-api-smoke.yml index 6dbcceebbd..b328939846 100644 --- a/.github/workflows/studio-windows-api-smoke.yml +++ b/.github/workflows/studio-windows-api-smoke.yml @@ -179,7 +179,8 @@ jobs: - name: Reset auth + boot Unsloth (API-only) run: | - unsloth studio reset-password + # Wipe (not reset-password): the boot below must re-seed a fresh .bootstrap_password. + rm -rf ~/.unsloth/studio/auth mkdir -p logs UNSLOTH_API_ONLY=1 unsloth studio -H 127.0.0.1 -p "$STUDIO_PORT" \ > logs/studio.log 2>&1 & diff --git a/.github/workflows/studio-windows-inference-smoke.yml b/.github/workflows/studio-windows-inference-smoke.yml index 3ebe442f52..d821664327 100644 --- a/.github/workflows/studio-windows-inference-smoke.yml +++ b/.github/workflows/studio-windows-inference-smoke.yml @@ -229,7 +229,8 @@ jobs: - name: Reset auth + boot Unsloth (API-only) run: | - unsloth studio reset-password + # Wipe (not reset-password): the boot below must re-seed a fresh .bootstrap_password. + rm -rf ~/.unsloth/studio/auth mkdir -p logs UNSLOTH_API_ONLY=1 unsloth studio -H 127.0.0.1 -p "$STUDIO_PORT" \ > logs/studio.log 2>&1 & @@ -573,7 +574,7 @@ jobs: - name: Reset auth + boot Unsloth (API-only, default tool policy) run: | - unsloth studio reset-password + rm -rf ~/.unsloth/studio/auth mkdir -p logs UNSLOTH_API_ONLY=1 unsloth studio -H 127.0.0.1 -p "$STUDIO_PORT" \ > logs/studio.log 2>&1 & @@ -1074,7 +1075,7 @@ jobs: - name: Reset auth + boot Unsloth (API-only) run: | - unsloth studio reset-password + rm -rf ~/.unsloth/studio/auth mkdir -p logs UNSLOTH_API_ONLY=1 unsloth studio -H 127.0.0.1 -p "$STUDIO_PORT" \ > logs/studio.log 2>&1 & @@ -1546,7 +1547,7 @@ jobs: - name: Reset auth + boot Unsloth (API-only) run: | - unsloth studio reset-password + rm -rf ~/.unsloth/studio/auth mkdir -p logs UNSLOTH_API_ONLY=1 unsloth studio -H 127.0.0.1 -p "$STUDIO_PORT" \ > logs/studio.log 2>&1 & @@ -1888,8 +1889,11 @@ jobs: # (step/substep -> Write-StudioStdoutMirror / Get-StudioAnsi). $script:StudioVtOk = $false $script:UnslothVerbose = $false + # Get-HostMachineArch is reached only on the absent path, where + # Test-VCRedistInstalled consults it before trusting the System32 DLL, so + # part A passes without it and only the clean-box part fails. foreach ($fn in @('Get-StudioAnsi', 'Write-StudioStdoutMirror', 'step', 'substep', - 'Invoke-SetupCommand', 'Refresh-Environment', + 'Invoke-SetupCommand', 'Refresh-Environment', 'Get-HostMachineArch', 'Test-VCRedistInstalled', 'Ensure-VCRedist')) { $src = Get-FunctionSource -Path $setup -Name $fn if (-not $src) { throw "Function '$fn' not found in setup.ps1" } diff --git a/.github/workflows/studio-windows-ui-smoke.yml b/.github/workflows/studio-windows-ui-smoke.yml index f401f7be44..d23cca323f 100644 --- a/.github/workflows/studio-windows-ui-smoke.yml +++ b/.github/workflows/studio-windows-ui-smoke.yml @@ -297,7 +297,8 @@ jobs: - name: Reset auth + boot Unsloth run: | - unsloth studio reset-password + # Wipe (not reset-password): the boot below must re-seed a fresh .bootstrap_password. + rm -rf ~/.unsloth/studio/auth mkdir -p logs UNSLOTH_API_ONLY=1 unsloth studio -H 127.0.0.1 -p "$STUDIO_PORT" \ > logs/studio.log 2>&1 & @@ -352,7 +353,7 @@ jobs: - name: Reset auth + boot Unsloth for extra UI tests (port 18897) run: | - unsloth studio reset-password + rm -rf ~/.unsloth/studio/auth mkdir -p logs UNSLOTH_API_ONLY=1 unsloth studio -H 127.0.0.1 -p 18897 \ > logs/studio_extra.log 2>&1 & diff --git a/studio/backend/auth/authentication.py b/studio/backend/auth/authentication.py index 94df994928..2e9520827e 100644 --- a/studio/backend/auth/authentication.py +++ b/studio/backend/auth/authentication.py @@ -11,11 +11,12 @@ import jwt from .storage import ( API_KEY_PREFIX, + credential_generation, get_jwt_secret, get_user_and_secret, load_jwt_secret, save_refresh_token, - validate_api_key, + validate_api_key_with_credential, verify_refresh_token, ) @@ -54,11 +55,14 @@ def create_access_token( expires_delta: Optional[timedelta] = None, *, desktop: bool = False, + secret: Optional[str] = None, ) -> str: """ Create a signed JWT for the given subject (e.g. username). - Valid across restarts: the signing secret is stored in SQLite. + Valid across restarts: the signing secret is stored in SQLite. Callers that + already verified a credential pass ``secret`` so a rotation landing mid-request + cannot sign the token with the credential that just replaced it. """ to_encode = {"sub": subject} if desktop: @@ -69,7 +73,7 @@ def create_access_token( to_encode.update({"exp": expire}) return jwt.encode( to_encode, - _get_secret_for_subject(subject), + secret if secret is not None else _get_secret_for_subject(subject), algorithm = ALGORITHM, ) @@ -96,15 +100,28 @@ def is_desktop_access_token(token: str) -> bool: return payload.get("sub") == subject and payload.get("desktop") is True -def create_refresh_token(subject: str, *, desktop: bool = False) -> str: +def create_refresh_token( + subject: str, + *, + desktop: bool = False, + secret: Optional[str] = None, +) -> str: """ Create a random refresh token, store its hash in SQLite, and return it. Refresh tokens are opaque (not JWTs); expire after REFRESH_TOKEN_EXPIRE_DAYS. + ``secret`` stamps the token with the credential version the caller verified, + so a rotation cannot leave a token minted from the replaced credential valid. """ token = secrets.token_urlsafe(48) expires_at = datetime.now(timezone.utc) + timedelta(days = REFRESH_TOKEN_EXPIRE_DAYS) - save_refresh_token(token, subject, expires_at.isoformat(), is_desktop = desktop) + save_refresh_token( + token, + subject, + expires_at.isoformat(), + is_desktop = desktop, + secret_gen = credential_generation(secret) if secret is not None else None, + ) return token @@ -137,7 +154,22 @@ def reload_secret() -> None: async def get_current_subject(credentials: HTTPAuthorizationCredentials = Depends(security)) -> str: """Validate JWT and require the password-change flow to be completed.""" - return await _get_current_subject( + subject, _generation = await _get_current_credential( + credentials, + allow_password_change = False, + ) + return subject + + +async def get_current_credential( + credentials: HTTPAuthorizationCredentials = Depends(security), +) -> Tuple[str, Optional[str]]: + """As get_current_subject, but also returns the credential generation. + + For routes that persist a new credential and must not do so on behalf of one + a concurrent reset has revoked. + """ + return await _get_current_credential( credentials, allow_password_change = False, ) @@ -158,10 +190,11 @@ async def get_current_subject_allow_password_change( credentials: HTTPAuthorizationCredentials = Depends(security), ) -> str: """Validate JWT but allow access to the password-change endpoint.""" - return await _get_current_subject( + subject, _generation = await _get_current_credential( credentials, allow_password_change = True, ) + return subject # The literal the examples ship with; pasted unedited more often than a revoked key. @@ -179,21 +212,27 @@ def _invalid_api_key_detail(token: str) -> str: return "Invalid or expired API key" -async def _get_current_subject( +async def _get_current_credential( credentials: HTTPAuthorizationCredentials, *, allow_password_change: bool -) -> str: - """FastAPI dependency: validate the JWT and return the subject. Use on protected routes.""" +) -> Tuple[str, Optional[str]]: + """Validate the bearer and return ``(subject, credential generation)``. + + The generation is the credential version this request actually authenticated + against. Routes that persist new credentials must bind their write to it, or + a reset landing mid-request would bless what it just revoked. + """ token = credentials.credentials # --- API key path (sk-unsloth-...) --- if token.startswith(API_KEY_PREFIX): - username = validate_api_key(token) - if username is None: + verified = validate_api_key_with_credential(token) + if verified is None: raise HTTPException( status_code = status.HTTP_401_UNAUTHORIZED, detail = _invalid_api_key_detail(token), ) - return username + username, secret = verified + return username, credential_generation(secret) # --- JWT path --- subject = _decode_subject_without_verification(token) @@ -224,7 +263,7 @@ async def _get_current_subject( status_code = status.HTTP_403_FORBIDDEN, detail = "Password change required", ) - return subject + return subject, credential_generation(jwt_secret) except jwt.InvalidTokenError: raise HTTPException( status_code = status.HTTP_401_UNAUTHORIZED, diff --git a/studio/backend/auth/storage.py b/studio/backend/auth/storage.py index 35135b21eb..6cf4d44834 100644 --- a/studio/backend/auth/storage.py +++ b/studio/backend/auth/storage.py @@ -9,6 +9,7 @@ import ipaddress import os import secrets import sqlite3 +import tempfile import threading from datetime import datetime, timezone from typing import Optional, Tuple @@ -30,6 +31,97 @@ _BOOTSTRAP_PW_PATH = DB_PATH.parent / ".bootstrap_password" _bootstrap_password: Optional[str] = None +def _bootstrap_file_bytes(password: str) -> bytes: + """Exact on-disk form: the secret plus one LF. + + Bytes, not text: text mode writes CRLF on Windows, and `$(cat ...)` strips + the LF but leaves the CR attached to the credential. + """ + return (password + "\n").encode("utf-8") + + +def _persist_bootstrap_password(password: str) -> None: + """Atomically write the bootstrap password 0600, LF terminated on every OS. + + A partial write would destroy the only plaintext recovery credential. + """ + fd, tmp_name = tempfile.mkstemp( + prefix = f".{_BOOTSTRAP_PW_PATH.name}.", dir = _BOOTSTRAP_PW_PATH.parent + ) + try: + with os.fdopen(fd, "wb") as f: + f.write(_bootstrap_file_bytes(password)) + try: + os.chmod(tmp_name, 0o600) + except OSError: + pass + os.replace(tmp_name, _BOOTSTRAP_PW_PATH) + except BaseException: + try: + os.unlink(tmp_name) + except OSError: + pass + raise + + +def _normalise_bootstrap_file(raw: bytes, password: str) -> None: + """Append the LF a pre-newline release left off. + + Append-only, and only when the file is exactly the credential: + clear_bootstrap_password() may unlink or (when unlink fails, notably on + Windows while this descriptor is open) truncate through another descriptor + after we read, so a rewrite could restore revoked plaintext. An append + cannot: worst case is a lone "\\n" over a cleared file, which strips back to + no bootstrap password. Pre-newline releases wrote no terminator at all, so + that is the only shape in the wild; anything else reads fine, since every + reader strips, and is left alone. + """ + if raw != password.encode("utf-8"): + return + + # O_BINARY: without it Windows opens in text mode and turns the LF straight + # back into CRLF, the bug being fixed. + fd = os.open( + _BOOTSTRAP_PW_PATH, + os.O_WRONLY | os.O_APPEND | getattr(os, "O_BINARY", 0), + ) + try: + os.write(fd, b"\n") + try: + os.fchmod(fd, 0o600) + except (AttributeError, OSError): + # fchmod only reached Windows in 3.13. + pass + finally: + os.close(fd) + + +def _read_persisted_bootstrap_password() -> Optional[str]: + """Read the persisted password, normalising the file if it is malformed.""" + if not _BOOTSTRAP_PW_PATH.is_file(): + return None + + # No caller handles a raise, so an unreadable file has to mean "no bootstrap + # password", not a dead backend. We write UTF-8, so undecodable bytes are + # damage whose plaintext is worthless anyway. + try: + raw = _BOOTSTRAP_PW_PATH.read_bytes() + password = raw.decode("utf-8").strip() + except (OSError, UnicodeDecodeError): + return None + if not password: + return None + + # Older releases wrote no terminator; best-effort, a read-only auth dir must + # not fail startup. + if raw != _bootstrap_file_bytes(password): + try: + _normalise_bootstrap_file(raw, password) + except OSError: + pass + return password + + def generate_bootstrap_password() -> str: """Generate a 4-word diceware passphrase and persist it to disk. @@ -43,10 +135,10 @@ def generate_bootstrap_password() -> str: return _bootstrap_password # Persisted from a previous run? - if _BOOTSTRAP_PW_PATH.is_file(): - _bootstrap_password = _BOOTSTRAP_PW_PATH.read_text(encoding = "utf-8").strip() - if _bootstrap_password: - return _bootstrap_password + persisted = _read_persisted_bootstrap_password() + if persisted: + _bootstrap_password = persisted + return _bootstrap_password # First startup: generate a fresh passphrase. import diceware @@ -57,11 +149,7 @@ def generate_bootstrap_password() -> str: # Persist so the same passphrase survives restarts until password change. ensure_dir(_BOOTSTRAP_PW_PATH.parent) - _BOOTSTRAP_PW_PATH.write_text(_bootstrap_password, encoding = "utf-8") - try: - os.chmod(_BOOTSTRAP_PW_PATH, 0o600) - except OSError: - pass + _persist_bootstrap_password(_bootstrap_password) return _bootstrap_password @@ -72,19 +160,14 @@ def get_bootstrap_password() -> Optional[str]: def _load_bootstrap_password() -> Optional[str]: - """Load an existing bootstrap password without creating one.""" + """Load an existing bootstrap password without creating one. + + Upgrades take this path, not generate_bootstrap_password() + (ensure_default_admin short-circuits once the admin row exists), so it has + to normalise too. + """ global _bootstrap_password - _bootstrap_password = None - if _BOOTSTRAP_PW_PATH.is_file(): - # No caller handles a raise, so an unreadable file has to mean "no bootstrap - # password", not a dead backend. We write UTF-8, so bytes that will not - # decode are damage whose plaintext is worthless anyway. - try: - bootstrap_password = _BOOTSTRAP_PW_PATH.read_text(encoding = "utf-8").strip() - except (OSError, UnicodeDecodeError): - return _bootstrap_password - if bootstrap_password: - _bootstrap_password = bootstrap_password + _bootstrap_password = _read_persisted_bootstrap_password() return _bootstrap_password @@ -103,7 +186,7 @@ def clear_bootstrap_password() -> None: # Removal failed (Windows AV, read-only auth dir). The hash is already # committed, so don't fail the change -- but truncate the file so its # stale plaintext can't be re-seeded by generate_bootstrap_password() - # if a later reset-password deletes auth.db and re-validates it. + # if auth.db is ever recreated. try: _BOOTSTRAP_PW_PATH.write_text("", encoding = "utf-8") cleared = True @@ -138,6 +221,31 @@ def _hash_token(token: str) -> str: return hashlib.sha256(token.encode("utf-8")).hexdigest() +class CredentialRotated(Exception): + """A password reset revoked the credential this request authenticated with.""" + + +def credential_generation(jwt_secret: str) -> str: + """Marker for the credential version a refresh token was issued under. + + Every password change rotates ``jwt_secret``, so a token stamped with the + previous one is rejected even if it was inserted after the revoking DELETE. + """ + return hashlib.sha256(jwt_secret.encode("utf-8")).hexdigest() + + +def _current_secret(conn: sqlite3.Connection, username: str) -> Optional[str]: + row = conn.execute( + "SELECT jwt_secret FROM auth_user WHERE username = ?", (username,) + ).fetchone() + return row["jwt_secret"] if row else None + + +def _current_generation(conn: sqlite3.Connection, username: str) -> Optional[str]: + secret = _current_secret(conn, username) + return credential_generation(secret) if secret is not None else None + + def get_connection() -> sqlite3.Connection: """Get a connection to the auth database, creating tables if needed.""" ensure_dir(DB_PATH.parent) @@ -181,7 +289,8 @@ def get_connection() -> sqlite3.Connection: token_hash TEXT NOT NULL, username TEXT NOT NULL, expires_at TEXT NOT NULL, - is_desktop INTEGER NOT NULL DEFAULT 0 + is_desktop INTEGER NOT NULL DEFAULT 0, + secret_gen TEXT ); """ ) @@ -220,6 +329,8 @@ def get_connection() -> sqlite3.Connection: refresh_columns = {row["name"] for row in conn.execute("PRAGMA table_info(refresh_tokens)")} if "is_desktop" not in refresh_columns: conn.execute("ALTER TABLE refresh_tokens ADD COLUMN is_desktop INTEGER NOT NULL DEFAULT 0") + if "secret_gen" not in refresh_columns: + conn.execute("ALTER TABLE refresh_tokens ADD COLUMN secret_gen TEXT") conn.commit() return conn @@ -593,12 +704,22 @@ def update_password( new_password: str, *, revoke_refresh_tokens: bool = False, -) -> bool: + expect_password_hash: Optional[str] = None, +) -> Optional[str]: """Update password, clear first-login requirement, rotate JWT secret. + Returns the new JWT secret, or None when nothing was updated. Callers that + mint tokens for the caller must sign with the returned secret: re-reading it + would pick up a reset that landed between this commit and the mint. + ``revoke_refresh_tokens`` deletes the user's refresh tokens in the SAME transaction: a separate delete could fail after the password commit and leave a pre-change token still able to mint access tokens. + + ``expect_password_hash`` makes the write conditional on the credential the + caller verified still being current, so a request that checked the old + password cannot overwrite a reset that landed while it was in flight. + Returns False when the credential moved underneath it. """ from .hashing import hash_password @@ -606,21 +727,32 @@ def update_password( jwt_secret = secrets.token_urlsafe(64) conn = get_connection() try: - cursor = conn.execute( - """ - UPDATE auth_user - SET password_salt = ?, password_hash = ?, jwt_secret = ?, must_change_password = 0 - WHERE username = ? - """, - (salt, pwd_hash, jwt_secret, username), - ) + if expect_password_hash is None: + cursor = conn.execute( + """ + UPDATE auth_user + SET password_salt = ?, password_hash = ?, jwt_secret = ?, must_change_password = 0 + WHERE username = ? + """, + (salt, pwd_hash, jwt_secret, username), + ) + else: + cursor = conn.execute( + """ + UPDATE auth_user + SET password_salt = ?, password_hash = ?, jwt_secret = ?, must_change_password = 0 + WHERE username = ? AND password_hash = ? + """, + (salt, pwd_hash, jwt_secret, username, expect_password_hash), + ) if revoke_refresh_tokens and cursor.rowcount > 0: conn.execute("DELETE FROM refresh_tokens WHERE username = ?", (username,)) conn.commit() if cursor.rowcount > 0: clear_bootstrap_password() clear_desktop_secret() - return cursor.rowcount > 0 + return jwt_secret + return None finally: conn.close() @@ -631,35 +763,49 @@ def save_refresh_token( expires_at: str, *, is_desktop: bool = False, + secret_gen: Optional[str] = None, ) -> None: """ Store a hashed refresh token with its associated username and expiry. + + ``secret_gen`` binds the token to a credential version; it defaults to the + current one, and callers that already verified a credential must pass the + version they verified rather than let this re-read a rotated one. """ token_hash = _hash_token(token) conn = get_connection() try: + if secret_gen is None: + secret_gen = _current_generation(conn, username) conn.execute( """ - INSERT INTO refresh_tokens (token_hash, username, expires_at, is_desktop) - VALUES (?, ?, ?, ?) + INSERT INTO refresh_tokens (token_hash, username, expires_at, is_desktop, secret_gen) + VALUES (?, ?, ?, ?, ?) """, - (token_hash, username, expires_at, int(is_desktop)), + (token_hash, username, expires_at, int(is_desktop), secret_gen), ) conn.commit() finally: conn.close() -def consume_refresh_token(token: str) -> Optional[Tuple[str, bool]]: +def consume_refresh_token(token: str) -> Optional[Tuple[str, bool, str]]: """Atomically validate-and-delete a refresh token for single-use rotation. DELETE RETURNING fuses validate and delete into one statement so two - concurrent refresh requests cannot both consume the same token. + concurrent refresh requests cannot both consume the same token. Returns + ``(username, is_desktop, jwt_secret)``; the caller must mint the replacement + tokens against that secret so a rotation landing mid-refresh cannot issue a + post-rotation session from a pre-rotation token. """ token_hash = _hash_token(token) now = datetime.now(timezone.utc).isoformat() conn = get_connection() try: + # One transaction with the delete: an unstamped legacy row has no + # generation to compare, so reading the credential after committing would + # hand a reset's new secret to a token issued before it. + conn.execute("BEGIN IMMEDIATE") conn.execute( "DELETE FROM refresh_tokens WHERE expires_at < ?", (now,), @@ -668,15 +814,21 @@ def consume_refresh_token(token: str) -> Optional[Tuple[str, bool]]: """ DELETE FROM refresh_tokens WHERE token_hash = ? AND expires_at >= ? - RETURNING username, is_desktop + RETURNING username, is_desktop, secret_gen """, (token_hash, now), ) row = cur.fetchone() - conn.commit() if row is None: + conn.commit() return None - return row["username"], bool(row["is_desktop"]) + secret = _current_secret(conn, row["username"]) + conn.commit() + if secret is None: + return None + if row["secret_gen"] is not None and row["secret_gen"] != credential_generation(secret): + return None + return row["username"], bool(row["is_desktop"]), secret finally: conn.close() @@ -700,7 +852,7 @@ def verify_refresh_token(token: str) -> Optional[Tuple[str, bool]]: cur = conn.execute( """ - SELECT id, username, expires_at, is_desktop FROM refresh_tokens + SELECT id, username, expires_at, is_desktop, secret_gen FROM refresh_tokens WHERE token_hash = ? """, (token_hash,), @@ -709,6 +861,13 @@ def verify_refresh_token(token: str) -> Optional[Tuple[str, bool]]: if row is None: return None + if row["secret_gen"] is not None and row["secret_gen"] != _current_generation( + conn, row["username"] + ): + conn.execute("DELETE FROM refresh_tokens WHERE id = ?", (row["id"],)) + conn.commit() + return None + # Check expiry expires_at = datetime.fromisoformat(row["expires_at"]) if datetime.now(timezone.utc) > expires_at: @@ -753,30 +912,41 @@ def create_desktop_secret() -> str: conn.close() -def validate_desktop_secret(raw_secret: str) -> Optional[str]: - """Return the real admin username when the desktop secret matches.""" +def validate_desktop_secret_with_credential(raw_secret: str) -> Optional[Tuple[str, str]]: + """Validate the desktop secret and return ``(username, jwt_secret)``. + + Both reads share one transaction so the returned secret is the credential + version the desktop secret was checked against; a reset landing mid-request + then invalidates the tokens minted from it rather than blessing them. + """ if not raw_secret.startswith(DESKTOP_SECRET_PREFIX): return None - if get_user_and_secret(DEFAULT_ADMIN_USERNAME) is None: - return None secret_hash = _pbkdf2_desktop_secret(raw_secret) conn = get_connection() try: - cur = conn.execute( + conn.execute("BEGIN") + row = conn.execute( "SELECT value FROM app_secrets WHERE key = ?", (_DESKTOP_SECRET_HASH_KEY,), - ) - row = cur.fetchone() - if row is None: + ).fetchone() + if row is None or not secrets.compare_digest(row["value"], secret_hash): return None - if not secrets.compare_digest(row["value"], secret_hash): + jwt_secret = _current_secret(conn, DEFAULT_ADMIN_USERNAME) + if jwt_secret is None: return None - return DEFAULT_ADMIN_USERNAME + return DEFAULT_ADMIN_USERNAME, jwt_secret finally: + conn.rollback() conn.close() +def validate_desktop_secret(raw_secret: str) -> Optional[str]: + """Return the real admin username when the desktop secret matches.""" + verified = validate_desktop_secret_with_credential(raw_secret) + return verified[0] if verified else None + + def clear_desktop_secret() -> None: """Remove backend-side desktop auth state.""" conn = get_connection() @@ -802,6 +972,7 @@ def create_api_key( name: str, expires_at: Optional[str] = None, internal: bool = False, + expect_gen: Optional[str] = None, ) -> Tuple[str, dict]: """Create a new API key for *username*. @@ -810,6 +981,10 @@ def create_api_key( Pass ``internal=True`` for keys minted by workflows (e.g. data-recipe runs) that should not appear in user-facing key listings. + + ``expect_gen`` ties the insert to the credential generation the request + authenticated under, so a session revoked by a concurrent password reset + cannot mint a key that outlives it. Raises ``CredentialRotated`` if it moved. """ raw_key = API_KEY_PREFIX + secrets.token_hex(16) key_hash = _pbkdf2_api_key(raw_key) @@ -818,6 +993,12 @@ def create_api_key( conn = get_connection() try: + if expect_gen is not None: + conn.execute("BEGIN IMMEDIATE") + if _current_generation(conn, username) != expect_gen: + raise CredentialRotated( + "The credential this request authenticated with was revoked." + ) conn.execute( """ INSERT INTO api_keys (username, key_prefix, key_hash, name, created_at, expires_at, is_internal) @@ -906,15 +1087,25 @@ def revoke_internal_api_key(key_id: int) -> bool: def validate_api_key(raw_key: str) -> Optional[str]: - """Validate *raw_key* and return the owning username, or ``None``. + """Validate *raw_key* and return the owning username, or ``None``.""" + verified = validate_api_key_with_credential(raw_key) + return verified[0] if verified else None - Also updates ``last_used_at`` on success. + +def validate_api_key_with_credential(raw_key: str) -> Optional[Tuple[str, str]]: + """Validate *raw_key* and return ``(username, jwt_secret)``, or ``None``. + + Also updates ``last_used_at`` on success. The key check and the credential + read share one write transaction, so the returned version is the one the key + was actually valid under: a reset committing right after cannot have its new + generation handed to a request the key it revoked authenticated. """ cache_id = _api_key_cache_id(raw_key) cached_hash = _api_key_hash_cache.get(cache_id) key_hash = cached_hash if cached_hash is not None else _pbkdf2_api_key(raw_key) conn = get_connection() try: + conn.execute("BEGIN IMMEDIATE") cur = conn.execute( "SELECT id, username, is_active, expires_at FROM api_keys WHERE key_hash = ?", (key_hash,), @@ -934,11 +1125,15 @@ def validate_api_key(raw_key: str) -> Optional[str]: expires = datetime.fromisoformat(row["expires_at"]) if datetime.now(timezone.utc) > expires: return None + secret = _current_secret(conn, row["username"]) + if secret is None: + return None conn.execute( "UPDATE api_keys SET last_used_at = ? WHERE id = ?", (datetime.now(timezone.utc).isoformat(), row["id"]), ) conn.commit() - return row["username"] + return row["username"], secret finally: + conn.rollback() conn.close() diff --git a/studio/backend/core/inference/llama_cpp.py b/studio/backend/core/inference/llama_cpp.py index dcfbfb3338..712caf43e5 100644 --- a/studio/backend/core/inference/llama_cpp.py +++ b/studio/backend/core/inference/llama_cpp.py @@ -95,6 +95,8 @@ from core.inference.tool_call_parser import ( MAX_ACT_REPROMPTS as _MAX_REPROMPTS, NUDGE_TOOL_CALLS_STATUS as _NUDGE_TOOL_CALLS_STATUS, REPROMPT_MAX_CHARS as _REPROMPT_MAX_CHARS, + is_reprompt_repeat as _is_reprompt_repeat, + is_reprompt_restatement as _is_reprompt_restatement, is_short_intent_without_action as _is_short_intent_without_action, reprompt_to_act_message as _reprompt_to_act_message, ) @@ -361,12 +363,32 @@ _DEFAULT_STREAM_STALL_TIMEOUT_S = 120.0 # 2 min # loop). Structured delta.tool_calls are grammar-bounded by llama-server; text # parsed from content is not, so one runaway turn could fan out unbounded. _MAX_TOOL_CALLS_PER_TURN = 8 -_FORCED_REPEAT_PLAN_SIGNAL = re.compile( - r"\b(?:i\s+will|i'll|let\s+me|going\s+to|need\s+to|call|use|run|search|fetch|render)\b", +# Obligation phrasing INTENT_SIGNAL leaves alone ("I need to call ..."), paired with +# an action verb. Sentence-anchored: mid-sentence the same words are prose that names +# a tool ("The API I should invoke is foo() because ..."), and suppressing that loses +# a real answer. "should"/"must" sit outside the need|have|ought group because they +# take a bare infinitive. "invoke"/"query" stay out of the verb list: they read as +# technical prose far more often than as a stall. +_FORCED_PLAN_INTENT = re.compile( + r"(?:^|[.!?]\s+)\s*" + r"(?:i\s+(?:(?:need|have|ought)\s+to|should|must)|need\s+to|going\s+to|must|should)" + r"\s+(?:\w+\s+){0,2}?(?:call|use|run|search|fetch|render)\b", + re.I | re.M, +) +# "the answer is not in the context" announces a *missing* answer, so the negated +# forms are excluded or the plan behind them would ship as the final response. +_FINAL_ANSWER_SIGNAL = re.compile( + r"\b(?:final\s+answer|answer\s*:|here\s+is|here's|in\s+summary|result\s*:" + r"|(?:the\s+)?answer\s+is(?!\s+(?:not|unavailable|unknown|unclear|missing)\b))\b", re.I, ) -_FINAL_ANSWER_SIGNAL = re.compile( - r"\b(?:final\s+answer|answer\s*:|here\s+is|here's|in\s+summary|result\s*:)\b", +# A plan that pivots ("I should call web_search, but Tokyo is the capital") has an +# answer attached, so the turn must survive. Leaking a plan sentence is cosmetic; +# dropping an answer is not, so the doubtful case keeps the output. The pivot has to +# carry text of its own: "I should call web_search, though." answers nothing. +_ANSWER_PIVOT = re.compile( + r"\b(?:but|however|although|though|that\s+said|in\s+the\s+meantime|meanwhile)\b" + r"[\W_]*(?:\w+[\W_]+){1,}\w", re.I, ) @@ -458,14 +480,28 @@ def _held_rehearsal_tail_len(text: str, active_tools: list[dict]) -> int: return len(tail) if tail and _is_rehearsal_prefix(tail, active_tools) else 0 -def _should_suppress_forced_no_tool_output(text: str) -> bool: - """Suppress only repeated forced-turn planning text, not final answers.""" +def _should_suppress_forced_no_tool_output(text: str, previous: str = "") -> bool: + """Suppress only repeated forced-turn planning text, not final answers. + + ``previous`` is the stall text that triggered the nudge, so a retry that + moved on can be told from one that just said the same thing again. + """ stripped = text.strip() if not stripped or len(stripped) >= _REPROMPT_MAX_CHARS: return False if _FINAL_ANSWER_SIGNAL.search(stripped): return False - return _FORCED_REPEAT_PLAN_SIGNAL.search(stripped) is not None + plan = _FORCED_PLAN_INTENT.search(stripped) + if plan is not None: + # Only the plan itself is safe to drop; anything the turn pivots to after it + # is the answer the user is waiting for. + return _ANSWER_PIVOT.search(stripped[plan.end() :]) is None + if not _is_short_intent_without_action(stripped): + return False + # INTENT_SIGNAL also fires on lead-ins to a real answer ("Now I have the results. + # The capital is Tokyo."), so a bare intent match is a stall only when the retry + # adds nothing. No ``previous`` keeps the standalone "is this a stall?" contract. + return not previous or _is_reprompt_restatement(stripped, previous) # ── Pre-compiled patterns for GGUF shard detection ─────────── @@ -11724,6 +11760,10 @@ class LlamaCppBackend: # direct answer ("4", "Hello!") won't match. Pattern shared with the # safetensors loop (tool_call_parser.INTENT_SIGNAL). _reprompt_count = 0 + # Budgeted apart from _reprompt_count so a pre-tool nudge can't spend it. + _post_tool_reprompts = 0 + # Text that triggered the last nudge; if the retry restates it, stop. + _last_reprompt_text = "" # Gates ``max_tool_iterations`` on real tool turns (not the enlarged range) so reserved # re-prompt slots don't extend the budget. Mirrors the safetensors guard. _tool_iters_done = 0 @@ -11731,7 +11771,7 @@ class LlamaCppBackend: # Reserve extra iterations for re-prompts so they don't consume the # caller's tool-call budget; only when tool iterations are allowed. - _extra = _MAX_REPROMPTS if max_tool_iterations > 0 else 0 + _extra = _MAX_REPROMPTS + 1 if max_tool_iterations > 0 else 0 for iteration in range(max_tool_iterations + _extra): if cancel_event is not None and cancel_event.is_set(): return @@ -12376,12 +12416,10 @@ class LlamaCppBackend: ) if not _safety_tc: # ── Re-prompt on plan-without-action ── - # If the model described its intent (forward-looking - # language) without calling a tool, nudge it to act. - # Fires at most once per request, only on short - # responses with intent signals -- "4" or "Hello!" - # won't trigger it. Use content if available, else - # fall back to reasoning text (reasoning-only stalls). + # Intent described without a tool call: nudge it to act. Up + # to _MAX_REPROMPTS times, only on short responses with intent + # signals -- "4" or "Hello!" won't trigger it. Uses content, + # else reasoning text (reasoning-only stalls). _stripped = content_accum.strip() if not _stripped: _stripped = reasoning_accum.strip() @@ -12391,18 +12429,33 @@ class LlamaCppBackend: r"(?i)\brender[_\s-]?html\b", _stripped, ) + # A post-tool stall still deserves a nudge, but each retry + # re-runs tools, so allow only one. RAG autoinject never lands + # in history, so _auto keeps a doc-grounded turn from reading + # as pre-tool (mirrors safetensors rag_autoinjected). + _already_acted = bool(_auto) or any( + record.executed for record in tool_controller.history + ) + if _already_acted: + _reprompt_used, _reprompt_cap = _post_tool_reprompts, 1 + else: + _reprompt_used, _reprompt_cap = _reprompt_count, _MAX_REPROMPTS # None keeps the default-on re-prompt; False disables it. if ( auto_heal_tool_calls and (nudge_tool_calls is None or nudge_tool_calls) and active_tools and not _render_html_already_done_intent - and _reprompt_count < _MAX_REPROMPTS + and _reprompt_used < _reprompt_cap + and not _is_reprompt_repeat(_stripped, _last_reprompt_text) and _is_short_intent_without_action(_stripped) ): _reprompt_count += 1 + if _already_acted: + _post_tool_reprompts += 1 + _last_reprompt_text = _stripped logger.info( - f"Re-prompt {_reprompt_count}/{_MAX_REPROMPTS}: " + f"Re-prompt {_reprompt_used + 1}/{_reprompt_cap}: " f"model responded without calling tools " f"({len(_stripped)} chars)" ) @@ -12440,7 +12493,10 @@ class LlamaCppBackend: if _forced_tool_call_pending: _forced_tool_call_pending = False - if not _should_suppress_forced_no_tool_output(_stripped): + if not _should_suppress_forced_no_tool_output( + _stripped, + _last_reprompt_text, + ): if cumulative_display: forced_visible_text = _strip_tool_markup( cumulative_display, @@ -12770,6 +12826,10 @@ class LlamaCppBackend: _kb_search_count += 1 completion = tool_controller.record_result(decision, result) resolved_provisional_tool_call_ids.add(decision.tool_call_id) + # A real execution opens the post-tool phase; carrying the pre-tool + # stall text over would read the same sentence as a repeat and + # swallow the one post-tool nudge. + _last_reprompt_text = "" # A tool ran this turn, so it counts against the caller's budget. _turn_executed_real_tool = True yield completion.tool_end_event() diff --git a/studio/backend/core/inference/safetensors_agentic.py b/studio/backend/core/inference/safetensors_agentic.py index 3057f7c2ac..3b733a85be 100644 --- a/studio/backend/core/inference/safetensors_agentic.py +++ b/studio/backend/core/inference/safetensors_agentic.py @@ -39,6 +39,7 @@ from core.inference.tool_call_parser import ( RAG_MAX_SEARCHES_PER_TURN, RAG_SEARCH_CAP_NUDGE, TOOL_XML_SIGNALS, + is_reprompt_repeat, is_short_intent_without_action, parse_tool_calls_from_text, reprompt_to_act_message, @@ -565,6 +566,8 @@ def run_safetensors_tool_loop( final_attempt_done = False next_call_id = 0 reprompt_count = 0 + # Text that triggered the last nudge; if the retry restates it, stop (GGUF parity). + last_reprompt_text = "" # A denied tool confirmation must not be answered with a plan-without-action # re-prompt (which would raise the confirmation gate again). tool_denied = False @@ -1015,9 +1018,11 @@ def run_safetensors_tool_loop( and not rag_autoinjected and not tool_denied and not any(record.executed for record in tool_controller.history) + and not is_reprompt_repeat(intent_text, last_reprompt_text) and is_short_intent_without_action(intent_text) ): reprompt_count += 1 + last_reprompt_text = intent_text logger.info( "Safetensors re-prompt %d/%d: model responded without " "calling tools (%d chars)", diff --git a/studio/backend/core/inference/tool_call_parser.py b/studio/backend/core/inference/tool_call_parser.py index 4c3fe234ae..28b544303d 100644 --- a/studio/backend/core/inference/tool_call_parser.py +++ b/studio/backend/core/inference/tool_call_parser.py @@ -166,15 +166,40 @@ RAG_SEARCH_CAP_NUDGE = ( # ── Plan-without-action re-prompt (shared by the GGUF and safetensors loops) ── +# Verbs naming work this turn. Narrow on purpose: "install"/"add"/"open" belong to +# advice for the user, which must not be re-prompted. +_ACTION_VERB = ( + r"(?:search|check|look|find|fetch|get|call|use|run|query|invoke|analy[sz]e" + r"|review|inspect|read|gather|examine|retrieve|browse|consult|verify" + r"|confirm|compute|calculate|determine|identify|render)" +) +# Offering to help hands control back exactly like "let me know": measured on real +# turns, "I'll do my best to help" and "allow me to assist" close a clarification +# request and never precede a tool call. "help you" keeps its plan reading when an +# action follows it ("I'll help you search the web"). +_HELP_OFFER = ( + r"(?:do(?:ing)?\s+my\s+best|try\s+my\s+best|be\s+(?:able|happy|glad)\s+to\b" + r"|assist\b|help\s+you\b(?!\s+" + _ACTION_VERB + r")|give\s+you\s+accurate\b)" +) # Forward-looking intent: the model says what it *will* do, not a final answer. INTENT_SIGNAL = re.compile( - r"(?i)(" - # Direct intent ("I'll", "Let me"); lookahead drops negated forms - # ("I will not") so a refusal does not re-prompt. - r"\b(i['\u2019](ll|m going to|m gonna)|i am (going to|gonna)|i will|i shall|let me|allow me)\b(?!\s+(?:not|never)\b)" + r"(?im)(" + # Direct intent ("I'll"); lookahead drops negated forms ("I will not"). + r"\b(i['\u2019](ll|m going to|m gonna)|i am (going to|gonna)|i will|i shall)\b" + r"(?!\s+(?:not|never)\b)(?!\s+" + _HELP_OFFER + r")" r"|" - # Step/plan framing: "First ...", "Step 1:", "Here's my plan" - r"\b(?:first\b|step \d+:?|here['\u2019]?s (?:my |the |a )?(?:plan|approach))" + # "let me know" hands control back rather than announcing an action. + r"\b(?:let me|allow me)\b(?!\s+(?:not|never|know)\b)(?!\s+to\s+" + _HELP_OFFER + r")" + r"|" + # Step/plan framing. "first" must open a sentence and be followed by a plan + # (pronoun, "my/our plan", or an action verb); otherwise it is prose ("The + # first line is blank.", "First place went to Alice") or advice to the user. + r"(?:^|[.!?]\s+)\s*(?:the\s+)?first\s+step\b" + r"|(?:^|[.!?]\s+)\s*first\s*[,:–—-]?\s+(?:my|our)\s+(?:plan|approach|step)\b" + r"|(?:^|[.!?]\s+)\s*first\s*[,:–—-]?\s+(?:i|we|let['’]?s|let us)\b" + r"|(?:^|[.!?]\s+)\s*first\s*[,:–—-]?\s+" + _ACTION_VERB + r"\b" + r"|" + r"\b(?:step \d+:?|here['\u2019]?s (?:my |the |a )?(?:plan|approach))" r"|" r"\b(?:now i|next i)\b" r")" @@ -193,6 +218,41 @@ def is_short_intent_without_action(text: str) -> bool: return 0 < len(stripped) < REPROMPT_MAX_CHARS and INTENT_SIGNAL.search(stripped) is not None +# Leading marks are kept unless they are quotes or brackets, so ".NET" survives; +# stripping all non-word chars would collapse "C++" and "C#" to the same token. +_REPEAT_TRAIL_PUNCT = ".,;:!?\"'`()[]{}<>‘’“”" +_REPEAT_LEAD_PUNCT = "\"'`([{‘“" + + +def _normalize_for_repeat(text: str) -> str: + words = [] + for word in text.lower().split(): + stripped = word.rstrip(_REPEAT_TRAIL_PUNCT).lstrip(_REPEAT_LEAD_PUNCT) + # Keep marks-only tokens: "value is 5" and "value is < 5" differ, and + # dropping the "<" threw the corrected attempt away. + words.append(stripped or word) + return " ".join(words) + + +# A nudge that just gets the same answer back has not worked, so stop there. +# Exact after normalisation, deliberately. Every relaxation tried here lost a real +# correction: a similarity ratio is length dependent (one changed token in a 50-word +# plan still scored 0.98), a set ignores order ("cats not dogs"), and ignoring filler +# words eats the target itself ("The Who", "OK Go"). A missed repeat costs one nudge +# out of MAX_ACT_REPROMPTS; a false one strands the plan unexecuted. +def is_reprompt_repeat(text: str, previous: str) -> bool: + return is_reprompt_restatement(text, previous) + + +# Same comparison, different decision: this one discards the turn. An appended answer +# must not match, and deletions flip meaning ("is not supported" -> "is supported"). +def is_reprompt_restatement(text: str, previous: str) -> bool: + if not previous: + return False + a, b = _normalize_for_repeat(text), _normalize_for_repeat(previous) + return bool(a) and a == b + + def reprompt_to_act_message(tool_hint: str) -> str: """The user message appended when re-prompting a plan-without-action turn.""" return ( diff --git a/studio/backend/routes/auth.py b/studio/backend/routes/auth.py index 1acc48e3a3..fe2f09fcd9 100644 --- a/studio/backend/routes/auth.py +++ b/studio/backend/routes/auth.py @@ -31,6 +31,7 @@ from auth import storage, hashing from auth.authentication import ( create_access_token, create_refresh_token, + get_current_credential, get_current_subject, get_current_subject_allow_password_change, refresh_access_token, @@ -399,7 +400,7 @@ async def login(payload: AuthLoginRequest, request: Request) -> Token: detail = f"Incorrect password. To reset it, run this in your terminal: {_reset_password_command()}", ) - salt, pwd_hash, _jwt_secret, must_change_password = record + salt, pwd_hash, jwt_secret, must_change_password = record if not hashing.verify_password(payload.password, salt, pwd_hash): _record_login_failure(key) raise HTTPException( @@ -409,8 +410,10 @@ async def login(payload: AuthLoginRequest, request: Request) -> Token: _clear_login_bucket(key) _clear_login_bucket(unknown_key) - access_token = create_access_token(subject = payload.username) - refresh_token = create_refresh_token(subject = payload.username) + # Issue against the credential version just verified, not whatever is in the DB + # now: a concurrent reset-password must not hand this login a post-reset session. + access_token = create_access_token(subject = payload.username, secret = jwt_secret) + refresh_token = create_refresh_token(subject = payload.username, secret = jwt_secret) return Token( access_token = access_token, refresh_token = refresh_token, @@ -438,16 +441,17 @@ async def logout( @router.post("/desktop-login", response_model = Token) async def desktop_login(payload: DesktopLoginRequest) -> Token: """Exchange a local desktop secret for normal admin-subject tokens.""" - username = storage.validate_desktop_secret(payload.secret) - if username is None: + verified = storage.validate_desktop_secret_with_credential(payload.secret) + if verified is None: raise HTTPException( status_code = status.HTTP_401_UNAUTHORIZED, detail = "Desktop authentication failed", ) + username, jwt_secret = verified return Token( - access_token = create_access_token(subject = username, desktop = True), - refresh_token = create_refresh_token(subject = username, desktop = True), + access_token = create_access_token(subject = username, desktop = True, secret = jwt_secret), + refresh_token = create_refresh_token(subject = username, desktop = True, secret = jwt_secret), token_type = "bearer", must_change_password = False, ) @@ -462,9 +466,11 @@ async def refresh(payload: RefreshTokenRequest) -> Token: status_code = status.HTTP_401_UNAUTHORIZED, detail = "Invalid or expired refresh token", ) - username, is_desktop = consumed - new_access_token = create_access_token(subject = username, desktop = is_desktop) - new_refresh_token = create_refresh_token(subject = username, desktop = is_desktop) + username, is_desktop, jwt_secret = consumed + new_access_token = create_access_token(subject = username, desktop = is_desktop, secret = jwt_secret) + new_refresh_token = create_refresh_token( + subject = username, desktop = is_desktop, secret = jwt_secret + ) return Token( access_token = new_access_token, @@ -507,13 +513,25 @@ async def change_password( # Single transaction: a separate refresh-token purge could fail after the # password commit, leaving pre-change tokens able to mint access tokens. - storage.update_password(current_subject, payload.new_password, revoke_refresh_tokens = True) + # Conditional on the hash just verified: a reset-password that landed while + # this request was in flight must not be overwritten by it. + new_secret = storage.update_password( + current_subject, + payload.new_password, + revoke_refresh_tokens = True, + expect_password_hash = pwd_hash, + ) + if new_secret is None: + raise HTTPException( + status_code = status.HTTP_409_CONFLICT, + detail = "The password changed while this request was in flight. Sign in again.", + ) try: request.app.state.bootstrap_password = None except AttributeError: pass - access_token = create_access_token(subject = current_subject) - refresh_token = create_refresh_token(subject = current_subject) + access_token = create_access_token(subject = current_subject, secret = new_secret) + refresh_token = create_refresh_token(subject = current_subject, secret = new_secret) return Token( access_token = access_token, refresh_token = refresh_token, @@ -541,20 +559,28 @@ def _row_to_api_key_response(row: dict) -> ApiKeyResponse: @router.post("/api-keys", response_model = CreateApiKeyResponse) async def create_api_key( - payload: CreateApiKeyRequest, current_subject: str = Depends(get_current_subject) + payload: CreateApiKeyRequest, credential: tuple = Depends(get_current_credential) ) -> CreateApiKeyResponse: """Create a new API key. The raw key is returned once and cannot be retrieved later.""" + current_subject, generation = credential expires_at = None if payload.expires_in_days is not None: expires_at = ( datetime.now(timezone.utc) + timedelta(days = payload.expires_in_days) ).isoformat() - raw_key, row = storage.create_api_key( - username = current_subject, - name = payload.name, - expires_at = expires_at, - ) + try: + raw_key, row = storage.create_api_key( + username = current_subject, + name = payload.name, + expires_at = expires_at, + expect_gen = generation, + ) + except storage.CredentialRotated: + raise HTTPException( + status_code = status.HTTP_401_UNAUTHORIZED, + detail = "Invalid or expired token", + ) return CreateApiKeyResponse( key = raw_key, api_key = _row_to_api_key_response(row), diff --git a/studio/backend/routes/data_recipe/jobs.py b/studio/backend/routes/data_recipe/jobs.py index e870e8855e..7fdf0abada 100644 --- a/studio/backend/routes/data_recipe/jobs.py +++ b/studio/backend/routes/data_recipe/jobs.py @@ -10,7 +10,10 @@ from datetime import datetime, timedelta, timezone from typing import Any, Optional from urllib.parse import urlparse -from fastapi import APIRouter, HTTPException, Query, Request +from fastapi import APIRouter, Depends, HTTPException, Query, Request + +from auth.authentication import get_current_credential +from auth.storage import CredentialRotated from fastapi.responses import JSONResponse, StreamingResponse from pydantic import ValidationError @@ -257,7 +260,11 @@ def _inject_local_structured_response_format( model_configs.extend(new_configs) -def _inject_local_providers(recipe: dict[str, Any], request: Request) -> Optional[int]: +def _inject_local_providers( + recipe: dict[str, Any], + request: Request, + expect_gen: Optional[str] = None, +) -> Optional[int]: """Mutate recipe in-place: point is_local providers at this server and mint a short-lived internal sk-unsloth-* key for workflow auth. @@ -313,6 +320,7 @@ def _inject_local_providers(recipe: dict[str, Any], request: Request) -> Optiona name = "data-recipe workflow", expires_at = expires_at, internal = True, + expect_gen = expect_gen, ) internal_key_id = int(row["id"]) @@ -375,7 +383,11 @@ def _normalize_run_name(value: Any) -> str | None: @router.post("/jobs", response_class = JSONResponse, response_model = JobCreateResponse) -def create_job(payload: RecipePayload, request: Request): +def create_job( + payload: RecipePayload, + request: Request, + credential: tuple = Depends(get_current_credential), +): recipe = payload.recipe if not recipe.get("columns"): raise HTTPException(status_code = 400, detail = "Recipe must include columns.") @@ -406,7 +418,11 @@ def create_job(payload: RecipePayload, request: Request): ) from exc try: - internal_api_key_id = _inject_local_providers(recipe, request) + internal_api_key_id = _inject_local_providers(recipe, request, credential[1]) + except CredentialRotated as exc: + # A reset-password landed after this request authenticated; the workflow key + # is refused, so answer like any other revoked credential rather than 500. + raise HTTPException(status_code = 401, detail = "Invalid or expired token") from exc except ValueError as exc: raise log_and_http_error( exc, diff --git a/studio/backend/run.py b/studio/backend/run.py index ef372e004e..2d9e714d90 100644 --- a/studio/backend/run.py +++ b/studio/backend/run.py @@ -10,7 +10,7 @@ import os import sys import time from pathlib import Path -from typing import Optional, Tuple +from typing import NoReturn, Optional, Sequence, Tuple def _fix_torch_cuda_ld_path(): @@ -689,6 +689,33 @@ def _get_pid_on_port(port: int) -> "tuple[int, str] | None": return None +def _bind_addresses(host: str, port: int) -> "set[str]": + """Every address *host* resolves to. `localhost` is both 127.0.0.1 and ::1, and + recording only the first lets a later launch on the other one miss us.""" + import socket + + try: + infos = socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM) + except OSError: + return {host} + return {info[4][0] for info in infos} or {host} + + +def _addresses_collide(recorded: "str | None", host: str, port: int) -> bool: + """Would a server bound to *recorded* block a bind to *host*? + + *recorded* may list several addresses. Unknown or wildcard on either side + collides: refusing with a clear message beats silently starting a duplicate. + """ + wildcards = ("0.0.0.0", "::", "") + if not recorded or host in wildcards: + return True + listed = {a.strip() for a in recorded.split(",") if a.strip()} + if not listed or listed & set(wildcards): + return True + return bool(listed & _bind_addresses(host, port)) + + def _is_port_free(host: str, port: int) -> bool: """Check if a port is available for binding. @@ -733,18 +760,213 @@ def _find_free_port( host: str, start: int, max_attempts: int = 20, + avoid_own_studio: bool = False, ) -> int: - """Find a free port from `start`, trying up to max_attempts ports.""" + """Find a free port from `start`, trying up to max_attempts ports. + + ``avoid_own_studio`` aborts rather than skipping past one of our own servers + in the fallback range, which would start a duplicate on a later port. + """ for offset in range(max_attempts): candidate = start + offset if _is_port_free(host, candidate): return candidate + if avoid_own_studio: + own = _own_studio_on_port(candidate, host) + if own is not None: + _abort_already_running(own, candidate) raise RuntimeError(f"Could not find a free port in range {start}-{start + max_attempts - 1}") from utils.paths.storage_roots import studio_root as _studio_root +# Legacy single-instance file; still read so `stop` finds an older build's server. _PID_FILE = _studio_root() / "studio.pid" +PID_FILE_GLOB = "studio-*.pid" + + +def _pid_file_for_port(port: int) -> Path: + # PID in the name: 127.0.0.1 and ::1 can share a port, and one file per port + # would let the second bind overwrite the first. + return _studio_root() / f"studio-{port}-{os.getpid()}.pid" + + +def _pid_alive(pid: int) -> bool: + try: + import psutil + return psutil.pid_exists(pid) + except ImportError: + pass + if sys.platform == "win32": + # os.kill(pid, 0) raises OSError for every pid on Windows, so tasklist is + # the only usable probe here. + import subprocess + try: + out = subprocess.run( + ["tasklist", "/FI", f"PID eq {int(pid)}", "/NH", "/FO", "CSV"], + capture_output = True, + text = True, + timeout = 10, + ).stdout + except Exception: + # Unconfirmed means keep, matching the CLI's _pid_alive. Pruning a + # live server's record is what lets the next launch fall back past it + # and strand it, which is the bug this file exists to fix. A stale + # record instead costs one clear "already running" message. + return True + return f'"{int(pid)}"' in out + try: + os.kill(pid, 0) + except ProcessLookupError: + return False + except OSError: + return True + return True + + +def _process_create_time(pid: int) -> "float | None": + try: + import psutil + return psutil.Process(pid).create_time() + except Exception: + return None + + +def _read_pid_record(path: Path) -> "tuple[int, float | None, str | None] | None": + """Parse ``pid`` / optional ``create_time`` / optional bind address.""" + try: + lines = path.read_text(encoding = "utf-8").splitlines() + except (OSError, UnicodeDecodeError): + return None + if not lines or not lines[0].strip().isdigit(): + return None + try: + # isdigit() is not enough: a superscript two passes it but int() rejects it. + pid = int(lines[0].strip()) + except ValueError: + return None + # kill(0) signals our whole process group; kill(1) is init. Never either. + if pid < 2: + return None + created = None + if len(lines) > 1: + try: + created = float(lines[1].strip()) + except ValueError: + created = None + address = lines[2].strip() if len(lines) > 2 and lines[2].strip() else None + return pid, created, address + + +def _pid_is_studio_backend(pid: int, created_times: "Sequence[float | None]" = ()) -> bool: + """False only when a recorded start time proves this PID is a different process. + + Any recorded time matching is enough -- a stale record must not veto a live + server that reused the PID. Untimed records cannot be checked at all, so they + are trusted: a legacy `python run.py` has no telltale argv, and guessing from + the command line rejected real servers. + """ + known = [c for c in created_times if c is not None] + if not known: + return True + actual = _process_create_time(pid) + if actual is None: + return True + return any(abs(actual - c) < 1.0 for c in known) + + +def _own_studio_on_port(port: int, host: str) -> "int | None": + """PID of one of our own servers already bound to *port* for *host*. + + Reads our own records rather than enumerating listeners: psutil is optional, + and without it a listener scan finds nothing and we silently start a duplicate. + """ + try: + paths = list(_studio_root().glob(f"studio-{port}-*.pid")) + except OSError: + return None + for path in paths: + record = _read_pid_record(path) + if record is None: + continue + pid, created, address = record + if not _pid_alive(pid): + # Pruning is a courtesy; an undeletable record must not abort startup. + try: + path.unlink(missing_ok = True) + except OSError: + pass + continue + if not _addresses_collide(address, host, port): + continue + if _pid_is_studio_backend(pid, [created]): + return pid + return _legacy_studio_on_port(port) + + +def _legacy_studio_on_port(port: int) -> "int | None": + """A pre-upgrade server recorded only its PID, so match it to the listener. + + Falling back past one leaves it running while `_write_pid_file` overwrites the + only record of it. When the listener is unknowable, assume it is ours. + """ + record = _read_pid_record(_PID_FILE) + if record is None: + return None + pid, created, _address = record + if not _pid_alive(pid): + return None + # A current build writes a per-port file too, so its port is already known -- + # and this port's records were just checked. Only count a record that still + # matches the live process: a stale one may just share a reused PID. + for other in _per_port_records(): + if other and other[0] == pid and _pid_is_studio_backend(pid, [other[1]]): + return None + blocker = _get_pid_on_port(port) + if blocker is not None and blocker[0] != pid: + return None + if not _pid_is_studio_backend(pid, [created]): + return None + return pid + + +def _per_port_records() -> "list[tuple[int, float | None, str | None] | None]": + try: + return [_read_pid_record(p) for p in _studio_root().glob(PID_FILE_GLOB)] + except OSError: + return [] + + +def _resolve_port( + host: str, + port: int, + avoid_own_studio: bool = True, +) -> int: + """The requested port, or the next free one. + + With ``avoid_own_studio`` this aborts rather than falling back past one of our + own servers, on *port* itself or anywhere in the fallback range: skipping one + is what strands it. Callers that read the bound port back pass False and keep + the plain fallback. + """ + if _is_port_free(host, port): + return port + if avoid_own_studio: + own = _own_studio_on_port(port, host) + if own is not None: + _abort_already_running(own, port) + return _find_free_port(host, port + 1, avoid_own_studio = avoid_own_studio) + + +def _abort_already_running(pid: int, port: int) -> "NoReturn": + print( + f"Error: Unsloth Studio is already running on port {port} (PID {pid}). Run " + "`unsloth studio stop` first, or start this one on a different --port.", + file = sys.stderr, + flush = True, + ) + sys.exit(1) + # Direct backend launches bypass the CLI's env re-export; do it here for # real custom roots so unsloth-zoo's import-time LLAMA_CPP_DEFAULT_DIR @@ -770,25 +992,101 @@ if _STUDIO_ROOT_RESOLVED != _LEGACY_STUDIO_ROOT: os.environ.setdefault("UNSLOTH_IS_PRESENT", "1") -def _write_pid_file(): - """Write the current process PID to the studio PID file.""" +_OWN_PID_FILE: "Path | None" = None + + +def _write_pid_file(port: int, host: str = ""): + """Record this PID under its own port so `stop` can find every server.""" + global _OWN_PID_FILE + path = _pid_file_for_port(port) try: - _PID_FILE.parent.mkdir(parents = True, exist_ok = True) - _PID_FILE.write_text(str(os.getpid()), encoding = "utf-8") + path.parent.mkdir(parents = True, exist_ok = True) + except OSError: + pass + try: + # Start time pins the record to this process; the bind address tells a + # later launch whether this server would actually block it. + created = _process_create_time(os.getpid()) + address = ",".join(sorted(_bind_addresses(host, port))) if host else "" + body = f"{os.getpid()}\n{'' if created is None else repr(created)}\n{address}" + # Write-then-rename: `stop` reads these concurrently, and a reader that + # catches the truncate window sees a corrupt record and deletes it. + tmp = path.with_name(path.name + ".tmp") + try: + tmp.write_text(body, encoding = "utf-8") + os.replace(tmp, path) + finally: + # A failed replace would otherwise leave the scratch file behind. It + # does not end in .pid, so no glob picks it up either way. + tmp.unlink(missing_ok = True) + except OSError: + pass + else: + _OWN_PID_FILE = path + # An older CLI's `stop` only reads this one, and expects a bare PID. Written + # independently of the per-port record: if that one failed, this is the only + # thing keeping the server stoppable at all. + try: + # Never take it from a server that is still running. A pre-upgrade server + # is recorded here and nowhere else, so overwriting its entry is exactly + # what strands it -- the orphan this file exists to prevent. + prior = _read_pid_record(_PID_FILE) if _PID_FILE.is_file() else None + if prior is None or prior[0] == os.getpid() or not _pid_alive(prior[0]): + _PID_FILE.write_text(str(os.getpid()), encoding = "utf-8") except OSError: pass -def _remove_pid_file(): - """Remove the PID file if it belongs to this process.""" +def _legacy_heir() -> "int | None": + """Another live server's PID, to hand the legacy studio.pid over to. + + Only one server owns studio.pid at a time, so its exit would otherwise drop + the single record an older CLI can read, stranding any sibling that is still + serving. + """ try: - if _PID_FILE.is_file(): - stored = _PID_FILE.read_text(encoding = "utf-8").strip() - if stored == str(os.getpid()): + paths = sorted(_studio_root().glob(PID_FILE_GLOB)) + except OSError: + return None + for path in paths: + if _OWN_PID_FILE is not None and path == _OWN_PID_FILE: + continue + record = _read_pid_record(path) + if record is None or record[0] == os.getpid(): + continue + if _pid_alive(record[0]) and _pid_is_studio_backend(record[0], [record[1]]): + return record[0] + return None + + +def _remove_pid_file(): + """Remove the PID files that belong to this process. + + _PID_FILE is checked even when the per-port record was never written, since + _write_pid_file writes the two independently. + """ + # Nothing here may raise: _graceful_shutdown calls this at the end, and an + # unreadable or undeletable record must not abandon the rest of the exit + # path. _read_pid_record already swallows OSError/UnicodeDecodeError. + if _OWN_PID_FILE is not None: + try: + record = _read_pid_record(_OWN_PID_FILE) if _OWN_PID_FILE.is_file() else None + if record is not None and record[0] == os.getpid(): + _OWN_PID_FILE.unlink(missing_ok = True) + except OSError: + pass + try: + record = _read_pid_record(_PID_FILE) if _PID_FILE.is_file() else None + if record is not None and record[0] == os.getpid(): + # Hand the pointer to a live sibling rather than deleting it. An + # older CLI reads only this file, so dropping it while another + # server is still up leaves that server unstoppable. + heir = _legacy_heir() + if heir is None: _PID_FILE.unlink(missing_ok = True) - # Runs first in _graceful_shutdown: a corrupt PID file raising here would - # abandon the children the rest of that function exists to kill. - except (OSError, UnicodeDecodeError): + else: + _PID_FILE.write_text(str(heir), encoding = "utf-8") + except OSError: pass @@ -798,7 +1096,6 @@ def _graceful_shutdown(server = None): Called from signal handlers to clean up children before exit. Critical on Windows where atexit handlers are unreliable after Ctrl+C. """ - _remove_pid_file() logger.info("Graceful shutdown initiated -- cleaning up subprocesses...") # 1. Shut down uvicorn (releases the listening socket). @@ -851,6 +1148,9 @@ def _graceful_shutdown(server = None): except Exception as e: logger.warning("Error in process-lifetime sweep: %s", e) + # Last: while cleanup runs the server is still alive, and dropping the record + # early leaves a retried `stop` or a new launch unable to find it. + _remove_pid_file() logger.info("All subprocesses cleaned up") @@ -1328,7 +1628,8 @@ def _apply_supplied_password(password_value: "Optional[str]") -> None: if not _auth_storage.requires_password_change(_admin): print( "Error: an Unsloth admin password is already set; --password only sets " - "the initial password. Run `unsloth studio reset-password` first.", + "the initial password. Change it in the UI, or run `unsloth studio " + "reset-password` for a new one.", file = sys.stderr, flush = True, ) @@ -1399,6 +1700,7 @@ def run_server( enable_tools: "Optional[bool]" = None, password: "Optional[str]" = None, emit_tauri_port: bool = True, + abort_if_own_studio: "Optional[bool]" = None, ): """ Start the FastAPI server. @@ -1532,10 +1834,16 @@ def run_server( ) # Auto-find a free port if the requested one is in use. - if not _is_port_free(host, port): - original_port = port - blocker = _get_pid_on_port(port) - port = _find_free_port(host, port + 1) + original_port = port + # Refusing rather than falling back is for callers that cannot follow us to + # the new port. `studio run` reads app.state.server_port back and the desktop + # app reads TAURI_PORT, so both should keep the plain fallback; only the + # bare launch, which has nothing but the banner, benefits from the refusal. + if abort_if_own_studio is None: + abort_if_own_studio = not api_only + port = _resolve_port(host, port, avoid_own_studio = abort_if_own_studio) + if port != original_port: + blocker = _get_pid_on_port(original_port) if not silent: print("") print("=" * 50) @@ -1733,7 +2041,7 @@ def run_server( (time.perf_counter() - boot_started) * 1000, ) - _write_pid_file() + _write_pid_file(port, host) import atexit atexit.register(_remove_pid_file) diff --git a/studio/backend/tests/data/plan_vs_answer.jsonl b/studio/backend/tests/data/plan_vs_answer.jsonl new file mode 100644 index 0000000000..7397c8da7e --- /dev/null +++ b/studio/backend/tests/data/plan_vs_answer.jsonl @@ -0,0 +1,300 @@ +{"id": "7cdc94d4ad818e68", "model": "llama3.2-1b", "prompt_class": "needs_tool_python", "prompt": "Sort this list and tell me the median: 8, 3, 91, 12, 7, 44, 2.", "text": "import statistics\n# Define the list of numbers\nnumbers = [8, 3, 91, 12, 7, 44, 2]\n# Sort the list in ascending order\nsorted_numbers = sorted(numbers)\n# Calculate the median\nmedian = statistics.median(sorted_numbers)\n# Return the result in a JSON format\nreturn", "retry_text": "", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "300cc2c42f91bf35", "model": "llama3.2-1b", "prompt_class": "needs_tool_python", "prompt": "Sort this list and tell me the median: 8, 3, 91, 12, 7, 44, 2.", "text": "import json\nimport statistics\n\n# Define the list of numbers\nnumbers = [8, 3, 91, 12, 7, 44, 2]\n\n# Sort the list in ascending order\nnumbers.sort()\n\n# Calculate the median\nmedian = statistics.median(numbers)\n\n# Convert the result to a JSON string\nresult = json.dumps(", "retry_text": "web_search(query=\"statistics.median\", url=\"https://www.example.com\")", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "d35c286e3b43a3bb", "model": "llama3.2-1b", "prompt_class": "needs_tool_python", "prompt": "Sort this list and tell me the median: 8, 3, 91, 12, 7, 44, 2.", "text": "import json\nimport statistics\n\n# Define the function calls\nfunction_calls = [", "retry_text": "import json\n\ndef web_search(query, url):\n # This is a mock web_search function\n print(f\"Searching for", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "31ea3bc4cfb9df5d", "model": "llama3.2-1b", "prompt_class": "no_tool", "prompt": "What is the difference between a list and a tuple in Python?", "text": "import json\n\n# List and tuple are both mutable, meaning they can be changed after creation.\n# They are also both ordered collections, meaning they maintain the order in which elements were added.\n\n# Create a list\nmy_list = [1, 2, 3, 4, 5]\n\n# Create a tuple\nmy_tuple = (1, 2, 3, 4, 5)\n\n# Modifying a list\nmy_list.append(6)\nprint(my_list) # Output: [1, 2, 3, 4, 5, 6]\n\n# Modifying a tuple\nmy_tuple = (1, 2, 3, 4, 5)\nmy_tuple.append(6)\nprint(my_tuple) # Output: (1, 2, 3, 4, 5, 6)\n\n# Accessing elements in a list\nprint(my_list[0]) # Output: 1\n\n# Accessing elements in a tuple\nprint(my_tuple[0]) # Output: 1\n\n# Modifying a list\nmy_list.insert(1, 7)\nprint(my_list) # Output: [1, 7, 2, 3, 4, 5]\n\n# Modifying a tuple\nmy_tuple.insert(1, 7)\nprint(my_tuple) # Output: (1, 7, 2, 3, 4, 5)\n\n# Creating a list and a tuple\nmy_list2 = []\nmy_tuple2 = ()\n\n# Modifying a list\nmy_list2.append(8)\nprint(my_list2) # Output: [1, 2, 3, 4, 5, 6, 8]\n\n# Modifying a tuple\nmy_tuple2 = (1, 2, 3, 4, 5)\nmy_tuple2.append(8)\nprint(my_tuple2) # Output: (1, 2, 3, 4, 5, 8)", "retry_text": "", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "2c701ec6cbbad60f", "model": "llama3.2-1b", "prompt_class": "format", "prompt": "Describe your approach to reviewing a pull request, as a list.", "text": "import json\n\n# Define the functions as described in the prompt\ndef web_search(query, url):\n return", "retry_text": "import web_search\n\nweb_search(\"example query\", \"https://www.example.com\")", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "17930edef7c0a391", "model": "llama3.2-1b", "prompt_class": "non_english", "prompt": "Explica en una frase que es el sobreajuste.", "text": "El sobreajuste se refiere a un problema en el que dos o más variables tienen valores equivalentes o cercanos, lo que puede provocar errores en el ejecución de una programación o comprensión de un lenguaje. Este sobreajuste puede ocurrir en diversas situaciones, como en la programación con variables dinámicas, en la resolución de ecuaciones cuadráticas, o en la interpretación de lenguajes de programación.", "retry_text": "", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "4fdf373b03b9674c", "model": "qwen3-0.6b", "prompt_class": "needs_tool_web", "prompt": "Who won the most recent Formula 1 race?", "text": "The most recent Formula 1 race was held in **Monaco** in the 2023 season, and the winner was **Lohman**.", "retry_text": "The most recent Formula 1 race was held in **Monaco** in the 2023 season, and the winner was **Lohman**.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "2f93cfb96347d03b", "model": "qwen3-0.6b", "prompt_class": "needs_tool_python", "prompt": "Sort this list and tell me the median: 8, 3, 91, 12, 7, 44, 2.", "text": "Sorted list: [2, 3, 7, 8, 12, 44, 91] \n**Median**: 8", "retry_text": "Sorted list: [2, 3, 7, 8, 12, 44, 91] \n**Median**: 8", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "ffe902b3a77da3a3", "model": "qwen3-0.6b", "prompt_class": "needs_tool_python", "prompt": "Sort this list and tell me the median: 8, 3, 91, 12, 7, 44, 2.", "text": "Sorted list: [2, 3, 7, 8, 12, 44, 91] \nMedian: 8 \n\nThe median is the middle number when the list has an odd number of elements.", "retry_text": "Sorted list: [2, 3, 7, 8, 12, 44, 91] \nMedian: 8 \n\nThe median is the middle number when the list has an odd number of elements.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "43700884f9e483bb", "model": "qwen3-0.6b", "prompt_class": "needs_tool_python", "prompt": "Sort this list and tell me the median: 8, 3, 91, 12, 7, 44, 2.", "text": "The sorted list is [2, 3, 7, 8, 12, 44, 91]. The median is 8.", "retry_text": "The sorted list is [2, 3, 7, 8, 12, 44, 91]. The median is 8.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "bb267a38ffd74bb8", "model": "qwen3-0.6b", "prompt_class": "needs_tool_python", "prompt": "Sort this list and tell me the median: 8, 3, 91, 12, 7, 44, 2.", "text": "The sorted list is [2, 3, 7, 8, 12, 44, 91], and the median is 12.", "retry_text": "The sorted list is [2, 3, 7, 8, 12, 44, 91], and the median is 8.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "b8428bd477953912", "model": "qwen3-0.6b", "prompt_class": "needs_tool_python", "prompt": "Sort this list and tell me the median: 8, 3, 91, 12, 7, 44, 2.", "text": "Sorted list: [2, 3, 7, 8, 12, 44, 91] \nMedian: 7", "retry_text": "Sorted list: [2, 3, 7, 8, 12, 44, 91] \nMedian: 8", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "a6257c91990092ea", "model": "qwen3-0.6b", "prompt_class": "needs_tool_python", "prompt": "Sort this list and tell me the median: 8, 3, 91, 12, 7, 44, 2.", "text": "Sorted list: 2, 3, 7, 8, 12, 44, 91 \nMedian: 8 (the middle value when the list has an odd number of elements)", "retry_text": "Sorted list: 2, 3, 7, 8, 12, 44, 91 \nMedian: 8", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "8ca73b260971a970", "model": "qwen3-0.6b", "prompt_class": "needs_tool_python", "prompt": "Sort this list and tell me the median: 8, 3, 91, 12, 7, 44, 2.", "text": "Sorted list: 2, 3, 7, 8, 12, 44, 91 \nMedian: 8", "retry_text": "Sorted list: 2, 3, 7, 8, 12, 44, 91 \nMedian: 8", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "2cf3c930c6256595", "model": "qwen3-0.6b", "prompt_class": "needs_tool_python", "prompt": "Sort this list and tell me the median: 8, 3, 91, 12, 7, 44, 2.", "text": "Sorted list: [2, 3, 7, 8, 12, 44, 91] \nMedian: 12 \n\nThe median is 12.", "retry_text": "Sorted list: [2, 3, 7, 8, 12, 44, 91] \nMedian: 12 \n\nThe median is 12.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "c094102b1a6f9645", "model": "qwen3-0.6b", "prompt_class": "needs_tool_python", "prompt": "Calculate the standard deviation of 4, 8, 15, 16, 23, 42.", "text": "The standard deviation of the dataset 4, 8, 15, 16, 23, and 42 is approximately 12.32. \n\n**Step-by-Step Explanation:**\n1. **Calculate the Mean:** \n Sum of the numbers = 108, divided by 6 (count) = **18**. \n2. **Calculate Squared Differences:** \n (4-18)² = 196, (8-18)² = 100, (15-18)² = 9, (16-18)² = 4, (23-18)² = 25, (42-18)² = 576. \n Sum of squared differences = **910**. \n3. **Compute Variance:** \n Variance = 910 / 6 ≈ **151.666...**. \n4. **Take Square Root:** \n Standard Deviation ≈ √151.666... ≈ **12.32**. \n\n**Answer:** The standard deviation is approximately **12.32**.", "retry_text": "The standard deviation of the dataset 4, 8, 15, 16, 23, and 42 is approximately **12.32**. \n\n**Step-by-Step Explanation:**\n1. **Calculate the Mean:** \n Sum of the numbers = 108, divided by 6 = **18**. \n2. **Calculate Squared Differences:** \n (4-18)² = 196, (8-18)² = 100, (15-18)² = 9, (16-18)² = 4, (23-18)² = 25, (42-18)² = 576. \n Sum of squared differences = **910**. \n3. **Compute Variance:** \n Variance = 910 / 6 ≈ **151.666...**. \n4. **Take Square Root:** \n Standard Deviation ≈ √151.666... ≈ **12.32**. \n\n**Answer:** The standard deviation is approximately **12.32**.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "82ad4f08c9443c13", "model": "qwen3-0.6b", "prompt_class": "needs_tool_python", "prompt": "Calculate the standard deviation of 4, 8, 15, 16, 23, 42.", "text": "The standard deviation of the numbers 4, 8, 15, 16, 23, and 42 is approximately 12.32.\n\n**Step-by-Step Explanation:**\n1. **Calculate the mean:** \n $ \\frac{4 + 8 + 15 + 16 + 23 + 42}{6} = \\frac{108}{6} = 18 $.\n\n2. **Find squared deviations from the mean:** \n $ (4 - 18)^2 = 196 $, $ (8 - 18)^2 = 100 $, $ (15 - 18)^2 = 9 $, $ (16 - 18)^2 = 4 $, $ (23 - 18)^2 = 25 $, $ (42 - 18)^2 = 576 $.\n\n3. **Sum squared deviations:** \n $ 196 + 100 + 9 + 4 + 25 + 576 = 910 $.\n\n4. **Calculate variance:** \n $ \\frac{910}{6} \\approx 151.67 $.\n\n5. **Take the square root of variance:** \n $ \\sqrt{151.67} \\approx 12.32 $.\n\n**Answer:** The standard deviation is approximately 12.32.", "retry_text": "The standard deviation of the numbers 4, 8, 15, 16, 23, and 42 is approximately 12.32.\n\n**Step-by-Step Explanation:**\n1. **Calculate the mean:** \n $ \\frac{4 + 8 + 15 + 16 + 23 + 42}{6} = \\frac{108}{6} = 18 $.\n\n2. **Find squared deviations from the mean:** \n $ (4 - 18)^2 = 196 $, $ (8 - 18)^2 = 100 $, $ (15 - 18)^2 = 9 $, $ (16 - 18)^2 = 4 $, $ (23 - 18)^2 = 25 $, $ (42 - 18)^2 = 576 $.\n\n3. **Sum squared deviations:** \n $ 196 + 100 + 9 + 4 + 25 + 576 = 910 $.\n\n4. **Calculate variance:** \n $ \\frac{910}{6} \\approx 151.67 $.\n\n5. **Take the square root of variance:** \n $ \\sqrt{151.67} \\approx 12.32 $.\n\n**Answer:** The standard deviation is approximately 12.32.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "434d3a6198ee764a", "model": "qwen3-0.6b", "prompt_class": "needs_tool_python", "prompt": "Calculate the standard deviation of 4, 8, 15, 16, 23, 42.", "text": "The standard deviation of the dataset 4, 8, 15, 16, 23, 42 is approximately 12.32. \n\n**Answer:** The standard deviation is approximately 12.32.", "retry_text": "The standard deviation of the dataset 4, 8, 15, 16, 23, and 42 is approximately **12.32**. \n\n**Answer:** The standard deviation is approximately 12.32.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "9f22d6beb4d34f32", "model": "qwen3-0.6b", "prompt_class": "needs_tool_python", "prompt": "Calculate the standard deviation of 4, 8, 15, 16, 23, 42.", "text": "The standard deviation of the dataset [4, 8, 15, 16, 23, 42] is approximately 12.32. \n\n**Steps:**\n1. **Calculate the mean**: (4 + 8 + 15 + 16 + 23 + 42) / 6 = 108 / 6 = 18.\n2. **Compute squared differences from the mean**: \n - (4-18)² = 196, (8-18)² = 100, (15-18)² = 9, (16-18)² = 4, (23-18)² = 25, (42-18)² = 576.\n3. **Sum squared differences**: 196 + 100 + 9 + 4 + 25 + 576 = 910.\n4. **Divide by count (6)**: 910 / 6 ≈ 151.666.\n5. **Take square root**: √(151.666...) ≈ 12.32.", "retry_text": "The standard deviation of the dataset [4, 8, 15, 16, 23, 42] is approximately **12.32**.\n\n**Steps:**\n1. **Mean**: (4 + 8 + 15 + 16 + 23 + 42) / 6 = 108 / 6 = 18.\n2. **Squared differences**: (4-18)² = 196, (8-18)² = 100, (15-18)² = 9, (16-18)² = 4, (23-18)² = 25, (42-18)² = 576.\n3. **Sum of squared differences**: 196 + 100 + 9 + 4 + 25 + 576 = 910.\n4. **Divide by count**: 910 / 6 ≈ 151.666.\n5. **Square root**: √(151.666...) ≈ 12.32.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "b612ea05e2b54573", "model": "qwen3-0.6b", "prompt_class": "needs_tool_python", "prompt": "How many primes are there below 10000?", "text": "The number of primes below 10000 is **1684**. This value is derived using the Sieve of Eratosthenes algorithm, which efficiently marks non-prime numbers by eliminating multiples of prime numbers, ensuring accuracy without unnecessary computations.", "retry_text": "The number of primes below 10,000 is **1684**. This value is derived using the Sieve of Eratosthenes algorithm, which efficiently marks non-prime numbers by eliminating their multiples, ensuring accuracy without unnecessary computations.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "eead70908f20af93", "model": "qwen3-0.6b", "prompt_class": "needs_tool_python", "prompt": "Work out the compound interest on 5000 at 3.5% for 12 years.", "text": "The compound interest on 5000 at 3.5% for 12 years can be calculated using the formula:\n\n$$ A = P \\times (1 + r)^t $$\n\nWhere:\n- $ P = 5000 $ (principal)\n- $ r = 3.5\\% = 0.035 $\n- $ t = 12 $ (time in years)\n\nFirst, compute $ (1 + 0.035)^{12} \\approx 1.4533 $. Then, the total amount $ A $ is:\n\n$$ 5000 \\times 1.4533 \\approx 7266.5 $$\n\nThe compound interest is:\n\n$$ 7266.5 - 5000 = 2266.5 $$\n\n**Answer:** The compound interest is approximately **$2266.50**.", "retry_text": "The compound interest on $5000 at 3.5% for 12 years is approximately **$2266.50**.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "98e626024169106e", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "What is the capital of Japan?", "text": "The capital of Japan is Tokyo.", "retry_text": "The capital of Japan is Tokyo.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "5d703c2433475f71", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "What is the capital of Japan?", "text": "The capital of Japan is Tokyo.", "retry_text": "The capital of Japan is Tokyo.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "90ad72f79996edab", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "What is the capital of Japan?", "text": "The capital of Japan is Tokyo.", "retry_text": "The capital of Japan is Tokyo.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "114e4cb212318cc7", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "What is the capital of Japan?", "text": "The capital of Japan is Tokyo.", "retry_text": "The capital of Japan is Tokyo.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "e1d5199f79924aa7", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "What is the capital of Japan?", "text": "The capital of Japan is Tokyo.", "retry_text": "The capital of Japan is Tokyo.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "70d28a74a973cccc", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "What is the capital of Japan?", "text": "The capital of Japan is Tokyo.", "retry_text": "The capital of Japan is Tokyo.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "455e9970d6a3ec42", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "What is the capital of Japan?", "text": "The capital of Japan is Tokyo.", "retry_text": "The capital of Japan is Tokyo.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "82f8b8f25827ef49", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "What is the capital of Japan?", "text": "The capital of Japan is Tokyo.", "retry_text": "The capital of Japan is Tokyo.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "8806bfc3a37d5c41", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "Explain what a mutex is in two sentences.", "text": "A mutex (mutual exclusion object) is a synchronization mechanism that ensures only one thread can access a shared resource at a time, preventing race conditions. It works by locking the resource when a thread accesses it and unlocking it when another thread locks it, thus controlling concurrent access.", "retry_text": "A mutex (mutual exclusion object) is a synchronization mechanism that ensures only one thread can access a shared resource at a time, preventing race conditions by locking the resource when a thread accesses it and unlocking it when another thread locks it.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "0259d6328e80d33e", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "Explain what a mutex is in two sentences.", "text": "A mutex (mutual exclusion object) is a synchronization primitive that ensures only one thread can access a shared resource at a time, preventing race conditions. It works by blocking other threads from accessing the resource until one is signaled, ensuring thread safety in concurrent programming.", "retry_text": "A mutex (mutual exclusion object) is a synchronization primitive that ensures only one thread can access a shared resource at a time, preventing race conditions. It works by blocking other threads from accessing the resource until one is signaled, ensuring thread safety in concurrent programming.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "ee78b728ee51cc8c", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "Explain what a mutex is in two sentences.", "text": "A mutex is a synchronization primitive used to protect shared resources from concurrent access. It ensures that only one thread at a time can access the resource, preventing race conditions.", "retry_text": "A mutex is a synchronization primitive used to protect shared resources from concurrent access. It ensures that only one thread at a time can access the resource, preventing race conditions.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "c931946998269339", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "Explain what a mutex is in two sentences.", "text": "A mutex is a synchronization primitive used to protect shared resources from concurrent access, ensuring that only one thread can access them at a time. It achieves this by locking the resource when a thread needs to access it and unlocking it when the resource is free, preventing race conditions.", "retry_text": "A mutex is a synchronization primitive used to protect shared resources from concurrent access, ensuring that only one thread can access them at a time. It achieves this by locking the resource when a thread needs to access it and unlocking it when the resource is free, preventing race conditions.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "9a5a6581c9019f7f", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "Explain what a mutex is in two sentences.", "text": "A mutex (mutual exclusive lock) is a synchronization primitive used to control shared resources, ensuring that multiple threads or processes can access them without interference. It prevents race conditions by enforcing exclusive access to the resource at a time.", "retry_text": "A mutex (mutual exclusive lock) is a synchronization primitive used to control shared resources, ensuring that multiple threads or processes can access them without interference. It prevents race conditions by enforcing exclusive access to the resource at a time.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "a12923f96ffafa6e", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "Explain what a mutex is in two sentences.", "text": "A mutex (mutual exclusion object) is a synchronization primitive used to protect shared resources by ensuring that only one thread can access them at a time. It prevents race conditions by allowing one thread to acquire the lock, and another thread must relinquish the lock before it can proceed. \n\nIn two sentences: \nA mutex is a synchronization object used to protect shared resources, ensuring that only one thread can access them at a time to avoid race conditions.", "retry_text": "A mutex (mutual exclusion object) is a synchronization primitive used to protect shared resources, ensuring that only one thread can access them at a time to avoid race conditions.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "e246df27c5212f5b", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "Explain what a mutex is in two sentences.", "text": "A mutex (mutual exclusive lock) is a synchronization mechanism that protects shared resources from concurrent access by ensuring only one thread can access them at a time. It prevents race conditions by allowing only one thread to acquire the lock before others can do so. \n\nAnother way: A mutex is a tool that ensures thread safety when multiple threads access shared resources, preventing interference and ensuring data consistency.", "retry_text": "A mutex (mutual exclusive lock) is a synchronization mechanism that protects shared resources from concurrent access by ensuring only one thread can access them at a time. It prevents race conditions by allowing only one thread to acquire the lock before others can do so. \n\nAnother way: A mutex is a tool that ensures thread safety when multiple threads access shared resources, preventing interference and ensuring data consistency.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "55adde375e13956d", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "Explain what a mutex is in two sentences.", "text": "A mutex is a synchronization primitive used to protect shared resources from concurrent access. It ensures that only one thread can access the resource at a time, preventing race conditions by controlling access to shared data.", "retry_text": "A mutex is a synchronization primitive used to protect shared resources from concurrent access. It ensures that only one thread can access the resource at a time, preventing race conditions by controlling access to shared data.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "f3ae8b3e9485a3f7", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "In plain English, what is a transformer model?", "text": "A transformer model is a type of neural network used in machine learning and natural language processing to handle long-range dependencies in sequences. Unlike traditional models like recurrent or RNNs, transformers use self-attention mechanisms to process the input in a way that allows the model to focus on relevant parts of the sequence, making them more efficient for tasks like language modeling and text generation.", "retry_text": "A transformer model is a type of neural network used in machine learning and natural language processing to handle long-range dependencies in sequences. Unlike traditional models like recurrent or RNNs, transformers use self-attention mechanisms to focus on relevant parts of the input, making them more efficient for tasks like language modeling and text generation.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "7501627df64f5901", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "In plain English, what is a transformer model?", "text": "A transformer model is a type of neural network designed to process long sequences of text, such as sentences or paragraphs, more efficiently than traditional models like RNNs or LSTMs. Here's a simple explanation:\n\n1. **Core Idea**: Transformers use self-attention mechanisms to focus on specific parts of the input at different times. Unlike traditional models, which process information sequentially, transformers can handle complex, long-term dependencies in text.\n\n2. **Key Advancement**: This allows models to understand and generate text even when the input is very long or contains complex relationships between words.\n\n3. **Common Use Cases**: Transformer models are widely used in tasks like language modeling (text generation, translation), summarization, and summarizing long documents.\n\nIn plain English, transformers enable powerful processing of text with greater efficiency and flexibility.", "retry_text": "A transformer model is a type of neural network designed to process long sequences of text, like sentences or paragraphs, more efficiently than traditional models like RNNs or LSTMs. It uses self-attention mechanisms to focus on specific parts of the input at different times, allowing it to handle complex, long-term dependencies in text.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "91e15fc0eb0e2627", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "In plain English, what is a transformer model?", "text": "A transformer model is a type of machine learning model used in **natural language processing (NLP)** to handle complex text and sequences. Here's a simple explanation:\n\n- **Purpose**: It's designed to process long sequences of text (like paragraphs or sentences) and understand context, which is useful for tasks like language translation, text generation, or summarization.\n- **Key Mechanism**: Unlike traditional models like RNNs or LSTMs, transformers use **self-attention** to dynamically determine which parts of the input to consider relevantly. This allows them to focus on the context and not just memorize the data.\n- **Comparison**: Unlike RNNs or LSTMs, which process data sequentially, transformers can handle long sequences more efficiently.\n\nIn short, a transformer model helps machines understand and generate text more effectively by focusing on context and long-term dependencies.", "retry_text": "A transformer model is a type of machine learning model used in **natural language processing (NLP)** to handle complex text and sequences. Here's a simple explanation:\n\n- **Purpose**: It's designed to process long sequences of text (like paragraphs or sentences) and understand context, which is useful for tasks like language translation, text generation, or summarization. \n- **Key Mechanism**: Unlike traditional models like RNNs or LSTMs, transformers use **self-attention** to dynamically determine which parts of the input to consider relevantly. This allows them to focus on context and not just memorize the data. \n- **Comparison**: Unlike RNNs or LSTMs, which process data sequentially, transformers can handle long sequences more efficiently. \n\nIn short, a transformer model helps machines understand and generate text more effectively by focusing on context and long-term dependencies.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "5c344eaff3a31f90", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "In plain English, what is a transformer model?", "text": "A transformer model is a type of deep learning model used to process and generate text or other sequential data. Unlike traditional neural networks that use recurrent units (like RNNs), transformers use self-attention mechanisms to focus on relevant parts of the input. This makes them highly efficient for handling long, complex sequences of text, such as in machine translation or text generation tasks.", "retry_text": "A transformer model is a type of deep learning model used to process and generate text or other sequential data. Unlike traditional neural networks that use recurrent units (like RNNs), transformers use self-attention mechanisms to focus on relevant parts of the input. This makes them highly efficient for handling long, complex sequences of text, such as in machine translation or text generation.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "70de4d7453ea6e5f", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "In plain English, what is a transformer model?", "text": "A transformer model is a type of neural network used to process and understand text, like how humans do. It works by breaking down long sequences of text into smaller parts. Transformers use self-attention to focus on different parts of the text, allowing them to process information efficiently. This makes them great for tasks like language translation, text generation, and speech recognition.", "retry_text": "A transformer model is a type of neural network used in natural language processing to process and understand text, like how humans do. It works by breaking down long sequences of text into smaller parts, using self-attention to focus on different parts of the text, and allowing efficient processing of information. This makes it great for tasks like language translation, text generation, and speech recognition.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "fec9676d5b701fb7", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "In plain English, what is a transformer model?", "text": "A transformer model is a type of neural network used in **natural language processing** to help with tasks like language modeling, text generation, and machine translation. Here's a simple breakdown in plain English:\n\n- **Purpose**: Processes long sequences of text (like sentences or paragraphs) to understand and generate human-like content.\n- **Key Components**: \n - **Self-Attention**: Allows the model to weigh the importance of different parts of the input sequence. \n - **Encoder-Decoder Architecture**: Breaks the problem into encoding the input text and decoding it into output text. \n- **Example**: Uses a transformer to translate text from one language to another, or generate text that aligns with a given prompt.\n\nIn short, a transformer model is a powerful tool for handling complex patterns in sequential data.", "retry_text": "A transformer model is a type of neural network used in natural language processing (NLP) to help with tasks like language modeling, text generation, and machine translation. In plain English:\n\n- It processes long sequences of text (e.g., sentences or paragraphs) to understand and generate human-like content. \n- It uses **self-attention** to weigh the importance of different parts of the input, allowing it to learn patterns and dependencies between words. \n- The model is structured as an **encoder-decoder** system, breaking the problem into encoding the input and decoding it into output. \n- Examples include translating text between languages or generating creative text. \n\nIn summary, a transformer model helps machines understand complex patterns in text by focusing on relationships between words rather than just individual elements.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "2d91756e22f14192", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "In plain English, what is a transformer model?", "text": "A transformer model is a type of neural network designed to process and understand long sequences of text, such as in natural language processing tasks. Unlike traditional RNNs or LSTMs, which process information sequentially, transformers use **self-attention** to dynamically highlight and focus on specific parts of the input, allowing them to handle complex, long-term dependencies in text more effectively. This makes them powerful for tasks like machine translation and text generation.", "retry_text": "A transformer model is a type of neural network used in natural language processing to handle long sequences of text. Unlike traditional RNNs or LSTMs (which process information sequentially), transformers use **self-attention** to dynamically highlight and focus on specific parts of the input, enabling them to process complex, long-term dependencies more effectively. This makes them powerful for tasks like machine translation and text generation.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "62f5e4af91d404bc", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "In plain English, what is a transformer model?", "text": "A transformer model is a type of neural network used in machine learning to process and transform input data efficiently. Unlike traditional models like RNNs or LSTMs, which process information sequentially, transformer models use **self-attention** to focus on specific parts of the input at different times. This allows them to learn complex patterns and dependencies in the data more effectively. The core idea is that the model can dynamically \"look\" at multiple parts of the input, enabling it to capture context and relationships in a way that traditional models might not.", "retry_text": "A transformer model is a type of neural network used in machine learning to process and transform input data efficiently. Unlike traditional models like RNNs or LSTMs, which process information sequentially, transformer models use **self-attention** to focus on specific parts of the input at different times. This allows them to learn complex patterns and dependencies in the data more effectively.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "29d4bd9e22b119a2", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "What is 2 + 2?", "text": "2 + 2 equals 4.", "retry_text": "2 + 2 equals 4.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "fccda71d724525a4", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "What is 2 + 2?", "text": "2 + 2 equals 4.", "retry_text": "2 + 2 equals 4.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "4368361fe083e78b", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "What is 2 + 2?", "text": "The result of 2 + 2 is 4.", "retry_text": "2 + 2 equals 4.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "d1d0c89509f6e2b1", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "What is 2 + 2?", "text": "2 + 2 equals 4.", "retry_text": "2 + 2 equals 4.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "553666af55035ee6", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "What is 2 + 2?", "text": "2 + 2 equals 4.", "retry_text": "2 + 2 equals 4.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "09332e87b52a264c", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "What is 2 + 2?", "text": "2 + 2 equals 4.", "retry_text": "2 + 2 equals 4.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "02910af1afb0fdeb", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "What is 2 + 2?", "text": "2 + 2 equals 4.", "retry_text": "2 + 2 equals 4.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "2ae2f8f18e6fb620", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "What is 2 + 2?", "text": "2 + 2 equals 4.", "retry_text": "2 + 2 equals 4.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "5ebee57bef9e7cc3", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "Give me a short definition of overfitting.", "text": "Overfitting occurs when a model learns too much from the training data, leading to poor generalization and high error rates.", "retry_text": "Overfitting occurs when a model learns too much from the training data, leading to poor generalization and high error rates.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "653926096e613ab4", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "Give me a short definition of overfitting.", "text": "Overfitting occurs when a model's complexity increases beyond the actual data, causing it to perform poorly on new, unseen data.", "retry_text": "Overfitting occurs when a model's complexity increases beyond the actual data, causing it to perform poorly on new, unseen data.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "55755ab88045d4cf", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "Give me a short definition of overfitting.", "text": "Overfitting occurs when a model's training data is too similar to the test data, causing the model to learn the patterns present in the training data instead of the underlying generalization.", "retry_text": "Overfitting occurs when a model learns too much from the training data, making it perform well on the training set but poorly on the test set.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "407cb9a2b45d875e", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "Give me a short definition of overfitting.", "text": "Overfitting occurs when a model learns the training data too well, leading to poor performance on new, unseen data.", "retry_text": "Overfitting occurs when a model learns the training data too well, leading to poor performance on new, unseen data.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "52fadbaf0c564664", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "Give me a short definition of overfitting.", "text": "Overfitting occurs when a model learns too much from the training data, leading to poor generalization to new, unseen data.", "retry_text": "Overfitting occurs when a model learns too much from the training data, leading to poor generalization to new, unseen data.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "b3ea25024fa7c2e5", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "Give me a short definition of overfitting.", "text": "Overfitting occurs when a model learns too much from the training data, leading to poor generalization and poor performance on new, unseen data.", "retry_text": "Overfitting occurs when a model learns too much from the training data, leading to poor generalization and poor performance on new, unseen data.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "e253899c9a49119c", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "Give me a short definition of overfitting.", "text": "Overfitting refers to a situation in machine learning where a model learns the training data too well, making it perform poorly on new, unseen data.", "retry_text": "Overfitting refers to a situation in machine learning where a model learns the training data too well, making it perform poorly on new, unseen data.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "cff4f8216561e80a", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "Give me a short definition of overfitting.", "text": "Overfitting occurs when a model's parameters become too specific to the training data, leading it to perform well on the training set but poorly on new, unseen data.", "retry_text": "Overfitting occurs when a model's parameters become too specific to the training data, leading it to perform well on the training set but poorly on new, unseen data.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "5f3fda191881eae4", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "Who wrote Pride and Prejudice?", "text": "The novel *Pride and Prejudice* was written by **Jane Austen**. It was published in 1813.", "retry_text": "Jane Austen.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "53710dec1aba5d15", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "What is the difference between a list and a tuple in Python?", "text": "A list and a tuple in Python are both used to store collections of elements, but they have key differences:\n\n### **Difference Between List and Tuple:**\n1. **Mutability:**\n - **List:** Mutable, allows elements to be added, removed, or modified in-place.\n - **Tuple:** Immutable, cannot be changed once created.\n\n2. **Fixed Length (Tuples):**\n - **Tuple:** Has a fixed length, and all elements must be the same type.\n - **List:** Can have variable lengths and elements of different types.\n\n3. **Uniqueness:**\n - **Tuple:** Can be initialized with all elements, but lists can have any elements.\n - **List:** Can be initialized with any elements, not just a single element.\n\n### **Example:**\n```python\n# List\nmy_list = [1, 2, 3]\nmy_list.append(4) # List is mutable\nprint(my_list) # Output: [1, 2, 3, 4]\n\n# Tuple\nmy_tuple = (1, 2, 3)\n# Cannot modify my_tuple\nprint(my_tuple) # Output: (1, 2, 3)\n```\n\nThese differences make lists suitable for dynamic data and tuples for static, unchangeable data.", "retry_text": "A list and a tuple in Python differ in several key aspects:\n\n1. **Mutability:** \n - **List:** Mutable, allows elements to be added, removed, or modified in-place. \n - **Tuple:** Immutable, cannot be changed once created.\n\n2. **Fixed Length:** \n - **Tuple:** Has a fixed length and all elements must be of the same type. \n - **List:** Can have variable lengths and elements of different types.\n\n3. **Uniqueness:** \n - **Tuple:** Can be initialized with all elements, but lists can have any elements. \n - **List:** Can be initialized with any elements, not just a single element.\n\n**Example:** \n```python\n# List\nmy_list = [1, 2, 3]\nmy_list.append(4) # List is mutable\nprint(my_list) # Output: [1, 2, 3, 4]\n\n# Tuple\nmy_tuple = (1, 2, 3)\n# Cannot modify my_tuple\nprint(my_tuple) # Output: (1, 2, 3)\n```", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "b681f92c716c4f2a", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "What is the difference between a list and a tuple in Python?", "text": "In Python, a **list** and a **tuple** are both used to store collections of elements, but they differ in several key aspects:\n\n### 1. **Immutable vs. Mutable** \n- **Tuple**: Once created, its elements **cannot be changed**. \n Example: `(1, 2, 3)` → changes to `2, 3, 4` would break the tuple. \n- **List**: Elements can be **modified** as long as you don't change them. \n Example: `[1, 2, 3]` → modifying `3` to `2` would update the list.\n\n### 2. **Element Type Consistency** \n- **Tuple**: All elements must be of the **same type** (e.g., integers, strings). \n- **List**: Elements can be of any type (e.g., integers, strings, etc).\n\n### 3. **Use Cases** \n- **Tuple**: Best when the data **remains constant** or **cannot be modified**. \n- **List**: Best when the data **can be updated or modified**.\n\n### Example\n```python\n# Tuple (immutable) \nt1 = (1, 2, 3) \nt2 = t1 + (4, 5) # t2 becomes (1, 2, 3, 4, 5)\n\n# List (mutable) \nl1 = [1, 2, 3] \nl2 = l1 + [4, 5] # l2 becomes (1, 2, 3, 4, 5)\n```\n\n### Summary\n- **Tuple**: Best for data that won’t change, or for immutable data. \n- **List**: Best for dynamic, mutable data.", "retry_text": "In Python, a **list** and a **tuple** are both used to store collections of elements, but they differ in several key aspects:\n\n### 1. **Immutable vs. Mutable** \n- **Tuple**: Once created, its elements **cannot be changed**. \n Example: `(1, 2, 3)` → changes to `2, 3, 4` would break the tuple. \n- **List**: Elements can be **modified** as long as you don't change them. \n Example: `[1, 2, 3]` → modifying `3` to `2` would update the list.\n\n### 2. **Element Type Consistency** \n- **Tuple**: All elements must be of the **same type** (e.g., integers, strings). \n- **List**: Elements can be of any type (e.g., integers, strings, etc).\n\n### 3. **Use Cases** \n- **Tuple**: Best for immutable data or when the data **remains constant**. \n- **List**: Best for dynamic, mutable data where elements can be updated or modified.\n\n### Example\n```python\n# Tuple (immutable) \nt1 = (1, 2, 3) \nt2 = t1 + (4, 5) # t2 becomes (1, 2, 3, 4, 5)\n\n# List (mutable) \nl1 = [1, 2, 3] \nl2 = l1 + [4, 5] # l2 becomes (1, 2, 3, 4, 5)\n```\n\n### Summary \n- **Tuple**: Best for data that won’t change or for immutable data. \n- **List**: Best for dynamic, mutable data.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "2c69370b7ac37217", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "What is the difference between a list and a tuple in Python?", "text": "In Python, the main difference between a list and a tuple is:\n\n1. **Mutability**: \n - A **list** is mutable, meaning you can add, remove, or modify elements. \n - A **tuple** is immutable, so once created, its elements cannot be changed.\n\n2. **Fixed Length**: \n - Tuples have a fixed length and cannot change their elements. \n - Lists can grow or shrink dynamically as elements are added or removed.\n\n3. **Access**: \n - Tuples are accessed using indexes, while lists use indices as well. \n\n4. **Use Cases**: \n - Lists are ideal for dynamic data where changes are frequent. \n - Tuples are used when you need to store data that should not change, such as coordinates or a sequence of numbers.", "retry_text": "In Python, the main difference between a list and a tuple is:\n\n1. **Mutability**: \n - A **list** is mutable (you can change its elements). \n - A **tuple** is immutable (elements cannot be changed once created).\n\n2. **Fixed Length**: \n - Tuples have a fixed length and cannot change their elements. \n - Lists can grow or shrink dynamically.\n\n3. **Access**: \n - Both lists and tuples use indices for access, though tuples are accessed using indexes directly.\n\n4. **Use Cases**: \n - Lists are ideal for dynamic data with frequent changes. \n - Tuples are used for data that should remain constant, such as coordinates or a sequence of numbers.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "a53e2fdb6fe27851", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "What is the difference between a list and a tuple in Python?", "text": "In Python, a **list** and a **tuple** are both iterable data types, but they have key differences:\n\n1. **Modification**: \n - **Lists** can be modified (e.g., `list.append()`, `list.remove()`, `list.insert()`) after creation. \n - **Tuples** cannot be modified once created.\n\n2. **Length**: \n - **Tuples** have a **fixed length** and cannot be expanded or shrunk. \n - **Lists** can dynamically grow or shrink.\n\n3. **Hashing**: \n - Tuples can be used for hashing (e.g., `hash(tuple)`) and are often used for unique identifiers. \n - Lists can also have a `hash()` method.\n\n4. **Use Cases**: \n - **Lists** are mutable and used for dynamic data. \n - **Tuples** are immutable and used for fixed, unchanging data.\n\nFor example: \n```python\nmy_list = [1, 2, 3] \nmy_list.append(4) # List changes \nprint(my_list) # Output: [1, 2, 3, 4] \n\nmy_tuple = (1, 2, 3) \nprint(my_tuple) # Output: (1, 2, 3) \n```", "retry_text": "In Python, a list and a tuple are both iterable data types but differ in their behaviors:\n\n1. **Modification**: \n - **Lists** can be modified (e.g., appended, removed, or inserted elements). \n - **Tuples** cannot be modified once created.\n\n2. **Length**: \n - **Tuples** have a **fixed length** and cannot be expanded or shrunk.\n\n3. **Hashing**: \n - Tuples can be used for hashing and are often used for unique identifiers. \n - Lists can also have a `hash()` method.\n\n4. **Use Cases**: \n - **Lists** are mutable and used for dynamic data. \n - **Tuples** are immutable and used for fixed, unchanging data.\n\nFor example: \n```python\nmy_list = [1, 2, 3] \nmy_list.append(4) # List changes \nprint(my_list) # Output: [1, 2, 3, 4] \n\nmy_tuple = (1, 2, 3) \nprint(my_tuple) # Output: (1, 2, 3)\n```", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "91fc9d1fb351d237", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "What is the difference between a list and a tuple in Python?", "text": "In Python, a **list** and a **tuple** are both used to store collections of data, but they differ in several key aspects:\n\n1. **Mutability**:\n - A **list** is **mutable**, meaning it can be modified (inserted, deleted, or updated elements).\n - A **tuple** is **immutable**, so its elements cannot be changed once created.\n\n2. **Order**:\n - Tuples preserve the order of elements, while lists also maintain the original order when elements are modified.\n\n3. **Use Cases**:\n - **Lists** are used when you want to change the order or add elements dynamically.\n - **Tuples** are used when you want to pass a collection of values as a group and not modify them.\n\nFor example:\n```python\nmy_list = [1, 2, 3]\nmy_list.append(4) # List is mutable\nprint(my_list) # Output: [1, 2, 3, 4]\n\nmy_tuple = (1, 2, 3)\nmy_tuple[0] = 5 # Tuple is immutable\nprint(my_tuple) # Output: (1, 2, 3)\n```\n\nThe main difference lies in their immutability and how they handle changes.", "retry_text": "A list and a tuple in Python differ primarily in their **mutability** and **behavior**:\n\n1. **Mutability**:\n - A **list** is **mutable** (can be modified, added, or removed elements).\n - A **tuple** is **immutable** (cannot be changed once created).\n\n2. **Order**:\n - Tuples preserve the original order of elements, while lists also maintain that order.\n\n3. **Use Cases**:\n - **Lists** are used when you dynamically modify or add elements.\n - **Tuples** are used when you want to pass a collection of values as a group and not modify them.\n\nTuples are immutable, making them ideal for static data structures, while lists allow dynamic modifications.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "616079ceb9f98220", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "What is the difference between a list and a tuple in Python?", "text": "In Python, a **list** and a **tuple** are both used to store collections of items, but they differ in several key aspects:\n\n1. **Uniqueness**: \n - A **tuple** is **uniquely defined** and cannot be modified once created. \n - A **list** is **dynamic** and allows adding or removing elements at any time. \n\n2. **Immutable**: \n - Tuples are **immutable** and cannot be changed after creation. \n - Lists are **mutable** and can be modified as needed. \n\n3. **Order**: \n - Tuples maintain a **fixed order** of elements. \n - Lists allow dynamic reordering (e.g., appending or removing elements). \n\n4. **Use Cases**: \n - **Tuples** are ideal for data that must remain unchanged, such as coordinates, constants, or fixed-order collections. \n - **Lists** are used for dynamic data, such as a list of names, a list of items to be added or removed in an application. \n\nFor example: \n- A tuple: `(\"apple\", \"banana\")` \n- A list: `[\"banana\", \"apple\", \"cherry\"]` \n\nThis distinction is crucial in Python when handling data structures and performance considerations.", "retry_text": "A **list** and a **tuple** are both used to store collections of items in Python, but they differ in several key aspects: \n\n1. **Uniqueness**: \n - A **tuple** is **uniquely defined** and cannot be modified once created. \n - A **list** is **dynamic** and allows adding or removing elements at any time. \n\n2. **Immutable**: \n - Tuples are **immutable** and cannot be changed after creation. \n - Lists are **mutable** and can be modified as needed. \n\n3. **Order**: \n - Tuples maintain a **fixed order** of elements. \n - Lists allow dynamic reordering (e.g., appending or removing elements). \n\n4. **Use Cases**: \n - **Tuples** are ideal for data that must remain unchanged, such as coordinates, constants, or fixed-order collections. \n - **Lists** are used for dynamic data, such as a list of names, a list of items to be added or removed in an application. \n\nFor example: \n- A tuple: `(\"apple\", \"banana\")` \n- A list: `[\"banana\", \"apple\", \"cherry\"]` \n\nThis distinction is crucial in Python when handling data structures and performance considerations.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "2078c91883c4a353", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "What is the difference between a list and a tuple in Python?", "text": "In Python, a **list** and a **tuple** are both iterable, but they differ in several key aspects:\n\n### 1. **Immutable vs. Mutable**\n- A **list** is **mutable**, meaning it can be changed or modified.\n- A **tuple** is **immutable**, so changes to its elements require creating a new tuple.\n\n### 2. **Dynamic vs. Static**\n- A **list** is dynamic—its elements can be added or removed, and its length can grow.\n- A **tuple** is static—its elements cannot be changed once created.\n\n### 3. **Use Cases**\n- **Lists** are ideal for data that can be dynamically added or modified (e.g., a list of coordinates, a list of names).\n- **Tuples** are used for data that cannot be modified (e.g., coordinates, a fixed set of values).\n\n### Example\n```python\n# List\nmy_list = [1, 2, 3]\nmy_list[0] = 4 # Changes the first element\nmy_list.append(4) # Adds an element\n\n# Tuple\nmy_tuple = (1, 2, 3)\nmy_tuple[0] = 5 # Cannot change, so a new tuple is created\n```\n\nThis distinction is fundamental in Python for handling different types of data efficiently.", "retry_text": "In Python, a **list** and a **tuple** are both iterable, but they differ in several key aspects:\n\n1. **Immutable vs. Mutable**:\n - A **list** is **mutable** (can be changed or modified).\n - A **tuple** is **immutable** (cannot be changed once created).\n\n2. **Dynamic vs. Static**:\n - A **list** is **dynamic** (elements can be added, removed, or modified).\n - A **tuple** is **static** (elements cannot be changed).\n\n3. **Use Cases**:\n - **Lists** are ideal for data that can be dynamically added or modified (e.g., a list of coordinates, a list of names).\n - **Tuples** are used for data that cannot be modified (e.g., coordinates, a fixed set of values).\n\nExample:\n```python\n# List\nmy_list = [1, 2, 3]\nmy_list[0] = 4 # Changes the first element\nmy_list.append(4) # Adds an element\n\n# Tuple\nmy_tuple = (1, 2, 3)\nmy_tuple[0] = 5 # Cannot change, so a new tuple is created\n```\n\nThis distinction is fundamental in Python for handling different types of data efficiently.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "fd3dec4b84816b0c", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "What is the difference between a list and a tuple in Python?", "text": "In Python, a **list** and a **tuple** are both used to store collections of items, but they differ in several key aspects:\n\n### 1. **Mutability**:\n - **List**: Mutable. You can change its elements after creation (e.g., `list.append()` or `list.remove()`) to modify the contents.\n - **Tuple**: Immutable. Once created, its elements cannot be changed, and any attempt to modify a tuple element will throw an error.\n\n### 2. **Structure**:\n - **List**: Elements are added in **order**, and elements can be inserted or removed at any position.\n - **Tuple**: Elements are added in **order**, and elements cannot be inserted or removed, making it static.\n\n### 3. **Comparison**:\n - When comparing two lists or tuples, they are compared element-wise. If a list has elements with different lengths, the longer list is considered \"greater\" in a tuple-wise comparison.\n - When comparing a list and a tuple, they are compared as a whole, and if the lists or tuples have different lengths, the one with more elements is considered greater.\n\n### 4. **Use Cases**:\n - **List**: For dynamic data that can be modified (e.g., a list of people, a list of prices).\n - **Tuple**: For static data that cannot be changed (e.g., a list of coordinates or a fixed set of values).\n\n### Example:\n```python\nmy_list = [1, 2, 3]\nmy_list.append(4)\nprint(my_list) # Output: [1, 2, 3, 4]\n\nmy_tuple = (1, 2, 3)\nprint(my_tuple) # Output: (1, 2, 3)\n```\n\nThis shows how the differences in mutability and structure affect their usage.", "retry_text": "In Python, a **list** and a **tuple** are both used to store collections of items, but they differ in several key aspects:\n\n### 1. **Mutability**:\n - **List**: Mutable. You can change its elements after creation (e.g., `list.append()` or `list.remove()`) to modify the contents.\n - **Tuple**: Immutable. Once created, its elements cannot be changed, and any attempt to modify a tuple element will throw an error.\n\n### 2. **Structure**:\n - **List**: Elements are added in **order**, and elements can be inserted or removed at any position.\n - **Tuple**: Elements are added in **order**, and elements cannot be inserted or removed, making it static.\n\n### 3. **Comparison**:\n - When comparing lists or tuples, they are compared element-wise. If a list has elements with different lengths, the longer list is considered \"greater\" in a tuple-wise comparison.\n - When comparing a list and a tuple, they are compared as a whole, and if the lists or tuples have different lengths, the one with more elements is considered greater.\n\n### 4. **Use Cases**:\n - **List**: For dynamic data that can be modified (e.g., a list of people, a list of prices).\n - **Tuple**: For static data that cannot be changed (e.g., a list of coordinates or a fixed set of values).\n\n### Example:\n```python\nmy_list = [1, 2, 3]\nmy_list.append(4)\nprint(my_list) # Output: [1, 2, 3, 4]\n\nmy_tuple = (1, 2, 3)\nprint(my_tuple) # Output: (1, 2, 3)\n```", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "41da97d37a1bfe04", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "Summarise what LoRA does in one paragraph.", "text": "LoRA (Low-Rank Adaptation) is a technique used in training large language models to improve their performance during training by adding a small linear transformation to the model's weights. This method helps in optimizing the model's parameters more efficiently by leveraging the model's existing structure, allowing for faster training without significant computational overhead.", "retry_text": "LoRA (Low-Rank Adaptation) is a technique used in training large language models to enhance their performance by introducing a small, linear transformation to the model's weights. This method allows for more efficient training by leveraging the model's existing structure, reducing computational overhead and enabling faster parameter optimization.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "7d705f85c729f2d5", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "Summarise what LoRA does in one paragraph.", "text": "LoRA stands for LoRALayer-wise, a technique used in training large language models (LLMs) to enhance performance and efficiency. It works by adding a small linear layer to the model's weights, enabling the model to learn the same representations without updating all the model's weights. This approach reduces computational overhead and speeds up training, particularly beneficial for very large models.", "retry_text": "LoRA stands for LoRALayer-wise, a technique used in training large language models (LLMs) to enhance performance and efficiency. It works by adding a small linear layer to the model's weights, enabling the model to learn the same representations without updating all the model's weights. This approach reduces computational overhead and speeds up training, particularly beneficial for very large models.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "e712132213910e8a", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "Summarise what LoRA does in one paragraph.", "text": "LoRA (LoRALayer) is a technique used in training large language models to improve efficiency by adding a small set of additional parameters to the main model. These parameters are not updated during training, allowing the model to be trained more quickly and with less computational cost. This approach reduces the number of parameters in the model while enabling faster and more effective training.", "retry_text": "LoRA (LoRALayer) is a technique used in training large language models to enhance efficiency by adding a small set of additional parameters to the main model. These parameters are not updated during training, allowing the model to be trained more quickly and with less computational cost. This approach reduces the number of parameters while enabling faster and more effective training.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "ed971da889aa00c5", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "Summarise what LoRA does in one paragraph.", "text": "LoRA (LoRA Optimization) is a technique used to enhance the efficiency and performance of training transformer models, such as BERT or GPT, by adding a small set of specific parameters to the base model. These parameters are designed to improve the model's ability to learn, allowing it to train faster and use less computational resources. This optimization technique enables models to achieve better accuracy without requiring a significant increase in model size or complexity.", "retry_text": "LoRA (LoRA Optimization) is a technique designed to enhance the efficiency and performance of transformer models like BERT or GPT by adding a small set of specific parameters to the base model. These parameters are tailored to improve the model's ability to learn, enabling faster training and reduced computational resource usage. This optimization allows models to achieve better accuracy without requiring a substantial increase in model size or complexity.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "cb283f6344409751", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "Summarise what LoRA does in one paragraph.", "text": "LoRA stands for Low-Rank Adaptation, a technique used to improve the performance of neural networks by adding a low-rank matrix to the model's weight matrix. This allows the model to adapt more efficiently and with less computational cost, making it suitable for tasks like language modeling where large-scale parameters are common.", "retry_text": "LoRA stands for Low-Rank Adaptation, a technique used to improve the performance of neural networks by adding a low-rank matrix to the model's weight matrix. This allows the model to adapt more efficiently and with less computational cost, making it suitable for tasks like language modeling where large-scale parameters are common.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "062b1b577a41b87a", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "Summarise what LoRA does in one paragraph.", "text": "LoRA (LoRA Optimization) is a technique used in deep learning models to improve performance by adding additional layers to the attention mechanism in large language models (like BERT or GPT). This optimization allows the model to be more efficient in training, as it reduces the number of parameters while maintaining high accuracy, which is crucial for handling large-scale tasks.", "retry_text": "LoRA (LoRA Optimization) is a technique used in deep learning models to improve performance by adding additional layers to the attention mechanism in large language models (like BERT or GPT). This optimization allows the model to be more efficient in training, as it reduces the number of parameters while maintaining high accuracy, which is crucial for handling large-scale tasks.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "7b07271fa7175e04", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "Summarise what LoRA does in one paragraph.", "text": "LoRA stands for LoRALayer and is a technique used in transformer models to enhance their performance. It allows for efficient fine-tuning of large models by modifying the weights in a way that improves the learning of the model without requiring full retraining. This method enables the model to learn better representations without significant computational overhead.", "retry_text": "LoRA stands for LoRALayer, a technique used in transformer models to enhance performance by allowing efficient fine-tuning of large models. It modifies the weights in a way that improves the model's learning without requiring full retraining, enabling the model to learn better representations without significant computational overhead.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "126f4d579ccec344", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "Summarise what LoRA does in one paragraph.", "text": "LoRA (LoRALayerwise) is a technique that modifies the weights of a model's layers in a specific position, allowing for enhanced performance during training by fine-tuning the model's parameters. This approach enables efficient training by reducing computational overhead without sacrificing model accuracy, particularly in scenarios where the model's parameters need to be optimized for specific tasks.", "retry_text": "LoRA (LoRALayerwise) is a technique that modifies the weights of a model's layers in a specific position, enabling efficient training by fine-tuning the model's parameters. This approach reduces computational overhead while preserving model accuracy, particularly in scenarios where parameter optimization is needed.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "1d74acd146fcd5c3", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "Is Python interpreted or compiled?", "text": "Python is interpreted and compiled. It works in a way where the interpreter runs the code directly, while the compiler (used in the Python standard library) is optimized for performance. The interpreter handles the runtime execution, and the compiler (in C) is used for code optimization and performance.", "retry_text": "Python is interpreted and compiled. It works in a way where the interpreter runs the code directly, while the compiler (used in the Python standard library) is optimized for performance. The interpreter handles the runtime execution, and the compiler (in C) is used for code optimization and performance.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "a88f0061096b96f8", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "Is Python interpreted or compiled?", "text": "Python is interpreted. The interpreter reads the code and executes it, which is a common way Python functions work.", "retry_text": "Python is interpreted. The interpreter reads and executes the code as it runs, which is a common way Python functions work.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "4eccea6235fa63b0", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "Is Python interpreted or compiled?", "text": "Python is interpreted and can be compiled. When you run Python code, it uses a runtime interpreter (interpreted mode) to execute the code. For performance, Python can also be compiled into a C extension module, which is interpreted at runtime (compiled mode).", "retry_text": "Python is interpreted and can be compiled. When you run Python code, it uses a runtime interpreter (interpreted mode) to execute the code. For performance, Python can also be compiled into a C extension module, which is interpreted at runtime (compiled mode).", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "d76dead29a1db13f", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "Is Python interpreted or compiled?", "text": "Python is interpreted by default. It reads and executes code directly from files, which is the most common way it's used. If you're using the Python interpreter (like in an environment), it runs the code as a script. If you're using a compiled version, like a bytecode file stored in an `.exe` or `.pyc` file, it's executed as a compiled program. Python is not a compiler—it's a language that can be interpreted or compiled at runtime.", "retry_text": "Python is interpreted by default. It reads and executes code directly from files, which is the most common way it's used. If you're using the Python interpreter (like in an environment), it runs the code as a script. If you're using a compiled version, like a bytecode file stored in an `.exe` or `.pyc` file, it's executed as a compiled program. Python is not a compiler—it's a language that can be interpreted or compiled at runtime.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "e455113745e55075", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "Is Python interpreted or compiled?", "text": "Python is interpreted and compiled. Here's a breakdown:\n\n- **Interpreted**: Python code is executed directly in the interpreter, meaning it runs as a program without needing a compiler. This is the standard method used.\n- **Compiled**: Python can also be compiled into bytecode (like in CPython), which is then executed by the interpreter. This approach is more efficient for performance-critical applications.\n\nPython is a **general-purpose language** that allows developers to write code that can be interpreted or compiled, depending on the environment.", "retry_text": "Python is interpreted and compiled. Here's a breakdown:\n\n- **Interpreted**: Python code is executed directly in the interpreter, meaning it runs as a program without requiring a compiler (this is the standard method).\n- **Compiled**: Python can also be compiled into bytecode (like in CPython), which is then executed by the interpreter. This approach is more efficient for performance-critical applications.\n\nPython is a **general-purpose language** that allows developers to write code that can be interpreted or compiled, depending on the environment.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "614649027bb66232", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "Is Python interpreted or compiled?", "text": "Python is interpreted or compiled depending on the context. When you run Python code, it is interpreted, meaning the code is executed directly without compilation. However, when you use compiled code (like using a library module), it's compiled into a module that can be executed quickly. Python uses a combination of interpreted and compiled code to achieve this.", "retry_text": "Python is interpreted or compiled depending on the context. When you run Python code, it is interpreted, meaning the code is executed directly without compilation. However, when you use compiled code (like using a library module), it is compiled into a module that can be executed quickly. Python uses a combination of interpreted and compiled code to achieve this.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "6814c671025ade74", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "Is Python interpreted or compiled?", "text": "Python is interpreted, not compiled. The interpreter processes and executes the code directly, translating it into machine code for execution. This contrasts with compiled languages like C++ or Java, where the code is compiled into a single file (a \"linked list\") that can be run without recompilation.", "retry_text": "Python is interpreted, not compiled. The interpreter processes and executes the code directly, translating it into machine code for execution.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "bc5650447d001e2d", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "Is Python interpreted or compiled?", "text": "Python is interpreted, not compiled. The interpreter runs the code directly, while the compiler compiles the code into machine code for execution.", "retry_text": "Python is interpreted, not compiled. The interpreter runs the code directly, while the compiler compiles the code into machine code for execution.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "6e6494aead8f1636", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "What colour do you get mixing blue and yellow?", "text": "When you mix blue and yellow, you get green. This is because the primary colors in the traditional color wheel are red, blue, and yellow. Mixing two primary colors results in the third color in the spectrum.", "retry_text": "When you mix blue and yellow, you get green. This is because the primary colors in the traditional color wheel are red, blue, and yellow. Mixing two primary colors results in the third color in the spectrum.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "5a39099b612f2c75", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "What colour do you get mixing blue and yellow?", "text": "When you mix blue and yellow, the resulting color is a complementary color pair. Blue and yellow are on the color wheel, and combining them produces a color that is not directly visible but complementary. The exact shade can vary depending on the mixing method (e.g., using a palette knife vs. a flat surface), but the result is typically a shade of blue or yellow in the opposite direction.", "retry_text": "When mixing blue and yellow, the resulting color is a complementary color pair. Blue and yellow are on the color wheel, and combining them produces a color that is not directly visible but complementary. The exact shade can vary depending on the mixing method, but the result is typically a shade of blue or yellow in the opposite direction.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "32dc34da4d27b3c4", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "What colour do you get mixing blue and yellow?", "text": "The mixing of blue and yellow results in yellow. This is a basic color theory fact.", "retry_text": "The mixing of blue and yellow results in yellow. This is a basic color theory fact.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "67a73b74efc2bbd5", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "What colour do you get mixing blue and yellow?", "text": "The color you get when mixing blue and yellow is yellow. This is because blue and yellow are complementary colors in the visible spectrum, and their combination results in the color yellow.", "retry_text": "The color you get when mixing blue and yellow is yellow.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "5643f5575982ee67", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "What colour do you get mixing blue and yellow?", "text": "The correct answer is that mixing blue and yellow results in **orange**. This is based on the traditional color mixing principles of additive color theory.", "retry_text": "The correct answer is that mixing blue and yellow results in **orange**. This is based on the traditional color mixing principles of additive color theory.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "04784cbe5123000d", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "What colour do you get mixing blue and yellow?", "text": "When mixing blue and yellow, the result is white. This is based on the principles of color theory, where the combination of primary colors (blue, yellow, and red) results in white. The exact color outcome depends on the intensity of the colors used, but typically, mixing equal parts blue and yellow yields the characteristic white.", "retry_text": "The color obtained when mixing blue and yellow is white. This is based on the principles of color theory, where primary colors (blue, yellow, and red) combine to produce the complementary color white.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "a25aab4f1135fbba", "model": "qwen3-0.6b", "prompt_class": "no_tool", "prompt": "What colour do you get mixing blue and yellow?", "text": "When you mix blue and yellow, the resulting color is **yellow**. This is because the primary colors (red, blue, and yellow) combine to produce secondary colors when mixed. Mixing two primary colors results in the secondary color.", "retry_text": "The color when mixing blue and yellow is **yellow**. This is because the primary colors (red, blue, and yellow) combine to produce secondary colors when mixed.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "5bdabb7899cfd8f8", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "Give me a numbered list of steps to fine tune a model.", "text": "Here’s a numbered list of steps to fine tune a model:\n\n1. **Data Preparation**: Collect and preprocess the dataset to ensure it’s suitable for the model. This includes handling missing data, scaling features, and cleaning the data.\n\n2. **Model Selection**: Choose an appropriate model (e.g., transformer, CNN, or RNN) that is suitable for the task and aligns with the data characteristics.\n\n3. **Hyperparameter Tuning**: Optimize the model’s hyperparameters (e.g., learning rate, batch size, regularization) to improve performance and convergence.\n\n4. **Initial Training**: Perform a full model training on the dataset to fine-tune the model to the task.\n\n5. **Fine-Tuning**: Adjust the model parameters based on the performance of the initial training phase to improve accuracy.\n\n6. **Evaluation**: Assess the model’s performance using appropriate metrics and compare it with the original model.\n\n7. **Deployment**: Once the model is optimized, deploy it for prediction or inference tasks.\n\nLet me know if you need further clarification!", "retry_text": "Here’s a numbered list of steps to fine tune a model:\n\n1. **Data Preparation** \n2. **Model Selection** \n3. **Hyperparameter Tuning** \n4. **Initial Training** \n5. **Fine-Tuning** \n6. **Evaluation** \n7. **Deployment** \n\nLet me know if you need further clarification!", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "20f4b59d62954aa6", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "Give me a numbered list of steps to fine tune a model.", "text": "Here’s a numbered list of steps to fine-tune a model:\n\n1. **Pre-training on a large dataset** \n Train the model on a large, diverse dataset to obtain a strong initial representation of the task.\n\n2. **Fine-tuning on the target dataset** \n Adjust the model on a smaller, specific dataset to fine-tune its parameters for the task at hand.\n\n3. **Validation during training** \n Monitor the model’s performance using a validation set or metric to ensure it generalizes well and doesn’t overfit.\n\n4. **Hyperparameter tuning** \n Optimize the model’s hyperparameters (e.g., learning rate, batch size, regularization) to improve efficiency and performance.\n\n5. **Using techniques like data augmentation** \n Augment the training data during fine-tuning to increase the model’s robustness and reduce overfitting.", "retry_text": "Here’s a numbered list of steps to fine tune a model:\n\n1. **Pre-training on a large dataset** \n Train the model on a large, diverse dataset to obtain a strong initial representation of the task. \n\n2. **Fine-tuning on the target dataset** \n Adjust the model on a smaller, specific dataset to fine-tune its parameters for the task at hand. \n\n3. **Validation during training** \n Monitor the model’s performance using a validation set or metric to ensure it generalizes well and doesn’t overfit. \n\n4. **Hyperparameter tuning** \n Optimize the model’s hyperparameters (e.g., learning rate, batch size, regularization) to improve efficiency and performance. \n\n5. **Using techniques like data augmentation** \n Augment the training data during fine-tuning to increase the model’s robustness and reduce overfitting.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "51864a903d9fb2bd", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "Give me a numbered list of steps to fine tune a model.", "text": "Here is a numbered list of steps to fine-tune a model:\n\n1. **Initial Model Selection**: Choose the appropriate model architecture that is suitable for the task and has a good performance baseline. \n2. **Data Collection**: Gather and preprocess the training data to ensure it is suitable for the model. \n3. **Model Initialization**: Start by initializing the model with the provided parameters. \n4. **Training Phase**: Train the model on the dataset to improve its performance. Adjust the number of parameters if needed. \n5. **Hyperparameter Tuning**: Conduct experiments to optimize hyperparameters (e.g., learning rate, batch size) to enhance model efficiency. \n6. **Validation Phase**: Perform validation to check for overfitting and ensure the model generalizes well. \n7. **Final Fine-tuning**: Fine-tune the model using additional data or in a new dataset to improve its performance on the target task. \n\nLet me know if you need further clarification!", "retry_text": "Here is a numbered list of steps to fine tune a model:\n\n1. **Initial Model Selection**: Choose the appropriate model architecture that is suitable for the task and has a good performance baseline. \n2. **Data Collection**: Gather and preprocess the training data to ensure it is suitable for the model. \n3. **Model Initialization**: Start by initializing the model with the provided parameters. \n4. **Training Phase**: Train the model on the dataset to improve its performance. Adjust the number of parameters if needed. \n5. **Hyperparameter Tuning**: Conduct experiments to optimize hyperparameters (e.g., learning rate, batch size) to enhance model efficiency. \n6. **Validation Phase**: Perform validation to check for overfitting and ensure the model generalizes well. \n7. **Final Fine-tuning**: Fine-tune the model using additional data or in a new dataset to improve its performance on the target task.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "a452cbc60878427b", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "Give me a numbered list of steps to fine tune a model.", "text": "Here is a numbered list of steps to fine tune a model:\n\n1. **Data Preparation**: \n - Clean and prepare the dataset for fine-tuning. \n - Handle missing values and imbalance in the data. \n\n2. **Model Selection**: \n - Choose a suitable model architecture (e.g., Transformer, CNN, or RNN) based on the task. \n\n3. **Hyperparameters Tuning**: \n - Optimize model hyperparameters (e.g., learning rate, batch size, number of epochs) to balance training efficiency and model performance. \n\n4. **Validation and Testing**: \n - Validate the model using a separate validation set to ensure it generalizes well. \n\n5. **Fine-tuning Execution**: \n - Adjust the model parameters (e.g., weights, biases) to improve performance on the target dataset. \n\n6. **Deployment**: \n - After fine-tuning, deploy the optimized model for real-world use.", "retry_text": "Here is a numbered list of steps to fine tune a model:\n\n1. **Data Preparation** \n - Clean and prepare the dataset for fine-tuning. \n - Handle missing values and imbalance in the data. \n\n2. **Model Selection** \n - Choose a suitable model architecture (e.g., Transformer, CNN, or RNN) based on the task. \n\n3. **Hyperparameters Tuning** \n - Optimize model hyperparameters (e.g., learning rate, batch size, number of epochs) to balance training efficiency and model performance. \n\n4. **Validation and Testing** \n - Validate the model using a separate validation set to ensure it generalizes well. \n\n5. **Fine-Tuning Execution** \n - Adjust the model parameters (e.g., weights, biases) to improve performance on the target dataset. \n\n6. **Deployment** \n - After fine-tuning, deploy the optimized model for real-world use.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "6d58c791b1b4028a", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "Give me a numbered list of steps to fine tune a model.", "text": "1. Load the pre-trained model. \n2. Verify the model’s readiness for fine-tuning. \n3. Prepare and clean the training data. \n4. Perform the fine-tuning phase using the original dataset. \n5. Evaluate the model’s performance after fine-tuning.", "retry_text": "1. Load the pre-trained model. \n2. Verify the model’s readiness for fine-tuning. \n3. Prepare and clean the training data. \n4. Perform the fine-tuning phase using the original dataset. \n5. Evaluate the model’s performance after fine-tuning.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "3f4913c6bbd0921f", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "Give me a numbered list of steps to fine tune a model.", "text": "Here's a numbered list of steps to fine-tune a model:\n\n1. **Pre-Train the Model**: Select a pre-trained model architecture (e.g., BERT, GPT) and fine-tune it on a new dataset to adapt it to the specific task.\n\n2. **Data Preparation**: Gather and preprocess the dataset for the task, ensuring it matches the model's input requirements.\n\n3. **Model Selection**: Choose a suitable model architecture that can effectively adapt to the task and is compatible with the training environment.\n\n4. **Training the Model**: Train the pre-trained model on the new data to maximize its performance and parameter optimization.\n\n5. **Fine-Tuning**: Adjust the model's parameters based on the new data to improve its accuracy and efficiency.\n\n6. **Evaluation**: After fine-tuning, evaluate the model's performance using appropriate metrics to ensure it meets the desired results.", "retry_text": "Here's a numbered list of steps to fine tune a model:\n\n1. **Pre-Train the Model**: Select a pre-trained model architecture (e.g., BERT, GPT) and fine-tune it on a new dataset to adapt it to the specific task.\n\n2. **Data Preparation**: Gather and preprocess the dataset for the task, ensuring it matches the model's input requirements.\n\n3. **Model Selection**: Choose a suitable model architecture that can effectively adapt to the task and is compatible with the training environment.\n\n4. **Training the Model**: Train the pre-trained model on the new data to maximize its performance and parameter optimization.\n\n5. **Fine-Tuning**: Adjust the model's parameters based on the new data to improve its accuracy and efficiency.\n\n6. **Evaluation**: After fine-tuning, evaluate the model's performance using appropriate metrics to ensure it meets the desired results.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "fc1e25c5d4a98acd", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "Outline, as a bulleted list, how to debug a memory leak.", "text": "- **Identify the source of the leak** by tracing where the memory is being allocated or released. \n- **Check memory usage** using tools like Valgrind or heap analysis tools to detect unallocated memory. \n- **Monitor memory consumption** with tools like `gmon -p` or `gmon -p -m` to track heap usage. \n- **Analyze heap dumps** to identify objects that are not being freed. \n- **Use the garbage collector** (GC) to determine if it's catching the leak. \n- **Test with different environments** to validate the cause. \n- **Use Valgrind** to pinpoint the exact location of the leak.", "retry_text": "- **Identify the source of the leak** by tracing where memory is allocated or released. \n- **Check memory usage** using tools like Valgrind or heap analysis to detect unallocated memory. \n- **Monitor memory consumption** with tools like `gmon -p` or `gmon -p -m` to track heap usage. \n- **Analyze heap dumps** to identify objects that are not being freed. \n- **Use the garbage collector** (GC) to determine if it's catching the leak. \n- **Test with different environments** to validate the cause. \n- **Use Valgrind** to pinpoint the exact location of the leak.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "66a9eeebd5692881", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "Outline, as a bulleted list, how to debug a memory leak.", "text": "- **Check for memory leaks in code** (e.g., using tools like Valgrind or memory dumps). \n- **Track allocated memory** (e.g., using `valgrind` or `gmonloc` to identify allocation/deallocation points). \n- **Locate memory allocation and deallocation points** in the code or runtime environment. \n- **Analyze heap and stack usage** to identify where the leak occurs. \n- **Use Valgrind or other memory analysis tools** to report and resolve leaks.", "retry_text": "- **Check for memory leaks using Valgrind** (e.g., by running `valgrind --leak-test-cmd` or analyzing memory dumps). \n- **Identify allocation/deallocation points** via tools like `gmonloc` or Valgrind's memory analysis tools. \n- **Track allocated memory** using `valgrind` or heap tracing to pinpoint where leaks occur. \n- **Analyze heap and stack usage** to determine the source of the memory issue. \n- **Use Valgrind to report and resolve leaks**, ensuring proper cleanup and memory management.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "afac0fda9391e9b9", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "Outline, as a bulleted list, how to debug a memory leak.", "text": "- **Identify the source of the memory leak** by inspecting the program's code for memory allocations and deallocations. \n- **Use tools like Valgrind or LeakCheck** to detect leaks in the application's memory usage. \n- **Track memory usage** by writing code that monitors allocations and deallocations. \n- **Analyze memory allocation and deallocation points** to pinpoint where the leak occurs. \n- **Check for unused variables or improper memory management** in the code. \n- **Validate the solution** after fixing the leak to ensure no new leaks are introduced.", "retry_text": "- **Identify the source of the memory leak** by inspecting the program's code for memory allocations and deallocations. \n- **Use tools like Valgrind or LeakCheck** to detect leaks in the application's memory usage. \n- **Track memory usage** by writing code that monitors allocations and deallocations. \n- **Analyze memory allocation and deallocation points** to pinpoint where the leak occurs. \n- **Check for unused variables or improper memory management** in the code. \n- **Validate the solution** after fixing the leak to ensure no new leaks are introduced.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "1a1a92f274563204", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "Outline, as a bulleted list, how to debug a memory leak.", "text": "- **Identify the source of the leak**: Use tools like `valgrind` to detect memory leaks by analyzing memory usage. \n- **Check allocated objects**: Look for memory allocations and ensure they are freed properly. \n- **Use `valgrind` to detect leaks**: Run commands like `valgrind --leak-check=static` or `valgrind --leak-check=full` to find memory leaks. \n- **Analyze memory usage**: Tools like `gmem` or `gprof` can help trace memory allocation and deallocation. \n- **Use a debugger**: Set breakpoints in the code to inspect memory usage and identify leaks. \n- **Implement memory management strategies**: Use smart pointers or proper deallocation in C/C++ to prevent leaks. \n- **Review and verify code for leaks**: Ensure all memory allocations and frees are handled correctly.", "retry_text": "- **Identify the source of the leak**: Use tools like `valgrind` to detect memory leaks by analyzing memory usage. \n- **Check allocated objects**: Ensure memory allocations and deallocations are handled properly. \n- **Use `valgrind` to detect leaks**: Run commands like `valgrind --leak-check=static` or `valgrind --leak-check=full` to find memory leaks. \n- **Analyze memory usage**: Tools like `gmem` or `gprof` can help trace memory allocation and deallocation. \n- **Use a debugger**: Set breakpoints in the code to inspect memory usage and identify leaks. \n- **Implement memory management strategies**: Use smart pointers or proper deallocation in C/C++ to prevent leaks. \n- **Review and verify code for leaks**: Ensure all memory allocations and frees are handled correctly.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "d052ea50b3e08f1c", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "Outline, as a bulleted list, how to debug a memory leak.", "text": "- **Identify the leak** by checking memory allocations and observing where the leak starts and ends. \n- **Analyze the heap dump** to pinpoint the source of the leak. \n- **Use tools like Valgrind or LeakCheck** to trace memory allocations and identify leaks. \n- **Check if memory is being freed incorrectly** (e.g., not marking it as free). \n- **Modify the code** to prevent memory leaks by ensuring proper allocation and freeing of memory. \n- **Test the fix** by running the application and verifying that the leak is resolved.", "retry_text": "- **Identify the leak** by checking memory allocations and observing where the leak starts and ends. \n- **Analyze the heap dump** to pinpoint the source of the leak. \n- **Use tools like Valgrind or LeakCheck** to trace memory allocations and identify leaks. \n- **Check if memory is being freed incorrectly** (e.g., not marking it as free). \n- **Modify the code** to prevent memory leaks by ensuring proper allocation and freeing of memory. \n- **Test the fix** by running the application and verifying that the leak is resolved.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "ad9fb6b1ed4c0158", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "Outline, as a bulleted list, how to debug a memory leak.", "text": "- **Identify the leak**: Use tools like Valgrind, gdb, or memory analysis tools to locate where memory is allocated and not freed. \n- **Check allocation/deallocation points**: Trace program execution to ensure memory is properly allocated and released. \n- **Verify memory usage**: Confirm that all memory allocations and frees are correctly managed. \n- **Ensure proper memory management**: Avoid dangling pointers and ensure all memory is released before the program exits.", "retry_text": "- **Identify the leak**: Use tools like Valgrind, gdb, or memory analysis tools to locate where memory is allocated and not freed. \n- **Check allocation/deallocation points**: Trace program execution to ensure memory is properly allocated and released. \n- **Verify memory usage**: Confirm that all memory allocations and frees are correctly managed. \n- **Ensure proper memory management**: Avoid dangling pointers and ensure all memory is released before the program exits.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "608c5d437fef5603", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "Outline, as a bulleted list, how to debug a memory leak.", "text": "- **Identify the leak**: Use tools like Valgrind, Leakcheck, or your application's built-in memory analysis tools to detect memory leaks by checking heap usage or logs. \n- **Analyze the problem**: Trace the code to identify where memory was allocated and released, and determine why a memory block was not freed. \n- **Debug the code**: Modify the code to prevent memory leaks, such as updating allocation and release functions or ensuring proper memory management. \n- **Test the application**: Re-run the application after fixes to ensure the leak is resolved and no other issues persist.", "retry_text": "- **Identify the leak**: Use tools like Valgrind, Leakcheck, or your application's built-in memory analysis tools to detect memory leaks by checking heap usage or logs. \n- **Analyze the problem**: Trace the code to identify where memory was allocated and released, and determine why a memory block was not freed. \n- **Debug the code**: Modify the code to prevent memory leaks, such as updating allocation and release functions or ensuring proper memory management. \n- **Test the application**: Re-run the application after fixes to ensure the leak is resolved and no other issues persist.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "737a72c4c49ec6d6", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "Outline, as a bulleted list, how to debug a memory leak.", "text": "- **Identify the Leak**: Use tools like `valgrind` or `gmem` to pinpoint where memory is being used incorrectly. \n- **Check Heap Usage**: Ensure all memory allocations and frees are properly tracked using the `heapq` or `heapq.heappush` module. \n- **Review Libraries**: Check if a library or framework (e.g., `heapq`, `heapq.heappush`) is not properly managing memory. \n- **Use Profiling Tools**: Tools like `valgrind` or GDB can help track memory allocation and usage patterns. \n- **Analyze Heap Usage**: Ensure heap memory is not exhausted by monitoring allocations and frees. \n- **Check for Proper Freeing**: Verify that every memory allocation is correctly freed using the appropriate `free` or `delete` functions. \n- **Use Debugging Tools**: Tools like GDB or `gmem` can help trace memory usage in the context of the program. \n- **Review Logs**: Look for logs related to memory allocation and usage to identify potential issues.", "retry_text": "- **Identify the Leak**: Use tools like `valgrind` or `gmem` to pinpoint where memory is being used incorrectly. \n- **Check Heap Usage**: Ensure all memory allocations and frees are properly tracked using the `heapq` or `heapq.heappush` module. \n- **Review Libraries**: Check if a library or framework (e.g., `heapq`, `heapq.heappush`) is not properly managing memory. \n- **Use Profiling Tools**: Tools like `valgrind` or GDB can help track memory allocation and usage patterns. \n- **Analyze Heap Usage**: Ensure heap memory is not exhausted by monitoring allocations and frees. \n- **Check for Proper Freeing**: Verify that every memory allocation is correctly freed using the appropriate `free` or `delete` functions. \n- **Use Debugging Tools**: Tools like GDB or `gmem` can help trace memory usage in the context of the program. \n- **Review Logs**: Look for logs related to memory allocation and usage to identify potential issues.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "ca9f375a692cb8e9", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "List the first three things you would check if training loss is NaN.", "text": "If training loss is NaN, you should check the following three things:\n\n1. **Data Validation**: Ensure the input data is properly formatted and contains valid values. Missing or incomplete data might cause the loss function to fail. \n2. **Model Initialization**: Verify that the model has been initialized correctly, including parameters and initializations. A uninitialized model might not learn effectively. \n3. **Training Progress**: Confirm that the training process has started and that the model has been updated properly, even if the loss is NaN at intermediate steps.", "retry_text": "The first three things to check if training loss is NaN are:\n\n1. **Data Validation**: Ensure the input data is properly formatted and contains valid values. Missing or incomplete data might cause the loss function to fail. \n2. **Model Initialization**: Verify that the model has been initialized correctly, including parameters and initializations. A uninitialized model might not learn effectively. \n3. **Training Progress**: Confirm that the training process has started and that the model has been updated properly, even if the loss is NaN at intermediate steps.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "df064c12eb1268c4", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "List the first three things you would check if training loss is NaN.", "text": "If training loss is NaN, you should check the following three things:\n\n1. **Missing Values**: Check for any missing entries in your training data that might affect the loss function calculation.\n2. **Non-Numeric Values**: Ensure that the training data contains only numeric values, as non-numeric values (e.g., strings or special characters) can cause the loss to be undefined.\n3. **Invalid Inputs**: Verify that there are no invalid inputs during training, such as division by zero or malformed data entries.", "retry_text": "If training loss is NaN, you should check the following three things:\n\n1. **Missing Values**: Ensure the training data contains no missing entries that could affect the loss function calculation. \n2. **Non-Numeric Values**: Verify that the training data contains only numeric values (e.g., integers, floats) instead of strings or special characters. \n3. **Invalid Inputs**: Check for invalid inputs like division by zero, malformed data entries, or errors in the training process.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "83bd51ee6dbacc14", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "List the first three things you would check if training loss is NaN.", "text": "If training loss is NaN, the first three things to check are: \n1. **Check for division by zero**: If the loss calculation involves division by zero (e.g., `loss = 1 / 0`), this could cause a NaN. \n2. **Verify model updates**: Ensure the model has been trained with enough parameters and the optimizer has optimized the loss function. \n3. **Confirm dataset completeness**: Ensure the dataset is fully available and does not contain missing or incomplete data.", "retry_text": "If training loss is NaN, the first three things to check are: \n1. **Check for division by zero** (e.g., `loss = 1 / 0`). \n2. **Verify model updates** (ensure the model has been trained with enough parameters and the optimizer has optimized the loss function). \n3. **Confirm dataset completeness** (ensure the dataset is fully available and contains no missing or incomplete data).", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "e344d44a16689e8e", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "List the first three things you would check if training loss is NaN.", "text": "If training loss is NaN, the first three things to check are: \n1. **Data Availability**: Ensure the training data is properly available and formatted. \n2. **Optimizer Initialization**: Verify that the optimizer (e.g., Adam, SGD) is initialized correctly. \n3. **Training Process**: Confirm that the training process has been executed for a sufficient number of epochs to allow convergence.", "retry_text": "If training loss is NaN, the first three things to check are: \n1. **Data Availability**: Ensure the training data is properly available and formatted. \n2. **Optimizer Initialization**: Verify that the optimizer (e.g., Adam, SGD) is initialized correctly. \n3. **Training Process**: Confirm that the training process has been executed for a sufficient number of epochs to allow convergence.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "a88294870a2c9f99", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "List the first three things you would check if training loss is NaN.", "text": "If training loss is NaN, the first three things to check are:\n\n1. **Check for division by zero** (e.g., dividing a number by zero in training loss calculations). \n2. **Verify data type** (e.g., ensuring the input data type matches expected values). \n3. **Check for missing or incomplete data** (e.g., missing training samples or values in the dataset).", "retry_text": "If training loss is NaN, the first three things to check are:\n\n1. **Check for division by zero** (e.g., dividing a number by zero in training loss calculations). \n2. **Verify data type** (e.g., ensuring the input data type matches expected values). \n3. **Check for missing or incomplete data** (e.g., missing training samples or values in the dataset).", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "fac14512075ca35c", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "List the first three things you would check if training loss is NaN.", "text": "If training loss is NaN, you should check the following three things:\n\n1. **Missing Values in the Training Data**: Ensure there are no missing entries in the training data that could cause issues with the loss function.\n2. **Model Configuration**: Verify that the model architecture is correctly defined and the weights and biases are initialized appropriately.\n3. **Optimizer Performance**: Check if the optimizer (e.g., Adam, SGD) is converging properly, which might lead to NaN values if it's not optimized effectively.", "retry_text": "List the first three things you would check if training loss is NaN:\n\n1. **Missing Values in the Training Data**: Ensure there are no missing entries in the training data that could cause issues with the loss function. \n2. **Model Configuration**: Verify that the model architecture is correctly defined and the weights and biases are initialized appropriately. \n3. **Optimizer Performance**: Check if the optimizer (e.g., Adam, SGD) is converging properly, which might lead to NaN values if it's not optimized effectively.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "3f23520b6e2895bd", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "List the first three things you would check if training loss is NaN.", "text": "If training loss is NaN, you should check the following:\n\n1. **Incomplete or Missing Training Data**: The training data may be incomplete or lack the necessary inputs for the model to compute the loss.\n2. **Incorrect Learning Rate**: A learning rate that is too large or too small could cause the loss to become undefined.\n3. **Optimizer Parameters**: The optimizer (e.g., Adam, RMSProp) may not be properly initialized or configured, leading to NaN values in the loss.", "retry_text": "Here are the first three things to check if training loss is NaN:\n\n1. **Incomplete or Missing Training Data**: The training data may be missing essential inputs required for the model to compute the loss. \n2. **Incorrect Learning Rate**: A learning rate that is too large or too small could cause the loss to become undefined. \n3. **Optimizer Parameters**: The optimizer (e.g., Adam, RMSProp) may not be properly initialized or configured, leading to NaN values in the loss.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "ec5763efa2dbc665", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "List the first three things you would check if training loss is NaN.", "text": "If training loss is NaN, consider the following first three checks:\n\n1. **Check the loss value is a valid number** (e.g., not a NaN or invalid value). \n2. **Verify the loss is being computed correctly** (e.g., inputs are properly fed, outputs are not empty or invalid). \n3. **Confirm the model is using a valid loss function** (e.g., not a numeric type or unsupported function).", "retry_text": "If training loss is NaN, consider the following first three checks:\n\n1. **Check the loss value is a valid number** (e.g., not a NaN or invalid value). \n2. **Verify the loss is being computed correctly** (e.g., inputs are properly fed, outputs are not empty or invalid). \n3. **Confirm the model is using a valid loss function** (e.g., not a numeric type or unsupported function).", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "e7c0bb256910c0a1", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "Walk me through setting up a Python virtualenv, step by step.", "text": "To set up a Python virtual environment step-by-step, follow these instructions:\n\n1. **Install Python**: \n Ensure you have Python installed. You can check this by typing `python -V` or `python3 -V` in your terminal. If not installed, download it from [https://www.python.org/downloads](https://www.python.org/downloads).\n\n2. **Create a Virtual Environment**: \n Use the command: \n ```bash\n python -m venv \n ``` \n Replace `` with a name (e.g., `my_venv`). This creates a directory where your Python environment is stored. If the directory doesn't exist, the command will create it.\n\n3. **Activate the Virtual Environment**: \n - **Using the shell**: \n ```bash\n source /bin/activate\n ``` \n This activates the environment and uses `pip` commands for package management. \n - **Using the terminal**: \n ```bash\n activate \n ``` \n This also activates the environment.\n\n4. **Verify the Environment**: \n After activation, check if the environment is active by running: \n ```bash\n pip --version\n ``` \n This confirms that the virtual environment is correctly set up.\n\n5. **Using the Environment**: \n Once the virtual environment is active, you can install packages using `pip install` or run scripts in the same directory.\n\n**Note**: If you want to use a custom path, specify it in the `venv` command. Also, if you need to create a new virtualenv, ensure that the directory exists before running the command.", "retry_text": "To set up a Python virtual environment, follow these steps:\n\n1. **Install Python** \n Ensure you have Python installed. You can check this by typing `python -V` or `python3 -V` in your terminal. If not installed, download it from [https://www.python.org/downloads](https://www.python.org/downloads).\n\n2. **Create a Virtual Environment** \n Use the command: \n ```bash\n python -m venv \n ``` \n Replace `` with a name (e.g., `my_venv`). This creates a directory where your Python environment is stored. If the directory doesn't exist, the command will create it.\n\n3. **Activate the Virtual Environment** \n - **Using the shell**: \n ```bash\n source /bin/activate\n ``` \n This activates the environment and uses `pip` commands for package management. \n - **Using the terminal**: \n ```bash\n activate \n ``` \n This also activates the environment.\n\n4. **Verify the Environment** \n After activation, check if the environment is active by running: \n ```bash\n pip --version\n ``` \n This confirms that the virtual environment is correctly set up.\n\n5. **Using the Environment** \n Once the virtual environment is active, you can install packages using `pip install` or run scripts in the same directory.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "6a58c90d0c6b8c6a", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "Walk me through setting up a Python virtualenv, step by step.", "text": "To set up a Python virtualenv step by step, follow this process:\n\n1. **Create the Virtualenv**: \n Use the command `python -m venv ` to create a new virtual environment. For example: \n ```bash \n python -m venv myvenv \n ``` \n This creates a folder named `myvenv` with the Python interpreter and packages for your project.\n\n2. **Activate the Virtualenv**: \n After creating the environment, activate it using: \n ```bash \n source myvenv/bin/activate \n ``` \n This makes the virtualenv available for executing code within the current working directory.\n\n3. **Install Dependencies**: \n If your project has dependencies, use `pip install` within the virtualenv: \n ```bash \n pip install -r requirements.txt \n ``` \n This installs all necessary packages for your project.\n\n4. **Use the Virtualenv**: \n Now, you can run code directly in the virtualenv: \n ```bash \n python your_script.py \n ``` \n This avoids conflicts with other Python environments.\n\n5. **Verify**: \n Check the path to ensure the virtualenv is active: \n ```bash \n python -c \"import sys; print(sys.path)\" \n ``` \n This will show where the virtualenv's packages are available.\n\nThis process ensures isolated, secure environments for project dependencies.", "retry_text": "To set up a Python virtualenv step by step:\n\n1. **Create the Virtualenv**: \n Use the command `python -m venv ` to create a new virtual environment. For example: \n ```bash \n python -m venv myvenv \n ``` \n\n2. **Activate the Virtualenv**: \n After creating the environment, activate it using: \n ```bash \n source myvenv/bin/activate \n ``` \n\n3. **Install Dependencies**: \n If your project has dependencies, use `pip install` within the virtualenv: \n ```bash \n pip install -r requirements.txt \n ``` \n\n4. **Use the Virtualenv**: \n Now, you can run code directly in the virtualenv: \n ```bash \n python your_script.py \n ``` \n\n5. **Verify**: \n Check the path to ensure the virtualenv is active: \n ```bash \n python -c \"import sys; print(sys.path)\" \n ``` \n\nThis process ensures isolated, secure environments for project dependencies.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "6012f002d10c0090", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "Walk me through setting up a Python virtualenv, step by step.", "text": "To set up a Python virtualenv step by step, follow this process:\n\n1. **Install Python**: \n If not already installed, install Python using your system's package manager (e.g., `apt-get` on Ubuntu/Debian or `yum` on CentOS). Example command: \n ```bash\n # For Ubuntu/Debian\n sudo apt-get update\n sudo apt-get install -y python3\n # For CentOS\n sudo yum install -y python3\n ```\n\n2. **Create a Virtual Environment**: \n Use `venv` or `virtualenv` to create a new environment. The command is: \n ```bash\n python3 -m venv my_venv\n ```\n\n This creates a directory named `my_venv` with Python and dependencies. Ensure the environment is activated. \n **Activate the virtualenv** (change directory to the new environment): \n ```bash\n source my_venv/bin/activate\n ```\n\n3. **Install Packages**: \n If your project requires specific packages, install them using `pip install package_name`. For example: \n ```bash\n pip install requirements.txt\n ```\n\n4. **Use the Virtualenv**: \n Now, you can use the virtualenv to install packages and run scripts without conflicts with your main Python environment.\n\n**Example**: \n```bash\n# Create a virtualenv\npython3 -m venv my_venv\n\n# Activate the virtualenv\nsource my_venv/bin/activate\n\n# Install dependencies\npip install some_package\n```\n\nThis setup allows you to manage multiple Python environments easily.", "retry_text": "Here's a step-by-step walkthrough of setting up a Python virtualenv:\n\n1. **Install Python**: \n If not already installed, install Python using your system's package manager. For example: \n ```bash\n # For Ubuntu/Debian\n sudo apt-get update\n sudo apt-get install -y python3\n # For CentOS\n sudo yum install -y python3\n ```\n\n2. **Create a Virtual Environment**: \n Use `venv` or `virtualenv` to create a new environment. The command is: \n ```bash\n python3 -m venv my_venv\n ```\n\n This creates a directory named `my_venv` with Python and dependencies. Ensure the environment is activated. \n **Activate the virtualenv**: \n ```bash\n source my_venv/bin/activate\n ```\n\n3. **Install Packages**: \n If your project requires specific packages, install them using `pip install package_name`. For example: \n ```bash\n pip install requirements.txt\n ```\n\n4. **Use the Virtualenv**: \n Now, you can use the virtualenv to install packages and run scripts without conflicts with your main Python environment.\n\n**Example**: \n```bash\n# Create a virtualenv\npython3 -m venv my_venv\n\n# Activate the virtualenv\nsource my_venv/bin/activate\n\n# Install dependencies\npip install some_package\n```\n\nThis setup allows you to manage multiple Python environments easily.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "4f318c8ba1ed6f94", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "What is your plan for answering a question you are unsure about?", "text": "When unsure about a question, I plan to first search the web for the information using the web_search function. If the query is resolved through online resources, I'll proceed with that. If not, I'll use Python code execution to address any specific tasks or data analysis required. I'm also prepared to adjust my plan based on the results found.", "retry_text": "When unsure about a question, I plan to first search the web using the `web_search` function. If the information is resolved through online resources, I'll proceed with that. If not, I'll use Python code execution to address any specific tasks or data analysis required. I'm also prepared to adjust my plan based on the results found.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "095bb6719849ccf9", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "What is your plan for answering a question you are unsure about?", "text": "When unsure about a question, my approach is to use the tools available, such as web searches or sandbox code execution. I'll ask for clarification or provide relevant information based on the tools. If I need assistance with specific tasks, I'll use the tools provided to get the required information.", "retry_text": "When unsure about a question, my approach is to use tools like web searches or sandbox code execution to gather information. I will ask for clarification or provide relevant details. If no tools are needed, I will directly answer the question as requested.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "6466d33bf4055aba", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "What is your plan for answering a question you are unsure about?", "text": "When unsure about a question, my plan is to: \n1. **Research the topic** to gather reliable information. \n2. **Analyze the information** to determine the most accurate answer. \n3. **Provide a well-structured response** that addresses the question while being helpful and comprehensive. \n\nThis approach ensures clarity and addresses the user’s needs effectively.", "retry_text": "When unsure about a question, my plan is: \n1. **Research** to gather reliable information. \n2. **Analyze** the information to determine the most accurate answer. \n3. **Provide a well-structured response** that addresses the question while being helpful and comprehensive. \n\nThis approach ensures clarity and addresses the user’s needs effectively.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "f571eddf37233a27", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "What is your plan for answering a question you are unsure about?", "text": "When unsure about a question, my plan is to first use the web_search function to gather information if the question involves specific content or needs external data. Otherwise, I rely on my existing knowledge and domain expertise. I remain flexible and adapt my responses based on the context and the nature of the question.", "retry_text": "I don't need a tool to answer this question. When unsure about a question, my plan is to use the web_search function to gather information if the question requires external data or specific content, or rely on my existing knowledge and domain expertise otherwise. I remain flexible and adapt my responses based on the context and the nature of the question.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "287075bcccadaa02", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "What is your plan for answering a question you are unsure about?", "text": "When unsure about a question, my plan is to: \n1. **Verify facts** by cross-referencing information from reliable sources. \n2. **Stay informed** by staying updated on the topic to ensure accuracy. \n3. **Consider different perspectives** to find the most comprehensive answer. \n\nI am always open to learning and adapting my approach to provide the best response.", "retry_text": "When unsure about a question, my plan is to: \n1. **Verify facts** by cross-referencing information from reliable sources. \n2. **Stay informed** by staying updated on the topic to ensure accuracy. \n3. **Consider different perspectives** to find the most comprehensive answer. \n\nI am always open to learning and adapting my approach to provide the best response.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "9474398e6db4735a", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "What is your plan for answering a question you are unsure about?", "text": "When unsure about a question, my plan is to: \n1. **Research thoroughly** using reliable sources. \n2. **Ask clarifying questions** to deepen understanding. \n3. **Analyze the context** and consider different perspectives. \n4. **Stay open to new information** and adapt based on available resources. \n\nLet me know if you'd like to refine this approach!", "retry_text": "I don't need a tool to answer this question. When unsure, my plan is to: \n1. **Research thoroughly** using reliable sources. \n2. **Ask clarifying questions** to deepen understanding. \n3. **Analyze the context** and consider different perspectives. \n4. **Stay open to new information** and adapt based on available resources. \n\nLet me know if you'd like to refine this approach!", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "16abe1d67280a8e4", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "What is your plan for answering a question you are unsure about?", "text": "When unsure about a question, I rely on available information and use the web_search function to gather details. I ensure thoroughness by checking the information thoroughly. If you have more context or specific details about the question, I can tailor my plan accordingly. Let me know!", "retry_text": "When unsure about a question, I rely on available information and use the web_search function to gather details. I ensure thoroughness by checking thoroughly. If you have more context or specific details about the question, I can tailor my plan accordingly. Let me know!", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "d851acb957839ac2", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "Describe your approach to reviewing a pull request, as a list.", "text": "Here’s a structured approach to reviewing a pull request:\n\n- **Code Review for Correctness and Style**: \n - Ensure code is logically sound and follows project standards. \n - Check for syntax errors, logic flaws, and code duplication. \n\n- **Identify Issues and Bug Fixes**: \n - Locate and resolve any bugs, edge cases, or issues in the code. \n\n- **Ensure Maintainability**: \n - Verify that the code is easy to understand, testable, and scalable for future updates. \n\n- **Testing**: \n - Conduct unit tests, integration tests, and end-to-end tests to validate the code. \n\n- **Communication with the Team**: \n - Coordinate with the team to ensure alignment and address any concerns. \n\n- **Documentation**: \n - Update documentation to reflect the changes made in the pull request. \n\nThis approach ensures a thorough and constructive review process.", "retry_text": "Here’s a structured approach to reviewing a pull request:\n\n- **Code Review for Correctness and Style**: \n - Ensure code is logically sound and follows project standards. \n - Check for syntax errors, logic flaws, and code duplication. \n\n- **Identify Issues and Bug Fixes**: \n - Locate and resolve any bugs, edge cases, or issues in the code. \n\n- **Ensure Maintainability**: \n - Verify that the code is easy to understand, testable, and scalable for future updates. \n\n- **Testing**: \n - Conduct unit tests, integration tests, and end-to-end tests to validate the code. \n\n- **Communication with the Team**: \n - Coordinate with the team to ensure alignment and address any concerns. \n\n- **Documentation**: \n - Update documentation to reflect the changes made in the pull request.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "5d1eee90ae0e4d93", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "Describe your approach to reviewing a pull request, as a list.", "text": "Here’s a structured approach to reviewing a pull request:\n\n1. **Code Review** \n - Check for bugs, syntax errors, and logical errors. \n - Ensure code follows coding standards and best practices. \n\n2. **Testing** \n - Validate functionality with unit tests and integration tests. \n - Ensure the code works as expected under various conditions. \n\n3. **Documentation** \n - Verify that the code is well-documented and explained. \n\n4. **Security** \n - Check for vulnerabilities (e.g., XSS, SQL injection) and security issues. \n\n5. **Code Quality** \n - Look for unused variables, inefficient loops, or code smells. \n\n6. **Contextual Understanding** \n - Understand the purpose of the pull request and the specific changes being made. \n\n7. **Impact on Other Components** \n - Ensure the code doesn’t introduce new issues or conflicts with existing code. \n\n8. **Automated Tools** \n - Use tools (e.g., linters, static analyzers) to catch issues before integration. \n\nThis approach ensures a thorough review while maintaining efficiency and quality.", "retry_text": "Here’s a structured approach to reviewing a pull request:\n\n1. **Code Review** \n - Check for bugs, syntax errors, and logical errors. \n - Ensure code follows coding standards and best practices. \n\n2. **Testing** \n - Validate functionality with unit tests and integration tests. \n - Ensure the code works as expected under various conditions. \n\n3. **Documentation** \n - Verify that the code is well-documented and explained. \n\n4. **Security** \n - Check for vulnerabilities (e.g., XSS, SQL injection) and security issues. \n\n5. **Code Quality** \n - Look for unused variables, inefficient loops, or code smells. \n\n6. **Contextual Understanding** \n - Understand the purpose of the pull request and the specific changes being made. \n\n7. **Impact on Other Components** \n - Ensure the code doesn’t introduce new issues or conflicts with existing code. \n\n8. **Automated Tools** \n - Use tools (e.g., linters, static analyzers) to catch issues before integration. \n\nThis approach ensures a thorough review while maintaining efficiency and quality.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "ae18ee4c0a550b21", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "Describe your approach to reviewing a pull request, as a list.", "text": "Here's a structured approach to reviewing a pull request:\n\n1. **Code Quality Checks** \n - ✅ Check for syntax errors, logical errors, and clean code. \n - ✅ Look for potential security vulnerabilities or code smells. \n\n2. **Documentation Review** \n - ✅ Ensure the PR includes well-documented changes and test cases. \n - ✅ Verify that any documentation related to the PR is updated and maintained. \n\n3. **Code Consistency** \n - ✅ Check for consistent coding style, naming conventions, and variable/constant usage. \n - ✅ Ensure all changes align with the project’s existing codebase. \n\n4. **Test Coverage** \n - ✅ Verify that the PR includes test cases for the affected functionality. \n - ✅ Check for any edge cases or scenarios that might need special attention. \n\n5. **Issue Resolution** \n - ✅ Ensure the PR addresses any open issues or bugs in the project. \n - ✅ Confirm that the PR has been reviewed and approved by relevant stakeholders. \n\n6. **CI/CD Integration** \n - ✅ Check that the PR is prepared for integration into the main codebase (CI/CD pipeline). \n - ✅ Ensure that the changes are compatible with existing workflows. \n\n7. **Future-Proofing** \n - ✅ Review for architectural changes that could break future features. \n - ✅ Ensure the code is maintainable and scalable for long-term use. \n\nThis approach ensures that the PR is reviewed thoroughly and ready for merging into the main codebase.", "retry_text": "Here's a structured approach to reviewing a pull request: \n\n1. **Code Quality Checks** \n - ✅ Check for syntax errors, logical errors, and clean code. \n - ✅ Ensure potential security vulnerabilities or code smells are addressed. \n\n2. **Documentation Review** \n - ✅ Verify well-documented changes and test cases are included. \n - ✅ Confirm documentation aligns with the project’s existing structure. \n\n3. **Code Consistency** \n - ✅ Check coding style, naming conventions, and variable/constant usage. \n - ✅ Ensure all changes align with the project’s codebase standards. \n\n4. **Test Coverage** \n - ✅ Verify the PR includes test cases for the affected functionality. \n - ✅ Check edge cases or scenarios requiring special attention. \n\n5. **Issue Resolution** \n - ✅ Ensure the PR addresses any open issues or bugs in the project. \n - ✅ Confirm the changes are ready for review and approval. \n\n6. **CI/CD Integration** \n - ✅ Check compatibility with existing CI/CD workflows. \n - ✅ Ensure changes are prepared for integration into the main codebase. \n\n7. **Future-Proofing** \n - ✅ Review for architectural changes that could break future features. \n - ✅ Ensure the code is maintainable, scalable, and ready for long-term use.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "93f33dfafe4b553e", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "Describe your approach to reviewing a pull request, as a list.", "text": "Here's a structured approach to reviewing a pull request:\n\n1. **Code Review**: Check the code for syntax errors, logic errors, and any potential issues. Ensure the implementation aligns with the project's coding standards.\n2. **Documentation**: Verify that the changes in the PR are documented in the codebase and that the documentation is updated accordingly.\n3. **Project Codebase**: Compare the PR changes with the existing codebase to ensure no missed or duplicated work. Check for any unintended side effects.\n4. **Security Vulnerabilities**: Look for any potential security flaws, such as insecure code or vulnerabilities that could be exploited.\n5. **Testing**: Ensure the changes pass all relevant tests, especially if the code is complex or new to the project.\n6. **Communication**: Communicate with the maintainers or the project's team to gather their feedback and address any concerns.\n\nThis approach ensures a thorough and constructive review of the PR.", "retry_text": "Here's a structured approach to reviewing a pull request:\n\n1. **Code Review**: Check the code for syntax errors, logic errors, and any potential issues. Ensure the implementation aligns with the project's coding standards. \n2. **Documentation**: Verify that the changes in the PR are documented in the codebase and that the documentation is updated accordingly. \n3. **Project Codebase**: Compare the PR changes with the existing codebase to ensure no missed or duplicated work. Check for any unintended side effects. \n4. **Security Vulnerabilities**: Look for any potential security flaws, such as insecure code or vulnerabilities that could be exploited. \n5. **Testing**: Ensure the changes pass all relevant tests, especially if the code is complex or new to the project. \n6. **Communication**: Communicate with the maintainers or the project's team to gather their feedback and address any concerns.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "e3307b82f25a0462", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "Describe your approach to reviewing a pull request, as a list.", "text": "Here's a structured approach to reviewing a pull request:\n\n1. **Code Review Quality Check** \n - Ensure code meets project standards and has no syntax errors \n - Check for logical errors, performance issues, or security flaws \n\n2. **Code Documentation** \n - Verify the issue description is well-documented and addresses the original problem \n - Ensure any changes are clearly explained in the documentation \n\n3. **Testing** \n - Conduct unit tests and integration tests \n - Check for compatibility issues with existing code and dependencies \n\n4. **Issue Resolution** \n - Address any issues raised in the pull request \n - Ensure the pull request is ready for merging \n\n5. **Code Policy Compliance** \n - Confirm it aligns with the project's code review policies and guidelines \n\nThis approach ensures a thorough and constructive review process.", "retry_text": "Here's a structured approach to reviewing a pull request:\n\n1. **Code Review Quality Check** \n - Ensure code meets project standards and has no syntax errors \n - Check for logical errors, performance issues, or security flaws \n\n2. **Code Documentation** \n - Verify the issue description is well-documented and addresses the original problem \n - Ensure any changes are clearly explained in the documentation \n\n3. **Testing** \n - Conduct unit tests and integration tests \n - Check for compatibility issues with existing code and dependencies \n\n4. **Issue Resolution** \n - Address any issues raised in the pull request \n - Ensure the pull request is ready for merging \n\n5. **Code Policy Compliance** \n - Confirm it aligns with the project's code review policies and guidelines \n\nThis approach ensures a thorough and constructive review process.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "bddfdd9ea80c6169", "model": "qwen3-0.6b", "prompt_class": "format", "prompt": "Describe your approach to reviewing a pull request, as a list.", "text": "Here's a structured approach to reviewing a pull request:\n\n1. **Initial Review** \n - **Commit Message**: Ensure the message is clear, concise, and addresses the issue. \n - **Code Formatting**: Check for consistent formatting, including whitespace, spacing, and code blocks. \n - **Licensing and Policies**: Verify the code complies with project policies (e.g., MIT, GPL, or specific licensing agreements). \n\n2. **Code Quality Check** \n - **Code Documentation**: Ensure changes are well-documented and align with existing documentation. \n - **Code Standards**: Confirm the code follows the project's coding guidelines (e.g., PEP 8, style rules). \n\n3. **Issue Resolution** \n - **Issue Description**: Validate the issue description is clear and the fix addresses the issue. \n - **Code for Fixes**: Review the fix implementation and ensure it solves the problem effectively. \n\n4. **Testing** \n - **Unit and Integration Tests**: Ensure the code works as intended in tests (e.g., unit tests, integration tests). \n\n5. **History Review** \n - **Conflict Checks**: Ensure there are no duplicate or conflicting changes in the history. \n - **PR History**: Confirm the PR includes the correct code and no unnecessary changes. \n\n6. **Documentation** \n - **Update Documentation**: Update documentation to reflect the changes made in the pull request. \n\n7. **Final Approval** \n - **Final Check**: Confirm all points are addressed and the PR is ready for review. \n\nThis approach ensures a thorough review while maintaining efficiency and quality.", "retry_text": "Here's a structured approach to reviewing a pull request:\n\n1. **Initial Review** \n - **Commit Message**: Ensure the message is clear, concise, and addresses the issue. \n - **Code Formatting**: Check for consistent formatting, including whitespace, spacing, and code blocks. \n\n2. **Code Quality Check** \n - **Code Documentation**: Ensure changes are well-documented and align with existing documentation. \n - **Code Standards**: Confirm the code follows the project's coding guidelines (e.g., PEP 8, style rules). \n\n3. **Issue Resolution** \n - **Issue Description**: Validate the issue description is clear and the fix addresses the issue. \n - **Code for Fixes**: Review the fix implementation and ensure it solves the problem effectively. \n\n4. **Testing** \n - **Unit and Integration Tests**: Ensure the code works as intended in tests (e.g., unit tests, integration tests). \n\n5. **History Review** \n - **Conflict Checks**: Ensure there are no duplicate or conflicting changes in the history. \n - **PR History**: Confirm the PR includes the correct code and no unnecessary changes. \n\n6. **Documentation** \n - **Update Documentation**: Update documentation to reflect the changes made in the pull request. \n\n7. **Final Approval** \n - **Final Check**: Confirm all points are addressed and the PR is ready for review. \n\nThis approach ensures a thorough review while maintaining efficiency and quality.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "2f578170c686ad08", "model": "qwen3-0.6b", "prompt_class": "ambiguous", "prompt": "Can you look into that for me?", "text": "I can help you with something. Could you please specify what you need assistance with?", "retry_text": "I can help you with something. Could you please specify what you need assistance with?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "0156c4d90b02722b", "model": "qwen3-0.6b", "prompt_class": "ambiguous", "prompt": "Can you look into that for me?", "text": "I can help you with that. Could you please specify what you need assistance with?", "retry_text": "I can help you with that. Could you please specify what you need assistance with?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "19beab9b9b904353", "model": "qwen3-0.6b", "prompt_class": "ambiguous", "prompt": "Can you look into that for me?", "text": "I can assist you if you need help with a specific task. Could you please clarify what you'd like me to do? For example, is there a web search you'd like to perform, or is there a coding problem you'd like to solve?", "retry_text": "I can assist you if you need help with a specific task. Could you please clarify what you'd like me to do? For example, is there a web search you'd like to perform, or is there a coding problem you'd like to solve?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "e077760a025ac077", "model": "qwen3-0.6b", "prompt_class": "ambiguous", "prompt": "Can you look into that for me?", "text": "I can help you with that! Could you please clarify what you need assistance with? For example, are you looking for information, a solution to a problem, or a specific action to take?", "retry_text": "I can help you with that! Could you please clarify what you need assistance with? For example, are you looking for information, a solution to a problem, or a specific action to take?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "8b5d6af1da80759a", "model": "qwen3-0.6b", "prompt_class": "ambiguous", "prompt": "Can you look into that for me?", "text": "I can help you with that! Could you please specify what you need assistance with?", "retry_text": "Could you please specify what you need assistance with?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "f989b1b1d2d66995", "model": "qwen3-0.6b", "prompt_class": "ambiguous", "prompt": "Can you look into that for me?", "text": "I can't look into that. I can only perform web searches or execute Python code. Could you please provide more details or specify what you're looking for?", "retry_text": "I cannot look into that. I can only perform web searches or execute Python code. Could you please provide more details or specify what you're looking for?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "069078e3f77bd1f3", "model": "qwen3-0.6b", "prompt_class": "ambiguous", "prompt": "Can you look into that for me?", "text": "Sure! Could you please tell me what you need assistance with?", "retry_text": "Sure! Could you please tell me what you need assistance with?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "d1a015f991ab3d00", "model": "qwen3-0.6b", "prompt_class": "ambiguous", "prompt": "Can you look into that for me?", "text": "I can help you with that. Could you please clarify what specific task or query you need assistance with?", "retry_text": "I can help you with that. Could you please clarify what specific task or query you need assistance with?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "8ba7363ec8d4808b", "model": "qwen3-0.6b", "prompt_class": "ambiguous", "prompt": "Fix it.", "text": "Could you please specify what \"Fix it\" refers to in your context? For example, is it a task to be completed, a code issue, a configuration problem, or something else? This will help me provide you with the most accurate assistance.", "retry_text": "Could you please specify what \"Fix it\" refers to in your context? For example, is it a task to be completed, a code issue, a configuration problem, or something else? This will help me provide you with the most accurate assistance.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "dd46a0139e68687c", "model": "qwen3-0.6b", "prompt_class": "ambiguous", "prompt": "Fix it.", "text": "I'm sorry, but I'm not sure what \"Fix it\" refers to. Could you please provide more context or specify the issue you'd like me to address?", "retry_text": "I'm sorry, but I'm not sure what \"Fix it\" refers to. Could you please provide more context or specify the issue you'd like me to address?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "09cbf0f6bc16ece4", "model": "qwen3-0.6b", "prompt_class": "ambiguous", "prompt": "Fix it.", "text": "Could you please specify what \"Fix it\" refers to? For example, is there a task, a requirement, or a problem that needs fixing? This will help me provide you with the appropriate assistance.", "retry_text": "Could you please specify what \"Fix it\" refers to? For example, is there a task, a requirement, or a problem that needs fixing? This will help me provide you with the appropriate assistance.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "5d1c121db57e292c", "model": "qwen3-0.6b", "prompt_class": "ambiguous", "prompt": "Fix it.", "text": "Could you please specify what issue or problem you're referring to? I'll be able to help you fix it.", "retry_text": "Could you please specify what issue or problem you're referring to? I'll be able to help you fix it.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "92551203a59734bf", "model": "qwen3-0.6b", "prompt_class": "ambiguous", "prompt": "Fix it.", "text": "Could you please specify what issue or problem you'd like to fix?", "retry_text": "Could you please specify what issue or problem you'd like to fix?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "71a8e1df216e3c57", "model": "qwen3-0.6b", "prompt_class": "ambiguous", "prompt": "Fix it.", "text": "Could you please specify what \"Fix it\" refers to? For example, is there a task, problem, or issue you want to address? Let me know and I'll do my best to help.", "retry_text": "Could you please specify what \"Fix it\" refers to? For example, is there a task, problem, or issue you want to address? Let me know and I'll do my best to help.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "3fd18b05b0bf9744", "model": "qwen3-0.6b", "prompt_class": "ambiguous", "prompt": "Fix it.", "text": "Could you please provide more context or specify what \"Fix it\" refers to in your situation?", "retry_text": "The message \"Fix it\" doesn't contain a specific request or task to address. To proceed, could you please provide more context or clarify what \"Fix it\" refers to in your situation?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "63b05dc7bff381cd", "model": "qwen3-0.6b", "prompt_class": "ambiguous", "prompt": "Tell me about the thing we discussed.", "text": "I don't have access to a conversation history or previous information. Could you please clarify what \"the thing we discussed\" refers to? Let me know and I'll do my best to assist.", "retry_text": "I don't have access to a conversation history or previous information. Could you please clarify what \"the thing we discussed\" refers to? Let me know and I'll do my best to assist.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "513c9d08d9dd0f83", "model": "qwen3-0.6b", "prompt_class": "ambiguous", "prompt": "Tell me about the thing we discussed.", "text": "I can't tell you about what we discussed without more context. Could you please specify the topic or provide details about the subject we've been discussing?", "retry_text": "I cannot proceed without more context. Could you please specify the subject or topic we discussed?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "986805e14cfb085e", "model": "qwen3-0.6b", "prompt_class": "ambiguous", "prompt": "Tell me about the thing we discussed.", "text": "Could you please specify what \"the thing we discussed\" refers to so I can provide a detailed response?", "retry_text": "Could you please specify what \"the thing we discussed\" refers to so I can provide a detailed response?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "3a8cc99a4fd7102d", "model": "qwen3-0.6b", "prompt_class": "ambiguous", "prompt": "Tell me about the thing we discussed.", "text": "I don't have access to specific information or a conversation history in this context. Could you please provide more details or clarify what you're referring to?", "retry_text": "I don't have access to specific information or a conversation history in this context. Could you please provide more details or clarify what you're referring to?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "1a596b971a3932b3", "model": "qwen3-0.6b", "prompt_class": "ambiguous", "prompt": "Tell me about the thing we discussed.", "text": "I can't tell you about the topic we discussed without more information. Could you please specify what the subject was or provide details about the conversation?", "retry_text": "I cannot provide a detailed explanation about the topic we discussed without specific information or context. Could you please share more details or clarify what the subject was or the conversation was about?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "b321d857119f0e1a", "model": "qwen3-0.6b", "prompt_class": "ambiguous", "prompt": "Tell me about the thing we discussed.", "text": "I can't determine the specific topic without additional context. Could you please clarify what topic or subject you're interested in?", "retry_text": "I cannot determine the specific topic without additional context. Could you please clarify what topic or subject you're interested in?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "f27d000a239b9998", "model": "qwen3-0.6b", "prompt_class": "ambiguous", "prompt": "Tell me about the thing we discussed.", "text": "I don't have access to your conversation history. Could you please provide more context or specify what \"the thing we discussed\" refers to?", "retry_text": "I don't have access to your conversation history. Could you please provide more context or specify what \"the thing we discussed\" refers to?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "df692d56e5002f3b", "model": "qwen3-0.6b", "prompt_class": "ambiguous", "prompt": "Tell me about the thing we discussed.", "text": "I don't have access to previous conversation history. Could you please specify what the \"thing we discussed\" refers to?", "retry_text": "I don't have access to previous conversation history. Could you please specify what the \"thing we discussed\" refers to?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "e2051d39a9ee4a41", "model": "qwen3-0.6b", "prompt_class": "ambiguous", "prompt": "Is it better?", "text": "The question \"Is it better?\" is a bit ambiguous. Could you please provide more context or clarify what specific aspect you're referring to (e.g., which decision, choice, or situation you're comparing)? This will help me better respond.", "retry_text": "The question \"Is it better?\" is ambiguous and lacks specificity. To assist you, please clarify what aspect or context you are referring to (e.g., which decision, choice, or situation you’re comparing).", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "587cc3f962fa5e14", "model": "qwen3-0.6b", "prompt_class": "ambiguous", "prompt": "Is it better?", "text": "I can't determine \"Is it better?\" without more context or specific information. Could you please provide the question or clarify what you're referring to?", "retry_text": "The question \"Is it better?\" is ambiguous without additional context. Could you please specify the question or clarify what you're referring to?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "b15957ffbe039c9e", "model": "qwen3-0.6b", "prompt_class": "ambiguous", "prompt": "Is it better?", "text": "The question is a bit ambiguous. Could you clarify what you're asking? For example, are you comparing two options, deciding which is better, or looking for information on a specific topic? This would help me provide a more accurate response.", "retry_text": "The question is ambiguous. Could you clarify what you're asking? For example, are you comparing two options, deciding which is better, or looking for information on a specific topic?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "a358f81fdb3324a7", "model": "qwen3-0.6b", "prompt_class": "ambiguous", "prompt": "Is it better?", "text": "The choice between using `web_search` or `python` depends on your specific needs. Here's how each tool can help:\n\n- **`web_search`**: Ideal for general information retrieval, as it can fetch full-page text from URLs. Useful for finding answers to questions or solving problems.\n- **`python`**: Great for executing code within a sandbox environment, which is perfect for tasks like data processing, automation, or scripting.\n\nIf you have a specific task or question in mind, let me know what tools you'd like to use!", "retry_text": "The choice between `web_search` and `python` depends on your specific needs. Here's how each tool functions:\n\n- **`web_search`**: Fetches full-page text from URLs to retrieve information, ideal for solving problems or finding answers to questions.\n- **`python`**: Executes code in a sandbox environment, suitable for tasks like data processing, automation, or scripting.\n\nIf you have a specific task, let me know what tools you’d like to use!", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "7ecabf000158e586", "model": "qwen3-0.6b", "prompt_class": "ambiguous", "prompt": "Is it better?", "text": "Is it better? Please provide the specific context or choices you're comparing, and I can help you determine the best option.", "retry_text": "The question \"Is it better?\" is ambiguous and lacks context or specific choices to compare. To assist you, please provide the specific question, comparison criteria, or options you're evaluating. For example, if you're comparing two products, services, or decisions, clarify what you're referring to. Without this information, I can't determine the best option.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "0cebe6188ebb1145", "model": "qwen3-0.6b", "prompt_class": "ambiguous", "prompt": "What should I do next?", "text": "To help you better, could you please provide more details or clarify what \"next\" refers to in your context? For example, are you looking for a specific action, a step-by-step guide, or assistance with something else?", "retry_text": "To assist you better, could you please provide more details or clarify what \"next\" refers to in your context? For example, are you looking for a specific action, a step-by-step guide, or something else?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "354119601b55966d", "model": "qwen3-0.6b", "prompt_class": "ambiguous", "prompt": "What should I do next?", "text": "To assist you effectively, could you please share more details about the situation or question you're facing? For example, are you looking for help with a specific problem, a request, or guidance on a topic?", "retry_text": "To assist you effectively, could you please share more details about the situation or question you're facing? For example, are you looking for help with a specific problem, a request, or guidance on a topic?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "4997cc2bb2015a82", "model": "qwen3-0.6b", "prompt_class": "ambiguous", "prompt": "What should I do next?", "text": "Could you please provide more details about the situation or the query you're facing?", "retry_text": "Could you please provide more details about the situation or the query you're facing?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "cbe166be7cdfccfe", "model": "qwen3-0.6b", "prompt_class": "ambiguous", "prompt": "What should I do next?", "text": "To help you better, could you please provide more details about your situation or the specific question you're facing?", "retry_text": "Could you please provide more details about your situation or the specific question you're facing?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "e3c3a09b2135bb07", "model": "qwen3-0.6b", "prompt_class": "ambiguous", "prompt": "What should I do next?", "text": "I can help you with specific questions. Could you please provide more details or clarify what you're looking for?", "retry_text": "Could you please specify what you need help with?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "eec98c2dd8620558", "model": "qwen3-0.6b", "prompt_class": "ambiguous", "prompt": "What should I do next?", "text": "Could you please clarify what you need assistance with? For example, are you looking for information, a solution to a problem, or help with something specific?", "retry_text": "Could you please clarify what you need assistance with? For example, are you looking for information, a solution to a problem, or help with something specific?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "55159a82d8ce1dc7", "model": "qwen3-0.6b", "prompt_class": "ambiguous", "prompt": "What should I do next?", "text": "The question \"What should I do next?\" is general and doesn't provide enough context for me to determine the appropriate action. Could you please specify the situation or problem you're facing so I can assist you better?", "retry_text": "The question \"What should I do next?\" is general and doesn't provide specific context for me to determine the appropriate action. Could you please specify the situation or problem you're facing so I can assist you better?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "5e31f765cb50a4e8", "model": "qwen3-0.6b", "prompt_class": "non_english", "prompt": "Quelle est la capitale de la France ?", "text": "La capitale de la France est Paris.", "retry_text": "La capitale de la France est Paris.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "5c719501e2307082", "model": "qwen3-0.6b", "prompt_class": "non_english", "prompt": "Quelle est la capitale de la France ?", "text": "La capitale de la France est Paris.", "retry_text": "La capitale de la France est Paris.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "ebb10de7c8d9f6cd", "model": "qwen3-0.6b", "prompt_class": "non_english", "prompt": "Quelle est la capitale de la France ?", "text": "La capitale de la France est Lyon.", "retry_text": "La capitale de la France est Lyon.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "3eefd2de8fb723f7", "model": "qwen3-0.6b", "prompt_class": "non_english", "prompt": "Quelle est la capitale de la France ?", "text": "La capitale de la France est Paris.", "retry_text": "La capitale de la France est Paris.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "0cca10877add3e01", "model": "qwen3-0.6b", "prompt_class": "non_english", "prompt": "Quelle est la capitale de la France ?", "text": "La capitale de la France est Paris.", "retry_text": "La capitale de la France est Paris.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "a06f840c1397b49a", "model": "qwen3-0.6b", "prompt_class": "non_english", "prompt": "Quelle est la capitale de la France ?", "text": "La capitale de la France est Paris.", "retry_text": "La capitale de la France est Paris.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "b9fae31106f76bfc", "model": "qwen3-0.6b", "prompt_class": "non_english", "prompt": "Quelle est la capitale de la France ?", "text": "La capitale de la France est Paris.", "retry_text": "La capitale de la France est Paris.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "1bd8528c0be010cb", "model": "qwen3-0.6b", "prompt_class": "non_english", "prompt": "Quelle est la capitale de la France ?", "text": "La capitale de la France est Paris.", "retry_text": "La capitale de la France est Paris.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "2fe04e2076dbadad", "model": "qwen3-0.6b", "prompt_class": "non_english", "prompt": "Explica en una frase que es el sobreajuste.", "text": "El sobreajuste es el fenómeno en el que una persona exagera su capacidad o habilidad, lo que lleva a una desviación en sus actos o resultados.", "retry_text": "El sobreajuste es el fenómeno en el que una persona exagera su capacidad o habilidad, lo que lleva a una desviación en sus actos o resultados.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "d09e13931c8de2f5", "model": "qwen3-0.6b", "prompt_class": "non_english", "prompt": "Explica en una frase que es el sobreajuste.", "text": "El sobreajuste es cuando el monto de la deuda es mayor que lo que debe pagar.", "retry_text": "El sobreajuste es cuando el monto de la deuda es mayor que lo que debe pagar.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "933c9d680abe10f3", "model": "qwen3-0.6b", "prompt_class": "non_english", "prompt": "Explica en una frase que es el sobreajuste.", "text": "Sobreajuste es cuando una actividad o sistema se enfrenta a una carga sobrecarga que puede no ser resuelta debido a la capacidad limitada de los recursos.", "retry_text": "Sobreajuste es la situación en la que una actividad o sistema enfrenta una carga sobrecarga que no puede ser resuelta debido a la capacidad limitada de los recursos.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "91712a4316d56f06", "model": "qwen3-0.6b", "prompt_class": "non_english", "prompt": "Explica en una frase que es el sobreajuste.", "text": "El sobreajuste se refiere a la excesiva consumo de recursos o recursos materiales.", "retry_text": "El sobreajuste se refiere a la excesiva consumo de recursos o recursos materiales.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "19851173a737b424", "model": "qwen3-0.6b", "prompt_class": "non_english", "prompt": "Explica en una frase que es el sobreajuste.", "text": "El sobreajuste ocurre cuando una persona experimenta una excesiva carga física o mental, causando desgaste.", "retry_text": "El sobreajuste ocurre cuando una persona experimenta una excesiva carga física o mental, causando desgaste.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "6273bf296ba0d580", "model": "qwen3-0.6b", "prompt_class": "non_english", "prompt": "Explica en una frase que es el sobreajuste.", "text": "El sobreajuste es un sobreajuste, es decir, una pérdida financiera o un impago que ocurre cuando se enfrentan desafíos económicos o operaciones no esperadas.", "retry_text": "El sobreajuste es una pérdida financiera o un impago que ocurre cuando se enfrentan desafíos económicos o operaciones no esperadas.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "c5fc34cdf315ee06", "model": "qwen3-0.6b", "prompt_class": "non_english", "prompt": "Was ist der Unterschied zwischen RAM und Festplatte?", "text": "RAM (Random Access Memory) and a hard drive (Festplatte) are two different types of storage and memory in computers. Here's the distinction:\n\n1. **RAM (Memory):**\n - **Purpose:** Temporary storage for active data and running applications.\n - **Speed:** Accesses data quickly (typically faster than a hard drive).\n - **Volatility:** Data is lost when the computer turns off.\n - **Usage:** Used for tasks that require immediate access, like browsing, running programs, or processing data.\n\n2. **Hard Drive (Festplatte):**\n - **Purpose:** Permanent storage for files, programs, and data.\n - **Speed:** Accesses data slowly compared to RAM.\n - **Durability:** Stores information long-term without losing data.\n - **Usage:** For storing software, user data, and backups.\n\nRAM is volatile, while a hard drive is non-volatile.", "retry_text": "RAM (Random Access Memory) and a hard drive (Festplatte) are two types of storage in computers. Here's the key difference:\n\n1. **RAM (Memory):**\n - **Purpose:** Temporary storage for active data and running applications.\n - **Speed:** Accesses data quickly (typically faster than a hard drive).\n - **Volatility:** Data is lost when the computer turns off.\n - **Usage:** Used for tasks requiring immediate access, like browsing, running programs, or processing data.\n\n2. **Hard Drive (Festplatte):**\n - **Purpose:** Permanent storage for files, programs, and data.\n - **Speed:** Accesses data slowly compared to RAM.\n - **Durability:** Stores information long-term without losing data.\n - **Usage:** For storing software, user data, and backups.\n\nRAM is volatile and loses data when the computer turns off, while a hard drive holds data permanently.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "56b6ed34e8ccc3b0", "model": "qwen3-0.6b", "prompt_class": "non_english", "prompt": "Was ist der Unterschied zwischen RAM und Festplatte?", "text": "RAM (Random Access Memory) and a hard drive (Festplatte) serve different purposes in a computer:\n\n1. **RAM (Memory):** \n - Holds data temporarily for quick access. It's used to run programs and store information currently being used by the CPU. \n - Is consumed when the computer is actively processing data (e.g., running a program). It's faster than the hard drive because it's accessed directly by the CPU.\n\n2. **Hard Drive (Festplatte):** \n - Stores data permanently. It's used to store files, system information, and software. \n - Is slower than RAM because it requires a separate processor to access the data.\n\n**Key Difference:** \nRAM is for quick, temporary tasks, while the hard drive is for long-term storage.", "retry_text": "RAM (Random Access Memory) and a hard drive (Festplatte) serve different purposes in a computer:\n\n1. **RAM (Memory):** \n - Stores data temporarily for quick access. It's used to run programs and hold information currently being used by the CPU. It's faster than the hard drive because it's directly accessed by the CPU.\n\n2. **Hard Drive (Festplatte):** \n - Stores data permanently. It holds files, system information, and software. It's slower than RAM because it requires a separate processor to access data.\n\n**Key Difference:** \nRAM is for quick, temporary tasks, while the hard drive stores long-term data.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "ae569736c21c98ab", "model": "qwen3-0.6b", "prompt_class": "non_english", "prompt": "Was ist der Unterschied zwischen RAM und Festplatte?", "text": "RAM (Random Access Memory) is temporary storage used to hold data that is currently being used by the CPU. It's volatile, meaning it loses its data when the power goes out. Unlike a hard drive, which stores data permanently, RAM is used for quick data access. \n\nA hard drive (or SSD) stores data permanently, and its speed depends on whether it's an SSD (non-volatile) or an HDD (volatile). SSDs are faster because they don't use mechanical components, while HDDs are slower. The hard drive can be read from (RAID) or not (non-Raid) depending on its configuration.", "retry_text": "RAM (Random Access Memory) is temporary storage used to hold data that the CPU is currently processing. It's volatile, meaning it loses its data when the power goes out. A hard drive (or SSD) stores data permanently, and its speed depends on whether it's an SSD (non-volatile) or an HDD (volatile). RAM is used for quick data access, while a hard drive is for long-term storage.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "9a67df1137489d09", "model": "qwen3-0.6b", "prompt_class": "non_english", "prompt": "Was ist der Unterschied zwischen RAM und Festplatte?", "text": "RAM (Random Access Memory) is a type of temporary storage used to hold active data while a computer is running programs. It's volatile, meaning it's not as stable as a hard drive, which stores data permanently. RAM is used to quickly access and process data as needed, whereas a hard drive (or SSD) stores data permanently, allowing for faster access times.", "retry_text": "RAM (Random Access Memory) is a type of temporary storage used to hold active data while a computer is running programs. It is volatile, meaning it loses its data when the power is off. RAM is used to quickly access and process data as needed, whereas a hard drive (or SSD) stores data permanently, allowing for faster access times.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "ae4fb07db438690b", "model": "qwen3-0.6b", "prompt_class": "non_english", "prompt": "Was ist der Unterschied zwischen RAM und Festplatte?", "text": "RAM (Random Access Memory) is a type of volatile memory used to hold data temporarily. It allows quick access to programs and data, making it essential for running applications or waiting for input. In contrast, a hard drive (Festplatte) is a non-volatile storage device used to store permanent data and software. It doesn't require constant power and can hold more data than RAM. Both components work together to support the computer's operations, with RAM being temporary and the hard drive storing permanent information.", "retry_text": "RAM (Random Access Memory) is a type of volatile memory used to hold data temporarily, while a hard drive (Festplatte) is a non-volatile storage device used to store permanent data and software. RAM allows quick access to programs and data, whereas the hard drive holds permanent information and storage. Both components work together to support the computer's operations.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "f71d15a0eaffe179", "model": "qwen3-0.6b", "prompt_class": "non_english", "prompt": "Was ist der Unterschied zwischen RAM und Festplatte?", "text": "RAM (Random Access Memory) is a type of temporary storage used to hold active data and instructions that the CPU is currently processing. It's faster than a hard drive because it's directly accessed by the CPU, allowing for quick data retrieval. A hard drive, in contrast, is a long-term storage device that holds data and operating system files, which can be accessed and saved over time. RAM is typically smaller in capacity but offers faster access speeds.", "retry_text": "RAM (Random Access Memory) and a hard drive are both storage components, but they serve different purposes: \n- **RAM** is a type of temporary memory used to hold active data and instructions the CPU is currently processing. It is faster than a hard drive because it's directly accessed by the CPU, allowing for quick data retrieval. \n- **Hard drive** holds data and operating system files over time, meaning it's less volatile and can store large amounts of information without losing it. \n\nRAM is typically smaller in capacity but provides faster access speeds.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "3333944ab8b3b14d", "model": "qwen3-0.6b", "prompt_class": "non_english", "prompt": "Was ist der Unterschied zwischen RAM und Festplatte?", "text": "RAM (Random Access Memory) is a type of temporary storage used in computers to hold data and instructions temporarily. It's volatile, meaning it loses data when the computer turns off. The hard drive, on the other hand, is a non-volatile storage device that holds the operating system and user data. It's not replaced, but it can be upgraded or replaced over time. RAM is used for quick access to data, while the hard drive stores more data.", "retry_text": "RAM (Random Access Memory) is a type of volatile storage used to hold data and instructions temporarily. It loses data when the computer turns off. A hard drive (HDD) is non-volatile and stores operating system and user data, not replaced over time. RAM is used for quick access, while HDDs store more data.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "22a85371668ee43a", "model": "qwen3-0.6b", "prompt_class": "post_tool", "prompt": "Thanks. So which version should I install?", "text": "The latest llama.cpp release version is **b6291** (2026-07-14). This version includes improvements to the Vulkan backend and faster prompt processing on CUDA. You should follow the official documentation or GitHub release notes for the most accurate installation instructions.", "retry_text": "The latest llama.cpp release version is **b6291** (2026-07-14). This version includes improvements to the Vulkan backend and faster prompt processing on CUDA. You should follow the official documentation or GitHub release notes for the most accurate installation instructions.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "4cf325393db5369b", "model": "qwen3-0.6b", "prompt_class": "post_tool", "prompt": "Thanks. So which version should I install?", "text": "The latest available version is **b6291** (2026-07-14). This version includes improvements such as Vulkan backend enhancements and faster prompt processing on CUDA.\n\nWould you like additional details about this version or any updates beyond this release?", "retry_text": "The latest available version is **b6291** (2026-07-14). This version includes improvements such as Vulkan backend enhancements and faster prompt processing on CUDA. Let me know if you need further details!", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "544a64cdb96703fa", "model": "qwen3-0.6b", "prompt_class": "post_tool", "prompt": "Thanks. So which version should I install?", "text": "The latest available version for Llama.cpp is **2026-07-14**, which includes improvements like Vulkan backend enhancements and faster prompt processing on CUDA. You can install it directly from the official Llama.cpp GitHub repository. For installation instructions, refer to the [Llama.cpp documentation](https://github.com/llama-project/llama.cpp) or the [GitHub release page](https://github.com/llama-project/llama.cpp/releases). Let me know if you need further assistance!", "retry_text": "The latest available version for Llama.cpp is **2026-07-14**, which includes improvements like Vulkan backend enhancements and faster prompt processing on CUDA. You can install it directly from the official Llama.cpp GitHub repository. For installation instructions, refer to the [Llama.cpp documentation](https://github.com/llama-project/llama.cpp) or the [GitHub release page](https://github.com/llama-project/llama.cpp/releases). Let me know if you need further assistance!", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "29a0818174227203", "model": "qwen3-0.6b", "prompt_class": "post_tool", "prompt": "Thanks. So which version should I install?", "text": "The latest llama.cpp version is **b6291** (2026-07-14). This release includes improvements for the Vulkan backend and faster prompt processing on CUDA. Let me know if you need further details!", "retry_text": "The latest llama.cpp version is **b6291** (2026-07-14). This release includes improvements for the Vulkan backend and faster prompt processing on CUDA. Let me know if you need further details!", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "e200b9622f9f9588", "model": "qwen3-0.6b", "prompt_class": "post_tool", "prompt": "Thanks. So which version should I install?", "text": "The latest known llama.cpp release version is **b6291** (2026-07-14). This version includes improvements such as Vulkan backend enhancements and faster prompt processing on CUDA. For the most accurate and up-to-date information, you can check the official llama.cpp documentation. Let me know if you need further details!", "retry_text": "The latest known llama.cpp release version is **b6291** (2026-07-14). This version includes improvements such as Vulkan backend enhancements and faster prompt processing on CUDA. Make sure to install it using the official documentation for the most accurate and up-to-date information. Let me know if you need further details!", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "d7da157d532ba3d6", "model": "qwen3-0.6b", "prompt_class": "post_tool", "prompt": "Thanks. So which version should I install?", "text": "The latest llama.cpp release version is **b6291 (2026-07-14)**. This version includes improvements such as enhanced Vulkan backend support and faster prompt processing on CUDA. You can install it via the official llama.cpp repository or via your environment setup. Let me know if you need further details!", "retry_text": "The latest llama.cpp release version is **b6291 (2026-07-14)**. This version includes improvements such as enhanced Vulkan backend support and faster prompt processing on CUDA. You can install it via the official llama.cpp repository or your environment setup. Let me know if you need further details!", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "a218f8ce82494ee9", "model": "qwen3-0.6b", "prompt_class": "post_tool", "prompt": "Thanks. So which version should I install?", "text": "The latest llama.cpp release is version **b6291** (2026-07-14). For installation, please refer to the official documentation or release notes for specific instructions. If you need help with dependencies or installation steps, I can assist further!", "retry_text": "The latest llama.cpp release is version **b6291** (2026-07-14). For installation, refer to the official documentation or release notes for specific instructions. Let me know if you need further assistance!", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "3ce863a497159351", "model": "qwen3-0.6b", "prompt_class": "post_tool", "prompt": "Do I need a coat?", "text": "Based on the current weather in Reykjavik (9°C, overcast, wind 22 km/h, humidity 81%), it's **not extremely cold**. The wind and high humidity might make it feel a bit chilly, but it's still a mild day. You might need a coat, especially if the wind is strong, but it's not overly harsh. If you're planning to go outdoors, a light jacket or a sweater could be sufficient.", "retry_text": "Based on the current weather in Reykjavik (9°C, overcast, wind 22 km/h, humidity 81%), it's **not extremely cold**. The wind and high humidity might make it feel a bit chilly, but it's still a mild day. You might need a coat, especially if you plan to go outdoors, but it's not overly harsh. A light jacket or sweater could be sufficient.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "9d59823d666f5eee", "model": "qwen3-0.6b", "prompt_class": "post_tool", "prompt": "Do I need a coat?", "text": "Given the current weather in Reykjavik (9°C, overcast, wind 22 km/h, humidity 81%), it is highly recommended to wear a **coat** to cope with the cold and potentially high humidity. While the temperature is relatively mild, the combination of wind and humidity can make the environment feel slightly colder. If you plan to go out, packing layers and a jacket is a good precaution.", "retry_text": "Yes, you should wear a coat in Reykjavik. The current weather is 9°C with wind and high humidity, which can make temperatures feel slightly colder. A coat is a good precaution to protect against the elements.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "0b32a9c799a643ef", "model": "qwen3-0.6b", "prompt_class": "post_tool", "prompt": "Do I need a coat?", "text": "Based on the current weather: 9°C with overcast skies and a breeze, a light jacket or sweater is recommended. If it's expected to be cold (e.g., snow or rain), a coat might be necessary. However, since the temperature is mild, a jacket is sufficient.", "retry_text": "Based on the current weather (9°C with overcast skies and a breeze), a light jacket or sweater is recommended. Since the temperature is mild, a coat may not be necessary.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "b28dcd77e59abd11", "model": "qwen3-0.6b", "prompt_class": "post_tool", "prompt": "Do I need a coat?", "text": "If the temperature is 9°C in Reykjavik, it's generally not very cold, but it's still a good idea to have a coat in light rain or snowfall. In this case, the weather is overcast, so it's possible there's a chance of rain. While a coat might not be necessary, it's still a prudent precaution to stay prepared.", "retry_text": "Based on the current weather in Reykjavik (9°C, overcast, light rain), a coat is still a prudent precaution to stay prepared, even if it's not very cold.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "8c6687b7138a1e68", "model": "qwen3-0.6b", "prompt_class": "post_tool", "prompt": "Do I need a coat?", "text": "In Reykjavik, the current weather is 9°C with overcast conditions. While the temperature is not extremely cold, the wind from the north east and high humidity (81%) may make it slightly chilly. In general, a coat is not necessary for such a mild temperature. However, if the wind is strong and the temperature is still warm, a coat could help. The exact need for a coat depends on the wind's strength and the time of year.", "retry_text": "In Reykjavik, the current weather is 9°C with overcast conditions. The wind and humidity (81%) may make it slightly chilly, but a coat is not necessary for such a mild temperature. If the wind is strong or the temperature remains warm, a coat could help.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "aa9668cc9c6353c6", "model": "qwen3-0.6b", "prompt_class": "post_tool", "prompt": "Do I need a coat?", "text": "Based on the current weather in Reykjavik (9°C, overcast, wind from the north east, humidity 81%), it's likely that you'll need a light jacket or sweater. The temperature is above freezing, so a warm layer is essential for comfort. If you don't have a jacket, a sweater could be a good alternative. Make sure to prepare for the weather!", "retry_text": "Based on the current weather in Reykjavik (9°C, overcast, wind from the north east, humidity 81%), it's likely that you will need a light jacket or sweater. The temperature is above freezing, so a warm layer is essential for comfort. If you don't have a jacket, a sweater could be a good alternative. Make sure to prepare for the weather!", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "766a83d5188fad2f", "model": "qwen3-0.6b", "prompt_class": "post_tool", "prompt": "Do I need a coat?", "text": "Based on the current weather in Reykjavik, which is **9°C (40°F)** with **overcast** conditions, there isn't a need for a heavy coat right now. The mild temperature combined with light clouds suggests it's a dry day, so a light jacket or a sweater should be sufficient. If it starts to rain soon, a light jacket is still a good option. Let me know if you'd like more details!", "retry_text": "Based on the current weather in Reykjavik, which is **9°C (40°F)** with **overcast** conditions, there isn't a need for a heavy coat. A light jacket or sweater is sufficient. If you're planning to go outside, a light clothing item will be appropriate. Let me know if you'd like further details!", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "422a14dde624fb20", "model": "qwen3-0.6b", "prompt_class": "post_tool", "prompt": "Do I need a coat?", "text": "Based on the current weather conditions in Reykjavik (9°C, overcast, wind at 22 km/h, and humidity at 81%), it is likely to be chilly. However, while it's not extremely cold, it's possible that the weather may still require a coat. If the temperature drops below 0°C, a coat would be essential, but with 9°C, it's more likely to be a mild day. Therefore, you may need a coat, but it's not extreme.", "retry_text": "Based on the current weather conditions in Reykjavik (9°C, overcast, wind at 22 km/h, and humidity at 81%), it is not extremely cold, but the weather is still variable. While it's not expected to drop below freezing, the chances of rain or snow are present. A coat might be necessary, but it's not extreme.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "6200c8ed5ffeafea", "model": "qwen3-0.6b", "prompt_class": "post_tool", "prompt": "Is that number even or odd?", "text": "The 40th Fibonacci number is **even**. This is because the Fibonacci sequence alternates between odd and even starting from 0 and 1, and since 40 is even, the 40th term is even.", "retry_text": "The 40th Fibonacci number is **odd**.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "ece827001e0f21af", "model": "qwen3-0.6b", "prompt_class": "post_tool", "prompt": "Is that number even or odd?", "text": "The 40th Fibonacci number is **even**. \n\n**Answer:** Even.", "retry_text": "The 40th Fibonacci number is **even**. The number is **102334155**.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "807e08728fab0668", "model": "qwen3-0.6b", "prompt_class": "post_tool", "prompt": "Is that number even or odd?", "text": "The 40th Fibonacci number is **odd**.", "retry_text": "The 40th Fibonacci number is **odd**.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "eb5098205f6aa11a", "model": "qwen3-0.6b", "prompt_class": "post_tool", "prompt": "Is that number even or odd?", "text": "The 40th Fibonacci number is **even**.", "retry_text": "The 40th Fibonacci number is **102334155**, which is **odd**.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "03f360a5c461d1df", "model": "qwen3-0.6b", "prompt_class": "post_tool", "prompt": "Is that number even or odd?", "text": "The 40th Fibonacci number is **odd**.", "retry_text": "The 40th Fibonacci number is **odd**.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "bd836b267afae418", "model": "qwen3-0.6b", "prompt_class": "post_tool", "prompt": "Is that number even or odd?", "text": "The 40th Fibonacci number is **even**. This is because the Fibonacci sequence modulo 2 cycles every 3 terms: 0, 1, 0, 1, 0, 1, etc. Since 40 is divisible by 3, the 40th term is congruent to 0 modulo 2.", "retry_text": "The 40th Fibonacci number is **even**. This is because the Fibonacci sequence modulo 2 cycles every 3 terms: 0, 1, 0, 1, 0, 1, ..., and since 40 is divisible by 3, the 40th term is congruent to 0 modulo 2.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "cb1b4451cfac3721", "model": "qwen3-0.6b", "prompt_class": "post_tool", "prompt": "Is that number even or odd?", "text": "The 40th Fibonacci number is even.", "retry_text": "The 40th Fibonacci number is odd.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "da47f38255c75c0b", "model": "qwen3-0.6b", "prompt_class": "post_tool", "prompt": "Is that number even or odd?", "text": "The 40th Fibonacci number is **odd**.", "retry_text": "The 40th Fibonacci number is **even**.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "5c67b8896031c82e", "model": "qwen3-0.6b", "prompt_class": "post_tool", "prompt": "How does that compare with last month?", "text": "The current price of Bitcoin is $61,204.33 as of July 2026. However, without access to the last month's price, I cannot provide a direct comparison. If you have the last month's price or any specific reference, I can help you analyze it.", "retry_text": "The current price of Bitcoin (BTC/USD) is $61,204.33 as of July 2026. Without access to last month's data, I cannot provide a direct comparison. If you have the last month's price or any specific reference, I can help analyze it.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "400ef3f65d82b485", "model": "qwen3-0.6b", "prompt_class": "post_tool", "prompt": "How does that compare with last month?", "text": "The current price of Bitcoin (BTC/USD) is **61,204.33**, as of July 29, 2026, at 3:00 UTC. This represents a **1.2% decline** compared to the day's price. However, without historical data, I cannot provide a direct comparison with last month's price. If you have access to historical Bitcoin price data, I can help analyze the trend.", "retry_text": "The current price of Bitcoin (BTC/USD) is **61,204.33**, as of July 29, 2026, at 3:00 UTC. This represents a **1.2% decline** compared to the day's price. To compare with last month, historical Bitcoin price data would be required, as this information is not accessible through standard means.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "1c609305d97bd2f1", "model": "qwen3-0.6b", "prompt_class": "post_tool", "prompt": "How does that compare with last month?", "text": "As of July 29, 2026, Bitcoin (BTC/USD) has declined by approximately 1.2% compared to the same day in the previous month. This indicates a downward trend in the market price.", "retry_text": "The current price of Bitcoin (BTC/USD) as of July 29, 2026, is $61,204.33, reflecting a 1.2% decline from the same day in the previous month. This indicates a downward trend in the market price.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "0e3e17ceee0537ad", "model": "qwen3-0.6b", "prompt_class": "post_tool", "prompt": "How does that compare with last month?", "text": "The current price of Bitcoin (BTC/USD) is $61,204.33 as of July 29, 2026, down 1.2% on the day. If you're comparing to last month's price, the exact percentage change would depend on the data available for that period. Since the tool response only includes the current price, I cannot provide a direct comparison to last month unless additional historical data is available.", "retry_text": "The current Bitcoin price (BTC/USD) is $61,204.33 as of July 29, 2026, down 1.2% on the day. To compare with last month's price, historical data would be required, which is not currently accessible through the provided tools.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "c45bec6121fa628e", "model": "qwen3-0.6b", "prompt_class": "post_tool", "prompt": "How does that compare with last month?", "text": "The current price of Bitcoin (BTC/USD) is approximately $61,204.33 as of July 29, 2026, which represents a 1.2% decline compared to the previous month. This indicates a sustained decrease in the cryptocurrency's value over the past month.", "retry_text": "The current price of Bitcoin (BTC/USD) is approximately $61,204.33 as of July 2026, representing a 1.2% decline compared to the previous month.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "4068ff179b618f8d", "model": "qwen3-0.6b", "prompt_class": "post_tool", "prompt": "Round that to two decimals and explain what it means.", "text": "The standard deviation of the numbers 4, 8, 15, 16, 23, and 42 is approximately **12.30**. This indicates the spread of the data around the mean (18) is about **12.3 units**, meaning most of the numbers cluster closely within this range. A standard deviation of 12.30 suggests the data is relatively tightly distributed around the average. \n\n**Explanation:** \nThe standard deviation measures how spread out the numbers are from the mean. A lower standard deviation means the numbers are closer to the mean, while a higher one indicates more variability. In this case, the numbers are fairly clustered around 18, resulting in a standard deviation of approximately 12.30.", "retry_text": "The standard deviation of the numbers 4, 8, 15, 16, 23, and 42 is approximately **12.30**. This indicates the spread of the data around the mean (18) is about **12.3 units**, meaning most of the numbers cluster closely within this range. A standard deviation of 12.30 suggests the data is relatively tightly distributed around the average.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "e6418b747ea08467", "model": "qwen3-0.6b", "prompt_class": "post_tool", "prompt": "Round that to two decimals and explain what it means.", "text": "The standard deviation of the numbers 4, 8, 15, 16, 23, and 42 is approximately **12.32** when rounded to two decimal places. This value indicates the spread of the data points around the mean, with a smaller standard deviation suggesting the numbers are closer to the mean.\n\n**Explanation:** \nThe standard deviation measures how much variation exists in the dataset. Here, the value of 12.32 indicates that the numbers are relatively spread out from the mean (18).", "retry_text": "The standard deviation of the numbers 4, 8, 15, 16, 23, and 42 is approximately **12.32** when rounded to two decimal places. This value indicates the spread of the data points around the mean, with a smaller standard deviation suggesting the numbers are closer to the mean.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "3a2aa2e024c6d40b", "model": "qwen3-0.6b", "prompt_class": "post_tool", "prompt": "Round that to two decimals and explain what it means.", "text": "The standard deviation of the numbers 4, 8, 15, 16, 23, and 42 is approximately 12.33 (rounded to two decimal places). This value indicates how spread out the numbers are from the mean (18). A smaller standard deviation means the data points are closer to the mean, while a larger one suggests greater variability. \n\n**Explanation:** \n- **Mean (μ):** 18 \n- **Standard Deviation (σ):** √(910/6) ≈ 12.33 \n- **Interpretation:** The data points are relatively close to the mean, with the standard deviation measuring the average distance from the mean.", "retry_text": "The standard deviation of the numbers 4, 8, 15, 16, 23, and 42 is approximately 12.33 (rounded to two decimal places). This value indicates how spread out the numbers are from the mean (18). A smaller standard deviation means the data points are closer to the mean, while a larger one suggests greater variability. \n\n**Explanation:** \n- **Mean (μ):** 18 \n- **Standard Deviation (σ):** √(910/6) ≈ 12.33 \n- **Interpretation:** The data points are relatively close to the mean, with the standard deviation measuring the average distance from the mean.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "5a730decea125659", "model": "qwen3-0.6b", "prompt_class": "post_tool", "prompt": "Round that to two decimals and explain what it means.", "text": "The standard deviation of the numbers 4, 8, 15, 16, 23, and 42 is approximately **11.55** when rounded to two decimal places. \n\n**Explanation:** \nStandard deviation measures how spread out the numbers are from the mean (18). A smaller standard deviation indicates the data points are closer to the mean, while a larger one suggests greater variability. In this case, the standard deviation of 11.55 means the numbers 4, 8, 15, 16, 23, and 42 are relatively close to the average value of 18.", "retry_text": "The standard deviation of the numbers 4, 8, 15, 16, 23, and 42 is approximately **11.55** when rounded to two decimal places. \n\n**Explanation:** \nStandard deviation measures the spread of the data points around the mean. A smaller value indicates the data points are closer to the mean (18), while a larger value suggests greater variability. In this case, the standard deviation of 11.55 means the numbers are relatively close to the average value of 18.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "223292e4c1cdde90", "model": "qwen3-1.7b", "prompt_class": "needs_tool_python", "prompt": "Sort this list and tell me the median: 8, 3, 91, 12, 7, 44, 2.", "text": "The sorted list is [2, 3, 7, 8, 12, 44, 91]. The median is the middle number, which is **8**.\n\n\nMedian: 8\n", "retry_text": "The list is already sorted: [2, 3, 7, 8, 12, 44, 91]. The median is the middle number, which is **8**.\n\n\nMedian: 8\n", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "a3bcca6020144587", "model": "qwen3-1.7b", "prompt_class": "needs_tool_python", "prompt": "Sort this list and tell me the median: 8, 3, 91, 12, 7, 44, 2.", "text": "The sorted list is [2, 3, 7, 8, 12, 44, 91]. The median is the middle number, which is **8**. \n\n\nSorted list: [2, 3, 7, 8, 12, 44, 91]\nMedian: 8\n", "retry_text": "The list is already sorted: [2, 3, 7, 8, 12, 44, 91]. The median is the middle number, which is **8**.\n\n\nSorted list: [2, 3, 7, 8, 12, 44, 91]\nMedian: 8\n", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "c84f38ec948fd858", "model": "qwen3-1.7b", "prompt_class": "needs_tool_python", "prompt": "Sort this list and tell me the median: 8, 3, 91, 12, 7, 44, 2.", "text": "The sorted list is: 2, 3, 7, 8, 12, 44, 91. The median is the middle value, which is **8**. \n\n\nThe median is 8.\n", "retry_text": "The list is already sorted: 2, 3, 7, 8, 12, 44, 91. The median is the middle value, which is **8**.\n\n\nThe median is 8.\n", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "73284425422443e2", "model": "qwen3-1.7b", "prompt_class": "needs_tool_python", "prompt": "Sort this list and tell me the median: 8, 3, 91, 12, 7, 44, 2.", "text": "The sorted list is: 2, 3, 7, 8, 12, 44, 91. The median is the middle value, which is **8**. \n\n\nThe median is 8.\n", "retry_text": "The list is already sorted: 2, 3, 7, 8, 12, 44, 91. The median is the middle value, which is **8**.\n\n\nThe median is 8.\n", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "f73fe5af0fc51b72", "model": "qwen3-1.7b", "prompt_class": "needs_tool_python", "prompt": "Sort this list and tell me the median: 8, 3, 91, 12, 7, 44, 2.", "text": "The sorted list is [2, 3, 7, 8, 12, 44, 91]. The median is the middle number, which is **8**. \n\n\nMedian: 8\n", "retry_text": "The list is already sorted: [2, 3, 7, 8, 12, 44, 91]. The median is the middle number, which is **8**.\n\n\nMedian: 8\n", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "17ec7a02b3423790", "model": "qwen3-1.7b", "prompt_class": "no_tool", "prompt": "Write a haiku about compilers.", "text": ".Compiler transforms code, logic in mind. \nParsing syntax, semantics guide. \nEfficient code, clear path.", "retry_text": "The user asked for a haiku about compilers. The previous response provided a haiku, so no tool is needed. I will provide the final answer in the requested format.\n\nFinal answer:\n.Compiler transforms code, logic in mind. \nParsing syntax, semantics guide. \nEfficient code, clear path.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "f068c4d5fba14d32", "model": "qwen3-1.7b", "prompt_class": "no_tool", "prompt": "In plain English, what is a transformer model?", "text": "A transformer model is a type of neural network designed to process and understand sequences of text (like sentences or paragraphs) by focusing on the most relevant parts of the input. Here's a simple breakdown:\n\n1. **Self-Attention**: The model can \"focus\" on specific parts of the input when processing each word. For example, if you're looking at a sentence like \"The quick brown fox jumps over the lazy dog,\" the model can remember which words are nearby or important to the current word being analyzed.\n\n2. **Positional Encodings**: Since the model doesn't know the order of words (like \"first\" or \"last\"), it uses special numbers (positionals) to tell it where each word is in the sequence.\n\n3. **Efficiency**: Unlike older models (like RNNs), transformers can handle long sequences efficiently by focusing on the most relevant parts, avoiding the vanishing gradient problem.\n\n**Why it works**: Transformers are great for tasks like language translation, text generation, and answering questions because they can understand context and relationships between words better than previous models. They form the basis of many modern NLP systems! 🧠", "retry_text": "A transformer model is a type of neural network used in natural language processing (NLP) to understand and generate human language. Here's a simplified explanation:\n\n- **Self-Attention**: It focuses on the most relevant parts of the input when processing each word. For example, when analyzing a sentence, it remembers which words are important for the current word being considered.\n- **Positional Encodings**: These are numbers added to the input to indicate the position of each word in the sequence (like \"first,\" \"second,\" etc.).\n- **Efficiency**: It handles long sequences well by focusing on key parts, avoiding the vanishing gradient problem seen in older models.\n\nTransformers are used for tasks like translation, text generation, and answering questions because they can understand context and relationships between words better than previous models. They form the basis of many modern NLP systems! 🧠", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "8f5266d898fed7f2", "model": "qwen3-1.7b", "prompt_class": "no_tool", "prompt": "In plain English, what is a transformer model?", "text": "A transformer model is a type of neural network design used in natural language processing (NLP) tasks like language translation, text generation, and more. Here's a simple breakdown:\n\n1. **Attention Mechanism**: It helps the model focus on the most relevant parts of the input (like words in a sentence) when making decisions. For example, when understanding a sentence, it can \"notice\" important words or phrases.\n\n2. **Efficiency**: Unlike older models (like RNNs), transformers can handle long sentences and complex relationships between words efficiently.\n\n3. **Applications**: They power tools like chatbots, language models (e.g., GPT), and translation services. They're great at understanding context and generating human-like text.\n\nIn short, transformers are like a \"brain\" that processes text by focusing on what's important, making them powerful for NLP tasks.", "retry_text": "A transformer model is a type of neural network designed to process and understand natural language. It uses an attention mechanism to focus on relevant parts of the input, making it efficient for tasks like language translation, text generation, and chatbots. It's widely used in AI systems like GPT and are great for handling complex language tasks by emphasizing context and relationships between words.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "c365a900cdb8a563", "model": "qwen3-1.7b", "prompt_class": "no_tool", "prompt": "Is Python interpreted or compiled?", "text": "Python is an interpreted language. This means that the code is executed line by line by an interpreter, rather than being compiled into machine code upfront. This allows for easier debugging and dynamic execution, but also means that Python is generally slower than compiled languages like C or C++.", "retry_text": "Python is an interpreted language. This means that the code is executed line by line by an interpreter, rather than being compiled into machine code upfront. This allows for easier debugging and dynamic execution, but also means that Python is generally slower than compiled languages like C or C++.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "1bd67f235ab60f74", "model": "qwen3-1.7b", "prompt_class": "no_tool", "prompt": "Is Python interpreted or compiled?", "text": "Python is an interpreted language. It is executed line by line by an interpreter, rather than being compiled into machine code. This means that Python code is processed directly by the interpreter during runtime.", "retry_text": "Python is an interpreted language. It is executed line by line by an interpreter, rather than being compiled into machine code. This means that Python code is processed directly by the interpreter during runtime.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "584878578d2b2182", "model": "qwen3-1.7b", "prompt_class": "no_tool", "prompt": "Is Python interpreted or compiled?", "text": "Python is an interpreted language. The Python interpreter reads and executes the code line by line, rather than compiling it into machine code. This means the code is executed in a virtual machine that processes the instructions as they are read, allowing for dynamic interaction and easy debugging.", "retry_text": "Python is an interpreted language. The Python interpreter reads and executes the code line by line, rather than compiling it into machine code. This means the code is executed in a virtual machine that processes the instructions as they are read, allowing for dynamic interaction and easy debugging.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "a3d4682f16eaddab", "model": "qwen3-1.7b", "prompt_class": "no_tool", "prompt": "Is Python interpreted or compiled?", "text": "Python is an interpreted language. This means that Python code is executed line by line by an interpreter, rather than being compiled into machine code beforehand. The Python interpreter reads and executes the code as it is written, which allows for dynamic interactions and scripting.", "retry_text": "Python is an interpreted language. This means that Python code is executed line by line by an interpreter, rather than being compiled into machine code beforehand. The Python interpreter reads and executes the code as it is written, which allows for dynamic interactions and scripting.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "6d967cd67d1d948e", "model": "qwen3-1.7b", "prompt_class": "no_tool", "prompt": "Is Python interpreted or compiled?", "text": "Python is an interpreted language. This means that the code is executed line by line by an interpreter, rather than being compiled into machine code upfront. While Python does have a compiler (called `pycompile`) that converts modules into byte code, the primary execution model for Python is interpreted.", "retry_text": "Python is an interpreted language. This means that the code is executed line by line by an interpreter, rather than being compiled into machine code upfront. While Python does have a compiler (called `pycompile`) that converts modules into byte code, the primary execution model for Python is interpreted.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "ea41a52096271b9d", "model": "qwen3-1.7b", "prompt_class": "no_tool", "prompt": "Is Python interpreted or compiled?", "text": "Python is an interpreted language. This means that the code is executed line by line by an interpreter, rather than being compiled into machine code beforehand.", "retry_text": "Python is an interpreted language. This means that the code is executed line by line by an interpreter, rather than being compiled into machine code beforehand.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "623ec9b699f31561", "model": "qwen3-1.7b", "prompt_class": "no_tool", "prompt": "Is Python interpreted or compiled?", "text": "Python is an interpreted language. This means that Python code is executed by an interpreter rather than being compiled into machine code. The interpreter reads and executes the code line by line, allowing for dynamic testing and development. Python's design as a high-level language makes it easier to read and write, as it abstracts away many low-level details of the underlying hardware.", "retry_text": "Python is an interpreted language. This means that Python code is executed by an interpreter rather than being compiled into machine code. The interpreter reads and executes the code line by line, allowing for dynamic testing and development. Python's design as a high-level language makes it easier to read and write, as it abstracts away many low-level details of the underlying hardware.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "969f400c2f5600da", "model": "qwen3-1.7b", "prompt_class": "no_tool", "prompt": "Is Python interpreted or compiled?", "text": "Python is an interpreted language. This means that the code is executed line by line by an interpreter, rather than being compiled into machine code beforehand.", "retry_text": "Python is an interpreted language. This means that the code is executed line by line by an interpreter, rather than being compiled into machine code beforehand.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "b8f3db6ec1773313", "model": "qwen3-1.7b", "prompt_class": "no_tool", "prompt": "What colour do you get mixing blue and yellow?", "text": "The color obtained by mixing blue and yellow depends on the context:\n\n1. **In paint (subtractive color mixing):** Blue (cyan) and yellow (magenta) mix to create **green**.\n2. **In light (additive color mixing):** Blue (cyan) and yellow (green) mix to create **orange**.\n\nSince the question is ambiguous, the most commonly accepted answer is **green** in paint, but **orange** in light. \n\nIf you need a specific context, please clarify!", "retry_text": "The color obtained by mixing blue and yellow depends on the context:\n\n1. **In paint (subtractive color mixing):** Blue (cyan) and yellow (magenta) mix to create **green**.\n2. **In light (additive color mixing):** Blue (cyan) and yellow (green) mix to create **orange**.\n\nIf you're referring to a specific context (e.g., paint, light, or digital displays), let me know! 🌈", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "b127285809237ac1", "model": "qwen3-1.7b", "prompt_class": "no_tool", "prompt": "What colour do you get mixing blue and yellow?", "text": "The color obtained by mixing blue and yellow depends on the context:\n\n1. **In the RYB color model (paint pigments):** \n Blue and yellow are primary colors. Mixing them produces **orange**.\n\n2. **In the RGB color model (light):** \n Blue (B) and yellow (G) light mix to create **white**.\n\n3. **In the visible spectrum:** \n Blue and yellow light combined produce **white** when illuminated together.\n\nIf the question refers to traditional art pigments, the answer is **orange**. If it's about light, the answer is **white**. The ambiguity arises from the model used. However, the most commonly expected answer in art contexts is **orange**.", "retry_text": "The question of mixing blue and yellow color depends on the context. In the RYB color model (paint pigments), blue and yellow are primary colors and mix to produce **orange**. In the RGB color model (light), blue and yellow light mix to produce **white**. \n\nSince the question does not specify the context, the most commonly expected answer in art contexts is **orange**. \n\nFinal answer: **orange**.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "ffe5a3ae48f4ce0b", "model": "qwen3-1.7b", "prompt_class": "format", "prompt": "What is your plan for answering a question you are unsure about?", "text": "I can't directly access external information or perform real-time searches. However, I can help you explore answers using the web_search function if you provide specific questions or topics. For example, if you have a question like \"What is the capital of France?\", I can help you find the answer by searching the web. Let me know your question, and I'll assist you!", "retry_text": "If you have a specific question or topic you'd like me to explore, I can use the `web_search` tool to find relevant information. For example, if you ask \"What is the capital of France?\", I'll search the web to retrieve the answer. Let me know your question, and I'll assist you!", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "dc87d6e06761c000", "model": "qwen3-1.7b", "prompt_class": "format", "prompt": "What is your plan for answering a question you are unsure about?", "text": "When answering a question I'm unsure about, I follow this plan:\n1. **Clarify the question** by asking for more details if needed\n2. **Use web_search** to look up information and find relevant sources\n3. **Analyze the results** to identify key facts and patterns\n4. **Cross-verify information** from multiple sources\n5. **Use Python** for complex data analysis or pattern recognition if applicable\n\nIf you have a specific question, feel free to ask, and I'll use these tools to find the best answer.", "retry_text": "I can use the web_search function to look up information and find relevant sources. Let me search for the information you need.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "ddbb4d2297d7ec7d", "model": "qwen3-1.7b", "prompt_class": "format", "prompt": "What is your plan for answering a question you are unsure about?", "text": "To address questions I'm unsure about, I would follow this plan:\n1. **Clarify the question**: Break down the query into components to better understand the context.\n2. **Use web search**: Retrieve information from credible sources to gather accurate details.\n3. **Analyze findings**: Evaluate the search results to identify the most relevant and reliable information.\n4. **Formulate an answer**: Synthesize the key points from the research into a clear, concise response.\n5. **Verify accuracy**: Cross-check information with multiple sources if necessary.\n\nFor example, if I'm unsure about a specific topic, I'd search for authoritative articles, academic papers, or expert opinions to build a well-informed answer. This ensures my responses are reliable and precise.", "retry_text": "I'm unsure about the specific question you're asking, but I can help you clarify it. Could you please provide more details or rephrase your question? This will help me better assist you.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "bfbc05ae5d07a9b5", "model": "qwen3-1.7b", "prompt_class": "format", "prompt": "What is your plan for answering a question you are unsure about?", "text": "When I'm unsure about an answer, I follow this plan:\n\n1. **Assess the Question**: I first analyze the question to understand its core requirements and identify any ambiguity or missing details.\n\n2. **Use Existing Knowledge**: I rely on my training data and logical reasoning to provide the most accurate response based on the information I have.\n\n3. **Check for Consistency**: I ensure the answer aligns with established facts, principles, and logical consistency.\n\n4. **Seek Clarification**: If uncertainty persists, I ask for additional information or context to provide a more precise answer.\n\n5. **Maintain Transparency**: If I can't fully resolve the uncertainty, I clearly state the limitations of my knowledge and suggest further research or consultation.\n\nThis approach balances accuracy with honesty, ensuring I provide the best possible answer while acknowledging my constraints.", "retry_text": "I am capable of using the provided tools to assist with the user's request. If a tool is needed, I will call web_search or python accordingly. Otherwise, I will provide the final answer as requested. Let me know if you need me to use any of the tools.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "cdfd816aaf2077d7", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "Can you look into that for me?", "text": "I'd be happy to help! Could you please clarify what you're asking me to look into? What specific topic, problem, or task do you want me to investigate?", "retry_text": "I need more details to assist you. Could you please specify what you'd like me to look into? For example, a topic, problem, or task?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "e9e0b44c1d61dc72", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "Can you look into that for me?", "text": "I'm not sure what you're referring to. Could you please provide more details or specify what you need help with? I'll do my best to assist you.", "retry_text": "I'm not sure what you're referring to. Could you please provide more details or specify what you need help with? I'll do my best to assist you.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "38a52bb2619ff85c", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "Can you look into that for me?", "text": "I'm sorry, but I need more information to assist you. Could you please clarify what you'd like me to look into?", "retry_text": "I'm sorry, but you haven't provided a specific request or query to investigate. Could you please share what you'd like me to look into?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "8f765bd993c3c45f", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "Can you look into that for me?", "text": "I'm sorry, but you didn't specify what you'd like me to look into. Could you please provide more details about the topic or issue you want me to investigate?", "retry_text": "I'm sorry, but you didn't specify what you'd like me to look into. Could you please provide more details about the topic or issue you want me to investigate?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "13781dbd1f883909", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "Can you look into that for me?", "text": "I'd be happy to help! Could you please clarify what you're looking for? Are there specific questions, problems, or topics you'd like me to investigate?", "retry_text": "I'm sorry, but I need more information to assist you. Could you please clarify what you'd like me to look into?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "2178e6a9edf35fec", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "Can you look into that for me?", "text": "I need more information to help you. Could you please specify what you'd like me to look into? For example, is there a particular topic, problem, or task you need assistance with?", "retry_text": "I need more information to help you. Could you please specify what you'd like me to look into? For example, is there a particular topic, problem, or task you need assistance with?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "7bcca37b32a10ce4", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "Can you look into that for me?", "text": "I need more information to help you. Could you please clarify what you'd like me to look into?", "retry_text": "I need more information to help you. Could you please clarify what you'd like me to look into?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "2be343b8c0094259", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "Can you look into that for me?", "text": "I need more information to help you. Could you please specify what you'd like me to investigate or look into?", "retry_text": "Sure, I can help you with that. Could you please tell me what you'd like me to look into?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "2108919942f2118d", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "Fix it.", "text": "Please provide the specific issue or code you'd like me to fix. Without additional details, I can't proceed with a tool call.", "retry_text": "I'm unable to fix anything without more specific information or a clear request. Could you please provide the issue or code you'd like me to assist with?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "8e6e5da549d03eb4", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "Fix it.", "text": "I need more information to help you fix something. Could you please describe the problem you're trying to resolve or provide details about what needs fixing?", "retry_text": "I need more information to help you fix something. Could you please describe the problem you're trying to resolve or provide details about what needs fixing?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "1d8d0081b4895276", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "Fix it.", "text": "Please provide more details about what needs to be fixed. What specific issue or problem are you encountering?", "retry_text": "I'm able to use tools to assist with your requests. Could you please clarify what specific task or problem you need help with? For example, are you looking to search the web, run Python code, or perform some other action?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "ba2c4215c0fdd0ec", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "Fix it.", "text": "I need more information to help you. Could you please describe the specific issue you'd like me to fix?", "retry_text": "I need more information to help you. Could you please describe the specific issue you'd like me to fix?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "537161cacfce688c", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "Fix it.", "text": "To help you fix something, please provide more details about the issue you're encountering. What exactly needs to be fixed? Is it a code error, a website problem, or something else?", "retry_text": "I'm able to use the web_search and python tools to help with tasks. Could you please specify what you need help with? For example, are you looking to fix a code error, resolve a website issue, or something else?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "2c82480d988e7954", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "Fix it.", "text": "What exactly do you need help with? Could you please provide more details about the issue you're trying to fix?", "retry_text": "I'm able to use tools to assist with tasks. Could you please specify what you need help with? For example, are you looking for a solution to a problem, need information, or want to run some code?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "c3004a8a1e7ec09c", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "Fix it.", "text": "The query \"Fix it\" is too vague. Could you please provide more details about what needs fixing? For example:\n- Are you encountering an error message?\n- Is the code not working as expected?\n- Is there a specific website or application that's causing issues?\n- What exactly do you mean by \"fix it\"?", "retry_text": "I need more details about what you need help with. Could you please provide specific information or a more detailed description of the issue you're facing?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "cd6b7893d7b40143", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "Fix it.", "text": "I need more information to help you fix something. Could you please describe the problem you're encountering or provide details about what needs to be fixed?", "retry_text": "I need more information to help you fix something. Could you please describe the problem you're encountering or provide details about what needs to be fixed?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "34aa380746f4e492", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "Tell me about the thing we discussed.", "text": "To discuss the \"thing\" you referred to, I need more context about the topic or subject you were discussing. Could you please provide details or clarify what you meant?", "retry_text": "To provide a detailed answer, I need to know the specific topic or subject you were discussing. Could you please clarify what \"the thing\" refers to? For example, were you talking about a concept, a project, a problem, or something else?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "006053963285470e", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "Tell me about the thing we discussed.", "text": "I'm not sure which specific topic you're referring to. Could you please clarify the subject or concept you'd like me to discuss? This will help me provide the most accurate information possible.", "retry_text": "I don't have enough information about the specific topic we discussed. Could you please clarify the subject or concept you're interested in? This will help me provide the most accurate details possible.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "a7184831399b129a", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "Tell me about the thing we discussed.", "text": "I'm not sure what \"the thing\" refers to, as there's no prior conversation history provided. Could you clarify the topic or context you're interested in? If you're referring to a specific event, subject, or discussion, please provide more details so I can assist you effectively.", "retry_text": "I don't have access to prior conversation history or specific context about \"the thing\" you discussed. Could you please clarify the topic, subject, or event you're referring to? This will help me provide accurate information or assist with any specific request.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "65ba22a67b62ced3", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "Tell me about the thing we discussed.", "text": "I'm not sure what \"the thing\" refers to, as there's no prior context in the conversation. Could you please clarify what specific topic or subject you're interested in? I can then help you explore it further using the available tools.", "retry_text": "I don't have any prior context about the \"thing\" we discussed. Could you please clarify what specific topic or subject you're interested in? I can then help you explore it further using the available tools.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "8edd6c1b5018261b", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "Tell me about the thing we discussed.", "text": "To provide information about the thing we discussed, I need to know the specific topic or subject you were referring to in our previous conversation. Could you please clarify or provide more context?", "retry_text": "I'm sorry, but I don't have enough information about the specific topic or subject we discussed in our previous conversation. Could you please provide more context or clarify what you're asking about?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "0c2f575a2e4b095e", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "Tell me about the thing we discussed.", "text": "I'm unable to recall the specific topic or context you were referring to in the previous conversation. Could you please clarify what \"the thing\" refers to? This will help me provide the most accurate information.", "retry_text": "I'm unable to recall the specific topic or context you were referring to in the previous conversation. Could you please clarify what \"the thing\" refers to? This will help me provide the most accurate information.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "46b92c84dca026a7", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "Tell me about the thing we discussed.", "text": "The thing we discussed was the functions available for assistance. Specifically, I explained the two tools: \n1. **`web_search`**: For fetching information from the web. \n2. **`python`**: For executing Python code in a sandboxed environment. \n\nLet me know if you'd like details about how to use these tools!", "retry_text": "We discussed the available functions I can use to assist with tasks. The two key tools are:\n\n1. **`web_search`**: For searching the web and retrieving information from specific URLs. \n2. **`python`**: For executing Python code in a sandboxed environment. \n\nLet me know if you'd like to explore how these tools can be used for specific tasks!", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "50f05d0ac9acbf00", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "Tell me about the thing we discussed.", "text": "I'm not sure which \"thing\" you're referring to—could you please clarify the topic or context you're interested in? I'll do my best to provide information about it.", "retry_text": "I'd like to help you with that. Could you please specify the \"thing\" you're interested in? For example, are you looking for information about a specific topic, event, or concept? This will help me provide the most accurate and useful information.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "e129c7a51dda21f9", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "Is it better?", "text": "To provide a meaningful answer, please clarify what you're comparing or asking about. For example, are you asking if a specific technology is better, a method is more efficient, or something else? Let me know!", "retry_text": "I'm able to use the tools provided to assist with your request. Could you please clarify what you're asking? For example, are you looking for information about a specific topic, comparing two options, or solving a particular problem? Let me know and I'll do my best to help!", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "1a6853e1909da911", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "Is it better?", "text": "To determine if something is \"better,\" I need more context. Could you please clarify what you're comparing (e.g., products, services, decisions, technologies, etc.)?", "retry_text": "I'm currently able to use the provided tools to assist with your requests. If you have a specific question or need help with something, feel free to ask!", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "98038d57e4ada26d", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "Is it better?", "text": "To determine if something is better, I need more context. Could you please clarify what you're comparing (e.g., decisions, actions, products, services, etc.)?", "retry_text": "I need more information to determine if something is better. Could you please clarify what you're comparing (e.g., decisions, actions, products, services, etc.)?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "61ef3e5dac66b7ad", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "Is it better?", "text": "To determine if something is \"better,\" I need more context. Could you clarify what you're comparing (e.g., methods, products, decisions, etc.)? For example: \n- Are you asking if a specific approach is better than another? \n- Or if a particular outcome is better than another? \n\nLet me know!", "retry_text": "To determine if something is \"better,\" I need more context. Could you clarify what you're comparing (e.g., methods, products, decisions, etc.)? For example: \n- Are you asking if a specific approach is better than another? \n- Or if a particular outcome is better than another? \n\nLet me know!", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "57d2aa31f7991c51", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "Is it better?", "text": "To determine if something is better, I need more context. Could you please clarify what \"it\" refers to and what specific comparison you're making? For example: \n- Are you comparing two products/services?\n- Are you evaluating a method or decision?\n- Or something else?\n\nLet me know and I'll help analyze!", "retry_text": "To determine if something is better, I need more context. Could you please clarify what \"it\" refers to and what specific comparison you're making? For example: \n- Are you comparing two products/services?\n- Are you evaluating a method or decision?\n- Or something else?\n\nLet me know and I'll help analyze!", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "a606a1bd54feada7", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "Is it better?", "text": "To determine if something is better, I need more context about what \"it\" refers to. Could you please specify the subject you're asking about (e.g., a product, method, decision, etc.)?", "retry_text": "I'm able to use the available tools to assist with your query. Could you please provide more details about what you're asking? For example, are you looking for information on a specific topic, a solution to a problem, or something else?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "ad1b5ffc53cc9267", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "Is it better?", "text": "To determine if something is \"better,\" I need more context. Could you please specify what you're asking about (e.g., a product, method, decision, or idea)? I'll do my best to help!", "retry_text": "I'm enabled to use tools to assist with your requests. Could you please clarify what you're asking about? For example, are you looking for information, a solution, or a specific action? Let me know!", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "7b8b6124fa977733", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "Is it better?", "text": "The question \"Is it better?\" is too vague without additional context. Could you please specify what you're comparing (e.g., products, methods, technologies, ideas, etc.)? I'd be happy to help!", "retry_text": "I'm able to use the available tools to assist with your request. Could you please provide more details or specify what you're asking about so I can help you effectively?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "58828ab2bc2487aa", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "What should I do next?", "text": "To help you next, please provide more details about what you're trying to accomplish or what specific question you have. This will allow me to give you accurate guidance.", "retry_text": "I'm ready to help you! Please let me know what you need assistance with.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "0806417d3e1c3c9a", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "What should I do next?", "text": "To provide helpful guidance, I need more context about what you're trying to accomplish. Could you please share more details about the situation or topic you're interested in? For example: \n- Are you asking about a specific task or problem? \n- What kind of decision are you making? \n- What are you trying to achieve? \n\nThis will help me tailor my response effectively.", "retry_text": "To provide helpful guidance, I need more details about what you're trying to accomplish. Could you please share more information about the situation or the specific question you're asking? For example: \n- Are you seeking advice on a particular topic? \n- What problem are you facing? \n- What are you trying to achieve? \n\nThis will help me tailor my response effectively.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "c16d91dc2be2198e", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "What should I do next?", "text": "To provide you with the best advice, please clarify the context of your question. Could you share more details about what you're asking for? For example, are you looking for help with a specific task, solving a problem, or need guidance on a particular topic?", "retry_text": "I'm able to help you with various tasks, but I need more specific information about what you're looking for. Could you please provide more details or clarify your question?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "f2060473624b583f", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "What should I do next?", "text": "To provide a helpful answer, please clarify the specific question or context you'd like assistance with. For example, are you looking for advice on a particular topic, needing help with a task, or wanting information on a specific subject?", "retry_text": "I'm sorry, but I don't have the capability to determine what the user should do next. Could you please provide more context or clarify your question?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "731b42891b7f4c36", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "What should I do next?", "text": "To provide a helpful response, I need to know what specific task or problem you're facing. Could you please share more details about what you'd like to accomplish or ask? For example, are you looking for information, solving a programming problem, or something else?", "retry_text": "I'm currently unable to determine what specific action you'd like to take. Could you please provide more details about what you're looking for? For example, are you trying to find information, solve a programming problem, or something else?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "cc3cfd20c226d267", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "What should I do next?", "text": "To provide a helpful answer, I need more context about what you're trying to achieve or what specific question you have. Could you please share more details about your situation?", "retry_text": "To provide a helpful answer, I need more context about what specific question or task you're referring to. Could you please share more details about what you'd like assistance with?", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "0c94f7b7a5d62902", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "What should I do next?", "text": "To provide helpful guidance, please clarify the specific task or problem you're facing. For example, are you looking for help with a programming problem, research, or something else? Sharing more details will allow me to assist better.", "retry_text": "I'm currently unable to provide a final answer without additional information. Please clarify the specific task or problem you're facing so I can assist you effectively.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "8e22c95bd049dfa2", "model": "qwen3-1.7b", "prompt_class": "ambiguous", "prompt": "What should I do next?", "text": "To provide helpful guidance, please clarify the specific question or problem you're asking. For example, are you looking for advice on a particular topic, need help with a task, or want to explore an idea? Let me know!", "retry_text": "I'm currently unable to provide a final answer without additional information. Could you please clarify what specific question or problem you're asking? This will help me best assist you.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "984ca6c8ff9b5f6c", "model": "qwen3-1.7b", "prompt_class": "non_english", "prompt": "Quelle est la capitale de la France ?", "text": "La capitale de la France est Paris.", "retry_text": "La capitale de la France est Paris.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "d41ca68a69b6c7de", "model": "qwen3-1.7b", "prompt_class": "non_english", "prompt": "Quelle est la capitale de la France ?", "text": "La capitale de la France est Paris.", "retry_text": "La capitale de la France est Paris.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "9768d2791f6b04a9", "model": "qwen3-1.7b", "prompt_class": "non_english", "prompt": "Quelle est la capitale de la France ?", "text": "La capitale de la France est Paris.", "retry_text": "La capitale de la France est Paris.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "8a739a9b7a92ec67", "model": "qwen3-1.7b", "prompt_class": "non_english", "prompt": "Quelle est la capitale de la France ?", "text": "La capitale de la France est Paris.", "retry_text": "La capitale de la France est Paris.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "f9d3c8eff0419ab5", "model": "qwen3-1.7b", "prompt_class": "non_english", "prompt": "Quelle est la capitale de la France ?", "text": "La capitale de la France est Paris.", "retry_text": "La capitale de la France est Paris.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "fb557a87ff8b5a88", "model": "qwen3-1.7b", "prompt_class": "non_english", "prompt": "Quelle est la capitale de la France ?", "text": "La capitale de la France est Paris.", "retry_text": "La capitale de la France est Paris.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "79cab97940c494d1", "model": "qwen3-1.7b", "prompt_class": "non_english", "prompt": "Was ist der Unterschied zwischen RAM und Festplatte?", "text": "RAM (Random Access Memory) und Festplatte (Hard Drive) sind beide Speicherarten, aber sie haben wichtige Unterschiede:\n\n1. **Zweck**: \n - **RAM**: Verwendet sich für **temporäre Daten**, die während des Betriebs benötigt werden (z. B. Software, Dateien, die gerade ausgeführt werden). \n - **Festplatte**: Verwendet sich für **langefristige Speicherung** (z. B. Systemdateien, Fotos, Videos, Software).\n\n2. **Speed**: \n - **RAM**: Sehr schnell (mehrere GHz), aber **verlustabhängig** (datenlos, wenn der Computer ausschaltet). \n - **Festplatte**: Langsam (meist 2-1000 MB/s), aber **verlustfrei** (daten持久).\n\n3. **Wasserdampf**: \n - **RAM**: **Volatile** (datenlos, wenn der Computer ausschaltet). \n - **Festplatte**: **Non-Volatile** (daten持久).\n\n4. **Größe**: \n - **RAM**: Klein (meist 8 GB bis 64 GB), **preislich** (jeder GB kostet mehr). \n - **Festplatte**: Groß (meist 1 TB bis 100 TB), **preisgünstiger** (jeder GB kostet weniger).\n\n5. **Verwendung**: \n - **RAM**: Für die Ausführung von Anwendungen und Prozessen. \n - **Festplatte**: Für die Speicherung von Daten und dem Betriebssystem.\n\n**Zusammenfassung**: RAM ist schnell, aber temporary; Festplatte ist langsam, aber permanent. Sie fungieren als \"Lebensmittel\" für den Computer – RAM für die \"Schnelleinheit\", Festplatte für die \"Langeinheit\".", "retry_text": "The key differences between RAM (Random Access Memory) and hard drives (Festplatte) are:\n\n1. **Purpose** \n - **RAM**: Temporary storage for active processes and data. \n - **Hard Drive**: Permanent storage for long-term data retention.\n\n2. **Speed** \n - **RAM**: Extremely fast (measured in GHz), but volatile (loses data when power is cut). \n - **Hard Drive**: Slower (typically 2-1000 MB/s), but non-volatile (retains data even without power).\n\n3. **Volatility** \n - **RAM**: Volatile (data disappears when the computer shuts down). \n - **Hard Drive**: Non-volatile (data remains even after shutdown).\n\n4. **Capacity** \n - **RAM**: Limited (typically 8-64 GB, priced per GB). \n - **Hard Drive**: Larger (1-100 TB, priced per GB).\n\n5. **Usage** \n - **RAM**: For running applications and multitasking. \n - **Hard Drive**: For storing operating systems, files, and software.\n\n**Summary**: RAM is fast but temporary, while hard drives are slow but permanent. They serve as the \"working memory\" and \"storage\" for a computer, respectively.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "da01aa1c8dcaf7c9", "model": "qwen3-1.7b", "prompt_class": "non_english", "prompt": "Was ist der Unterschied zwischen RAM und Festplatte?", "text": "Ram und Festplatte sind zwei verschiedene Arten von Speicher in einem Computer. Hier ist das grundlegende Unterschied:\n\n1. **Zweck**:\n - **RAM (Random Access Memory)**: Verwendet sich für aktive Prozesse und temporäre Daten. Sie sind schneller als Festplatten, aber begrenzt in ihrer Kapazität.\n - **Festplatte (HDD)**: Verwendet sich für langfristige Speicherung von Daten. Sie sind langsamer, aber viel größer in der Kapazität.\n\n2. **Schnelligkeit**:\n - **RAM**: Ist sehr schnell, da die Daten direkt im Computer erreichbar sind.\n - **Festplatte**: Ist langsamer, da die Daten über einen Laufwerk-Controller gespeichert werden.\n\n3. **Dauerhaftigkeit**:\n - **RAM**: Verliert die Daten, wenn der Computer ausschaltet.\n - **Festplatte**: Reträt die Daten, auch wenn der Computer ausgeschaltet ist.\n\n4. **Kapazität**:\n - **RAM**: Kleinere Kapazität (meist in GB), aber schnell.\n - **Festplatte**: Große Kapazität (in TB oder PB), aber langsamer.\n\n5. **Verwendung**:\n - **RAM**: Für das Laufen von Anwendungen und der Verarbeitung von Daten.\n - **Festplatte**: Für die langfristige Speicherung von Dateien, Systemdateien und Daten, die nicht in der RAM aufbewahrt werden.\n\n**Zusammenfassung**: RAM ist temporary und schnell, Festplatten sind permanent und großkapazitär, aber langsamer.", "retry_text": "The main differences between RAM (Random Access Memory) and HDD (Hard Disk Drive) are as follows:\n\n1. **Purpose**:\n - **RAM**: Temporary storage for active processes and data. It is used to run applications and handle data temporarily.\n - **HDD**: Permanent storage for long-term data retention, including operating system files, user data, and applications.\n\n2. **Speed**:\n - **RAM**: Extremely fast (nanoseconds), as data is directly accessible by the CPU.\n - **HDD**: Slower (milliseconds), as data is read from a spinning disk or solid-state drive (SSD).\n\n3. **Capacity**:\n - **RAM**: Limited (typically 8 GB–16 GB or more for modern systems), but fast.\n - **HDD**: Large capacity (up to several terabytes or even petabytes), but slower.\n\n4. **Durability**:\n - **RAM**: Lost when the computer powers off.\n - **HDD**: Retains data even when the computer is off.\n\n5. **Usage**:\n - **RAM**: For temporary data storage and processing.\n - **HDD**: For long-term storage of files, system files, and data.\n\nIn summary, RAM is fast but limited, while HDD is slow but offers massive storage capacity.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "55b056063beb61c3", "model": "qwen3-1.7b", "prompt_class": "non_english", "prompt": "Was ist der Unterschied zwischen RAM und Festplatte?", "text": "The main differences between RAM (Random Access Memory) and a hard drive (HDD or SSD) are:\n\n1. **Purpose** \n - **RAM**: Temporary memory for running applications and active processes. \n - **Hard Drive**: Permanent storage for storing data (files, OS, programs) even when the computer is off.\n\n2. **Speed** \n - **RAM**: Extremely fast (nanoseconds), but limited in capacity. \n - **Hard Drive**: Slower (milliseconds), but larger in capacity.\n\n3. **Volatility** \n - **RAM**: Lost when the computer turns off. \n - **Hard Drive**: Retains data permanently.\n\n4. **Capacity** \n - **RAM**: Small (e.g., 8GB, 16GB) for temporary use. \n - **Hard Drive**: Large (e.g., 1TB or more) for long-term storage.\n\n5. **Cost** \n - **RAM**: Expensive (high-speed types like DDR5 are pricey). \n - **Hard Drive**: Affordable (SSDs are cheaper than HDDs).\n\n**Key Takeaway**: RAM is like a \"workspace\" for running programs, while a hard drive is like a \"storage box\" for keeping data permanently.", "retry_text": "The main differences between RAM (Random Access Memory) and a hard drive (HDD/SSD) are as follows:\n\n1. **Purpose** \n - **RAM**: Temporary memory for running applications and active processes. \n - **Hard Drive**: Permanent storage for storing data (files, OS, programs) even when the computer is off.\n\n2. **Speed** \n - **RAM**: Extremely fast (nanoseconds), but limited in capacity. \n - **Hard Drive**: Slower (milliseconds), but larger in capacity.\n\n3. **Volatility** \n - **RAM**: Lost when the computer turns off. \n - **Hard Drive**: Retains data permanently.\n\n4. **Capacity** \n - **RAM**: Small (e.g., 8GB, 16GB) for temporary use. \n - **Hard Drive**: Large (e.g., 1TB or more) for long-term storage.\n\n5. **Cost** \n - **RAM**: Expensive (high-speed types like DDR5 are pricey). \n - **Hard Drive**: Affordable (SSDs are cheaper than HDDs).\n\n**Key Takeaway**: RAM is like a \"workspace\" for running programs, while a hard drive is like a \"storage box\" for keeping data permanently.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "3d0c9d47e01670b8", "model": "qwen3-1.7b", "prompt_class": "non_english", "prompt": "Was ist der Unterschied zwischen RAM und Festplatte?", "text": "RAM (Random Access Memory) und Festplatte (Hard Disk Drive) sind beiden Speicherarten, aber sie unterscheiden sich in mehreren Aspekten:\n\n1. **Zweck und Verwendung**:\n - **RAM**: Temporärer Speicher, verwendet, um laufende Programme und Daten zu speichern, um schnelle Zugriffe zu ermöglichen.\n - **Festplatte**: Permanenter Speicher, verwendet, um Daten (z. B. Betriebssystem, Programme, Dateien) zu speichern, unabhängig von der Stromversorgung.\n\n2. **Speed (Schnelligkeit)**:\n - **RAM**: Schneller als Festplatten (in der Größenordnung von GB/s bis TB/s).\n - **Festplatte**: Langsam (in der Größenordnung von MB/s bis GB/s).\n\n3. **Dauerhaltigkeit**:\n - **RAM**: Verliert Daten, wenn der Computer ausschaltet.\n - **Festplatte**: Retiert Daten, selbst wenn der Computer ausgeschaltet ist.\n\n4. **Kapazität**:\n - **RAM**: Geringer (meist 8 GB bis 16 GB für moderne Geräte).\n - **Festplatte**: Höher (meist 1 TB bis 100 TB).\n\n5. **Preis**:\n - **RAM**: Höherer Preis pro GB im Vergleich zu Festplatten.\n - **Festplatte**: Weniger teuer, aber größer und langlebig.\n\n**Zusammenfassung**: RAM ist für schnelle Datenverarbeitung und Programme, Festplatten für langfristige Speicherung.", "retry_text": "Der Hauptunterschied zwischen RAM und Festplatte (HDD) liegt in ihrer Funktion und Verwendung:\n\n- **RAM (Random Access Memory)**: \n - **Zweck**: Temporärer Speicher für laufende Programme und Daten. \n - **Schnelligkeit**: Schneller als Festplatten (z. B. GB/s). \n - **Dauerhaltigkeit**: Verliert Daten, wenn der Computer ausschaltet. \n - **Kapazität**: Gering (meist 8–16 GB für moderne Geräte). \n\n- **Festplatte (HDD)**: \n - **Zweck**: Langfristige Speicherung von Daten (z. B. Betriebssystem, Programme). \n - **Schnelligkeit**: Langsam (z. B. MB/s). \n - **Dauerhaltigkeit**: Retiert Daten, selbst wenn der Computer ausgeschaltet ist. \n - **Kapazität**: Höher (z. B. 1–100 TB). \n\n**Zusammenfassung**: RAM ist für schnelle Datenverarbeitung, Festplatten für langfristige Speicherung.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "7cfc2d3be5a9a3c8", "model": "qwen3-1.7b", "prompt_class": "non_english", "prompt": "Was ist der Unterschied zwischen RAM und Festplatte?", "text": "RAM (Random Access Memory) and HDD (Hard Disk Drive) are two types of storage in a computer, but they serve different purposes:\n\n1. **Function**:\n - **RAM**: Temporary storage for data and programs currently in use. It's like a \"working area\" for the CPU.\n - **HDD**: Permanent storage for files and data. It's like a \"permanent record\" for the computer.\n\n2. **Speed**:\n - **RAM**: Extremely fast (nanoseconds), as it's directly accessible by the CPU.\n - **HDD**: Slower (milliseconds), due to mechanical components (spinning disks or platters).\n\n3. **Capacity**:\n - **RAM**: Limited (typically 8GB–32GB or more, depending on the system).\n - **HDD**: Large capacity (up to several terabytes), but slower than RAM.\n\n4. **Data Persistence**:\n - **RAM**: Data is lost when the power is off.\n - **HDD**: Data remains even when the power is off.\n\n**Key Takeaway**: RAM is for \"running\" your programs, while HDD is for \"storing\" your files. RAM is fast but limited, while HDD is slow but massive in storage.", "retry_text": "The main differences between RAM (Random Access Memory) and a hard disk drive (HDD) are:\n\n1. **Purpose**:\n - **RAM**: Temporary storage for active programs and data. It's like a \"working area\" for the CPU.\n - **HDD**: Permanent storage for files and data. It's like a \"permanent record\" for the computer.\n\n2. **Speed**:\n - **RAM**: Extremely fast (nanoseconds), as it's directly accessible by the CPU.\n - **HDD**: Slower (milliseconds), due to mechanical components (spinning disks or platters).\n\n3. **Capacity**:\n - **RAM**: Limited (typically 8GB–32GB or more, depending on the system).\n - **HDD**: Large capacity (up to several terabytes), but slower than RAM.\n\n4. **Data Persistence**:\n - **RAM**: Data is lost when the power is off.\n - **HDD**: Data remains even when the power is off.\n\n**Key Takeaway**: RAM is for \"running\" your programs, while HDD is for \"storing\" your files. RAM is fast but limited, while HDD is slow but massive in storage.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "0ce4f6ef24ac1f5d", "model": "qwen3-1.7b", "prompt_class": "non_english", "prompt": "Was ist der Unterschied zwischen RAM und Festplatte?", "text": "Ram (Random Access Memory) und Festplatten (SSD) sind beiden Speicherformate, aber sie unterscheiden sich in mehreren Aspekten:\n\n1. **Verwendung**:\n - **RAM**: Verwendet sich für **temporäre Daten** (z. B. Daten, die gerade genutzt werden), um die Computerleistung zu verbessern.\n - **SSD**: Verwendet sich für **dauerhaften Speicher** (z. B. Daten, die nach dem Ausschalten noch benötigt werden).\n\n2. **Geschwindigkeit**:\n - **RAM**: Much **faster** (bis zu 3200 MHz oder höher) als SSD (typisch 7200 RPM oder 10000 RPM).\n - **SSD**: Seltener **schneller** als HDD, aber langsamer als RAM.\n\n3. **Volatilität**:\n - **RAM**: **Verliert Daten**, wenn die Stromversorgung abgeschaltet wird.\n - **SSD**: **Hält Daten**, auch nach dem Ausschalten.\n\n4. **Kosten**:\n - **RAM**: Much **preiswerter** als SSD, aber **teurer** als HDD.\n - **SSD**: **Teurer** als HDD, aber **billiger** als RAM.\n\n5. **Speicherkapazität**:\n - **RAM**: Geringer (z. B. 8 GB bis 16 GB in modernen Computern).\n - **SSD**: Höher (z. B. 1 TB bis 10 TB oder mehr).\n\n**Zusammenfassung**: \nRAM ist für **Temporärer Speicher** und **schneller**, während SSD für **Dauerhaften Speicher** und **schwerer** ist. RAM wird zur Leistungssicherung verwendet, während SSD zur Datenarchivierung eingesetzt wird.", "retry_text": "The main differences between RAM and SSD are as follows:\n\n1. **Purpose**:\n - **RAM**: Temporary storage for running applications and data, used for speed and performance.\n - **SSD**: Permanent storage for data, used for storing files and system components.\n\n2. **Speed**:\n - **RAM**: Extremely fast (e.g., 3200 MHz or higher), but slower than SSDs.\n - **SSD**: Slower than RAM but faster than HDDs.\n\n3. **Volatility**:\n - **RAM**: Lost when power is cut.\n - **SSD**: Retains data even after power loss.\n\n4. **Cost**:\n - **RAM**: More expensive than SSDs but cheaper than HDDs.\n - **SSD**: More expensive than HDDs but cheaper than RAM.\n\n5. **Capacity**:\n - **RAM**: Limited (e.g., 8-16 GB in modern PCs).\n - **SSD**: Higher capacity (e.g., 1 TB to 10 TB).\n\n**Summary**: RAM is for temporary, high-speed data, while SSD is for permanent, durable storage. RAM enhances performance, and SSDs store data persistently.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "a5e685449c85d80d", "model": "qwen3-1.7b", "prompt_class": "non_english", "prompt": "Was ist der Unterschied zwischen RAM und Festplatte?", "text": "RAM (Random Access Memory) und Festplatte (Hard Disk Drive) sind beiden Speichertechnologien, aber sie unterscheiden sich in mehreren Aspekten:\n\n1. **Verwendung**:\n - **RAM**: Verwendet sich für **temporären Speicher**, um Daten zu speichern, die aktuell genutzt werden (z. B. beim Ausführen von Anwendungen).\n - **Festplatte**: Verwendet sich für **langfristige Speicherung**, um Dateien, Fotos, Videos und andere Daten zu store.\n\n2. **Volatilität**:\n - **RAM**: Ist **verlustlos** (d.h. Daten werden nach dem Ausschalten verloren).\n - **Festplatte**: Ist **verlustlos** (d.h. Daten bleiben nach dem Ausschalten gespeichert).\n\n3. **Geschwindigkeit**:\n - **RAM**: Schneller als Festplatten, da die Daten direkt zugänglich sind.\n - **Festplatte**: Langsam, da die Daten über ein mechanisches System (HDD) oder ein elektronisches (SSD) System gespeichert werden.\n\n4. **Kapazität**:\n - **RAM**: Eingeschränkt (meist in GB oder TB), aber schnell.\n - **Festplatte**: Großer Speicher (in TB oder PB), aber langsamer.\n\n5. **Zweck**:\n - **RAM**: Für die **Laufzeitverarbeitung** (z. B. Programmierung, Spiele).\n - **Festplatte**: Für die **Langlebigkeit** (z. B. Dateien, Dokumente, Videos).\n\n**Beispiel**: Wenn du ein Spiel spielst, wird das Spiel in RAM abgelegt, um schneller zu laufen. Die gespeicherten Dateien des Spiels werden auf der Festplatte abgelegt.", "retry_text": "The main differences between RAM (Random Access Memory) and a hard drive (Festplatte) are as follows:\n\n1. **Purpose**:\n - **RAM**: Temporary storage for active processes and data to enable quick access during program execution.\n - **Hard Drive**: Long-term storage for files, documents, and data, offering persistent storage even after power loss.\n\n2. **Speed**:\n - **RAM**: Extremely fast (microseconds), as data is accessed directly by the CPU.\n - **Hard Drive**: Slower (milliseconds to seconds), relying on mechanical or solid-state technology.\n\n3. **Capacity**:\n - **RAM**: Limited (typically 8 GB–16 GB or more for modern systems), but highly volatile.\n - **Hard Drive**: Large capacity (TB–PB), designed for long-term data storage.\n\n4. **Durability**:\n - **RAM**: Volatile (data lost when power is removed).\n - **Hard Drive**: Non-volatile (data retained even without power).\n\n5. **Usage**:\n - **RAM**: For temporary data processing (e.g., running applications).\n - **Hard Drive**: For long-term storage (e.g., saving files, installing software).\n\n**Example**: When playing a game, the game data is in RAM for quick access, while the game files are stored on the hard drive.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "8ef143573be12e7e", "model": "qwen3-1.7b", "prompt_class": "non_english", "prompt": "Was ist der Unterschied zwischen RAM und Festplatte?", "text": "RAM (Random Access Memory) und Festplatte (Hard Disk) sind zwei verschiedene Speichertechnologien, die in einem Computer verwendet werden, um Daten zu speichern und zu verarbeiten. Hier ist der Hauptunterschied:\n\n1. **Speichertype**:\n - **RAM**: Vollelektrisch (voltage-controlled) und wird bei Stromversorgung verloren gehen (voltage loss). Es ist temporär und wird bei Komponentenabbau (Schaltung) gelöscht.\n - **Festplatte**: Nicht-Elektrisch (non-volatile) und speichert Daten, auch wenn der Computer ausgeschaltet ist.\n\n2. **Zweck**:\n - **RAM**: Verwendung für **temporäre Daten** (z. B. offene Anwendungen, aktuelle Prozesse), die schnell aufgerufen werden müssen.\n - **Festplatte**: Verwendung für **dauerhaftes Speichern** (z. B. Systemdateien, Dateien, Software, Bilder).\n\n3. **Leistung**:\n - **RAM**: Schneller als Festplatten (durch direkte Zugriff auf Speicheradressen).\n - **Festplatte**: Langsam, da die Daten über magnetische Platten aufgerufen werden müssen.\n\n4. **Kapazität**:\n - **RAM**: Klein (meist in GB bis MB), aber sehr schnell.\n - **Festplatte**: Groß (in TB bis PB), aber langsamer.\n\n5. **Dauerhaftigkeit**:\n - **RAM**: Verloren, wenn der Computer ausgeschaltet wird.\n - **Festplatte**: Dauerhaft, auch nach dem Abstechen.\n\n**Zusammenfassung**: RAM ist für schnelle, temporäre Daten und Festplatten für dauerhaftes Speichern. Die Festplatte ist der Hauptspeicher für das Betriebssystem und Dateien, während RAM für die aktive Ausführung von Anwendungen verwendet wird.", "retry_text": "Der Hauptunterschied zwischen RAM (Random Access Memory) und einer Festplatte (Hard Disk) liegt in ihrer Funktion und Verwendung:\n\n1. **Funktion**: \n - **RAM**: Temporärer Speicher, der Daten für aktive Prozesse benötigt (z. B. offene Anwendungen). \n - **Festplatte**: Dauerhaftes Speichern von Daten, auch nach dem Ausgeschaltet werden. \n\n2. **Speed**: \n - **RAM**: Schneller (direkter Zugriff auf Speicheradressen). \n - **Festplatte**: Langsam (magnetische Platten). \n\n3. **Dauerhaftigkeit**: \n - **RAM**: Verloren, wenn der Computer ausgeschaltet wird. \n - **Festplatte**: Dauerhaft, auch nach dem Abstechen. \n\n4. **Kapazität**: \n - **RAM**: Klein (GB bis MB), aber sehr schnell. \n - **Festplatte**: Groß (TB bis PB), aber langsamer. \n\n**Zusammenfassung**: RAM ist für schnelle, temporäre Daten und Festplatten für dauerhaftes Speichern. Die Festplatte ist der Hauptspeicher für das Betriebssystem und Dateien, während RAM für die aktive Ausführung von Anwendungen verwendet wird.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "47f321d70a93d9c7", "model": "qwen3-1.7b", "prompt_class": "post_tool", "prompt": "Do I need a coat?", "text": "Yes, you would need a coat in Reykjavik right now. The temperature is 9°C, with wind and humidity adding to the chill. A warm jacket or sweater would be advisable to stay comfortable. 🌧️", "retry_text": "Yes, you would need a coat in Reykjavik right now. The temperature is 9°C, with wind and humidity adding to the chill. A warm jacket or sweater would be advisable to stay comfortable. 🌧️", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "48c7f73caaacd00a", "model": "qwen3-1.7b", "prompt_class": "post_tool", "prompt": "Do I need a coat?", "text": "Based on the current weather in Reykjavik:\n- **Temperature**: 9°C (cold)\n- **Wind**: 22 km/h from the northeast\n- **Humidity**: 81% (moderate)\n\nYou **might need a coat** if you're going outside for extended periods. A light coat or scarf would be sufficient. The wind could make it feel colder, so consider bringing gloves or a thicker layer if you're exposed to the elements. The overcast sky and low humidity don't significantly impact comfort. 🌧️", "retry_text": "Based on the current weather in Reykjavik: \n- **Temperature**: 9°C (cold) \n- **Wind**: 22 km/h from the northeast \n- **Humidity**: 81% (moderate) \n\nYou **might need a coat** if you're going outside for extended periods. A light coat or scarf would be sufficient. The wind could make it feel colder, so consider bringing gloves or a thicker layer if you're exposed to the elements. The overcast sky and low humidity don't significantly impact comfort. 🌧️", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "f261136504d79f67", "model": "qwen3-1.7b", "prompt_class": "post_tool", "prompt": "Is that number even or odd?", "text": "The 40th Fibonacci number is **odd**. \n\n**Reasoning**: \nThe Fibonacci sequence modulo 2 follows a repeating pattern every 3 numbers: \n- Even (0), Odd (1), Odd (2), Even (3), Odd (4), Odd (5), Even (6), ... \n\nSince the 40th number corresponds to position 40, which leaves a remainder of 1 when divided by 3 (40 ÷ 3 = 13 remainder 1), it falls in the \"odd\" category. Thus, the number is **odd**.", "retry_text": "The 40th Fibonacci number is **odd**.\n\n**Reasoning**: \nThe Fibonacci sequence modulo 2 follows a repeating pattern every 3 numbers: \n- Even (0), Odd (1), Odd (2), Even (3), Odd (4), Odd (5), Even (6), ... \n\nSince the 40th number corresponds to position 40, which leaves a remainder of 1 when divided by 3 (40 ÷ 3 = 13 remainder 1), it falls in the \"odd\" category. Thus, the number is **odd**.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "a912d6b7d027d48d", "model": "qwen3-1.7b", "prompt_class": "post_tool", "prompt": "Is that number even or odd?", "text": "The 40th Fibonacci number is **odd**. \n\n**Explanation**: Fibonacci numbers follow a pattern where every third number is even (e.g., F(0) = 0, F(3) = 2, F(6) = 8, etc.). Since 40 ≡ 1 mod 3, F(40) is odd.", "retry_text": "The 40th Fibonacci number is **odd**.\n\n**Explanation**: Fibonacci numbers follow a pattern where every third number is even (e.g., F(0) = 0, F(3) = 2, F(6) = 8, etc.). Since 40 ≡ 1 mod 3, F(40) is odd.", "retry_tool_calls": 0, "retry_samples": 3} +{"id": "cdc26ed374980575", "model": "qwen3-1.7b", "prompt_class": "post_tool", "prompt": "Is that number even or odd?", "text": "The 40th Fibonacci number is **odd**. \n\n**Explanation:** \nThe Fibonacci sequence alternates between even and odd numbers, but the pattern depends on the starting index. If the sequence starts with $ F_0 = 0 $ and $ F_1 = 1 $, the parity (even/odd) of the $ n $-th term follows the pattern: \n- $ F_0 $ (even) \n- $ F_1 $ (odd) \n- $ F_2 $ (odd) \n- $ F_3 $ (even) \n- $ F_4 $ (odd) \n- $ F_5 $ (odd) \n- $ F_6 $ (even) \n- ... \n\nFor $ n = 40 $, the parity depends on whether $ n $ is a multiple of 3. Since $ 40 \\mod 3 = 1 $, the 40th term is **odd**. \n\n**Final Answer:** Odd.", "retry_text": "The 40th Fibonacci number is **odd**.\n\n**Explanation:** \nThe Fibonacci sequence alternates between even and odd numbers, but the pattern depends on the starting index. If the sequence starts with $ F_0 = 0 $ and $ F_1 = 1 $, the parity (even/odd) of the $ n $-th term follows the pattern: \n- $ F_0 $ (even) \n- $ F_1 $ (odd) \n- $ F_2 $ (odd) \n- $ F_3 $ (even) \n- $ F_4 $ (odd) \n- $ F_5 $ (odd) \n- $ F_6 $ (even) \n- ... \n\nFor $ n = 40 $, the parity depends on whether $ n $ is a multiple of 3. Since $ 40 \\mod 3 = 1 $, the 40th term is **odd**. \n\n**Final Answer:** Odd.", "retry_tool_calls": 0, "retry_samples": 3} diff --git a/studio/backend/tests/test_change_password_policy.py b/studio/backend/tests/test_change_password_policy.py index c73e9ed839..fc095760d0 100644 --- a/studio/backend/tests/test_change_password_policy.py +++ b/studio/backend/tests/test_change_password_policy.py @@ -67,9 +67,11 @@ def test_rejects_password_containing_spaces(_user): def test_allows_password_without_spaces(_user, monkeypatch): - monkeypatch.setattr(auth_routes.storage, "update_password", lambda *args, **kwargs: True) - monkeypatch.setattr(auth_routes, "create_access_token", lambda subject: "at") - monkeypatch.setattr(auth_routes, "create_refresh_token", lambda subject: "rt") + monkeypatch.setattr( + auth_routes.storage, "update_password", lambda *args, **kwargs: "rotated-secret" + ) + monkeypatch.setattr(auth_routes, "create_access_token", lambda subject, **kwargs: "at") + monkeypatch.setattr(auth_routes, "create_refresh_token", lambda subject, **kwargs: "rt") token = _change("correct-horse-battery") assert token.access_token == "at" assert token.must_change_password is False diff --git a/studio/backend/tests/test_credential_rotation_race.py b/studio/backend/tests/test_credential_rotation_race.py new file mode 100644 index 0000000000..9b0f95aa02 --- /dev/null +++ b/studio/backend/tests/test_credential_rotation_race.py @@ -0,0 +1,255 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +"""A password rotation must not leave a session minted from the replaced credential. + +`unsloth studio reset-password` rotates in place against a live server, so a login +can verify the old password, have the rotation land, and only then mint its tokens. +Issuance is bound to the credential version that was verified, so such a login gets +tokens that are already dead rather than a session that outlives the reset. +""" + +import secrets +from datetime import datetime, timedelta, timezone + +import jwt +import pytest + +from auth import hashing, storage +from auth.authentication import ALGORITHM, create_access_token, create_refresh_token + + +@pytest.fixture(autouse = True) +def isolated_auth_db(tmp_path, monkeypatch): + monkeypatch.setattr(storage, "DB_PATH", tmp_path / "auth.db") + monkeypatch.setattr(storage, "_BOOTSTRAP_PW_PATH", tmp_path / ".bootstrap_password") + monkeypatch.setattr(storage, "_bootstrap_password", None) + monkeypatch.setattr(storage, "_api_key_pbkdf2_salt_cache", None) + yield + + +@pytest.fixture +def admin(): + storage.create_initial_user( + username = storage.DEFAULT_ADMIN_USERNAME, + password = "old-password-123", + jwt_secret = secrets.token_urlsafe(64), + ) + return storage.DEFAULT_ADMIN_USERNAME + + +def _verified_secret(username): + return storage.get_user_and_secret(username)[2] + + +def test_access_token_from_the_replaced_credential_is_rejected(admin): + secret = _verified_secret(admin) + + storage.update_password(admin, "new-password-456", revoke_refresh_tokens = True) + token = create_access_token(subject = admin, secret = secret) + + with pytest.raises(jwt.InvalidTokenError): + jwt.decode(token, storage.get_jwt_secret(admin), algorithms = [ALGORITHM]) + + +def test_refresh_token_from_the_replaced_credential_is_rejected(admin): + secret = _verified_secret(admin) + + # Inserted AFTER the rotation's DELETE, so revocation alone cannot catch it. + storage.update_password(admin, "new-password-456", revoke_refresh_tokens = True) + token = create_refresh_token(subject = admin, secret = secret) + + assert storage.verify_refresh_token(token) is None + assert storage.consume_refresh_token(token) is None + + +def test_a_rejected_refresh_token_is_dropped(admin): + secret = _verified_secret(admin) + storage.update_password(admin, "new-password-456", revoke_refresh_tokens = True) + token = create_refresh_token(subject = admin, secret = secret) + + storage.verify_refresh_token(token) + + conn = storage.get_connection() + try: + assert conn.execute("SELECT COUNT(*) AS c FROM refresh_tokens").fetchone()["c"] == 0 + finally: + conn.close() + + +def test_tokens_from_the_current_credential_still_work(admin): + secret = _verified_secret(admin) + + access = create_access_token(subject = admin, secret = secret) + refresh = create_refresh_token(subject = admin, secret = secret) + + jwt.decode(access, storage.get_jwt_secret(admin), algorithms = [ALGORITHM]) + assert storage.verify_refresh_token(refresh) == (admin, False) + + +def test_refresh_cannot_outlive_a_rotation_it_raced(admin): + # /refresh consumes, then mints. A rotation landing in between must not let + # the replacement pair be signed with the credential that just replaced it. + secret = _verified_secret(admin) + token = create_refresh_token(subject = admin, secret = secret) + consumed = storage.consume_refresh_token(token) + assert consumed is not None + _username, _is_desktop, consumed_secret = consumed + + storage.update_password(admin, "new-password-456", revoke_refresh_tokens = True) + access = create_access_token(subject = admin, secret = consumed_secret) + refresh = create_refresh_token(subject = admin, secret = consumed_secret) + + with pytest.raises(jwt.InvalidTokenError): + jwt.decode(access, storage.get_jwt_secret(admin), algorithms = [ALGORITHM]) + assert storage.verify_refresh_token(refresh) is None + + +def test_desktop_login_cannot_outlive_a_rotation_it_raced(admin): + # The reset deletes the desktop secret, so a desktop-login that validated it + # just beforehand must not mint a session that survives. + raw = storage.create_desktop_secret() + verified = storage.validate_desktop_secret_with_credential(raw) + assert verified is not None + _username, verified_secret = verified + + storage.update_password(admin, "new-password-456", revoke_refresh_tokens = True) + access = create_access_token(subject = admin, desktop = True, secret = verified_secret) + refresh = create_refresh_token(subject = admin, desktop = True, secret = verified_secret) + + with pytest.raises(jwt.InvalidTokenError): + jwt.decode(access, storage.get_jwt_secret(admin), algorithms = [ALGORITHM]) + assert storage.verify_refresh_token(refresh) is None + + +def test_change_password_cannot_overwrite_a_rotation_it_raced(admin): + # A change-password that verified the old hash must not clobber a reset that + # committed while it was in flight. + _salt, verified_hash, _secret, _must_change = storage.get_user_and_secret(admin) + + storage.update_password(admin, "reset-by-the-cli-789", revoke_refresh_tokens = True) + + assert not storage.update_password( + admin, + "attacker-chosen-000", + revoke_refresh_tokens = True, + expect_password_hash = verified_hash, + ) + salt, pwd_hash, _s, _m = storage.get_user_and_secret(admin) + assert hashing.verify_password("reset-by-the-cli-789", salt, pwd_hash) + + +def test_api_key_creation_from_a_revoked_credential_is_refused(admin): + generation = storage.credential_generation(_verified_secret(admin)) + + storage.update_password(admin, "new-password-456", revoke_refresh_tokens = True) + + with pytest.raises(storage.CredentialRotated): + storage.create_api_key(username = admin, name = "k", expect_gen = generation) + conn = storage.get_connection() + try: + assert conn.execute("SELECT COUNT(*) AS c FROM api_keys").fetchone()["c"] == 0 + finally: + conn.close() + + +def test_api_key_creation_under_the_current_credential_still_works(admin): + generation = storage.credential_generation(_verified_secret(admin)) + + raw_key, _row = storage.create_api_key(username = admin, name = "k", expect_gen = generation) + + assert storage.validate_api_key(raw_key) == admin + + +def test_change_password_tokens_are_bound_to_its_own_write(admin): + # The tokens returned to a successful change-password must be signed with the + # secret that write produced, not whatever a later reset put in the DB. + _salt, verified_hash, _secret, _must = storage.get_user_and_secret(admin) + new_secret = storage.update_password( + admin, + "chosen-by-the-user", + revoke_refresh_tokens = True, + expect_password_hash = verified_hash, + ) + assert new_secret is not None + + storage.update_password(admin, "reset-by-the-cli-789", revoke_refresh_tokens = True) + access = create_access_token(subject = admin, secret = new_secret) + refresh = create_refresh_token(subject = admin, secret = new_secret) + + with pytest.raises(jwt.InvalidTokenError): + jwt.decode(access, storage.get_jwt_secret(admin), algorithms = [ALGORITHM]) + assert storage.verify_refresh_token(refresh) is None + + +def test_internal_api_key_minting_honours_the_request_generation(admin): + generation = storage.credential_generation(_verified_secret(admin)) + storage.update_password(admin, "new-password-456", revoke_refresh_tokens = True) + + with pytest.raises(storage.CredentialRotated): + storage.create_api_key( + username = admin, + name = "data-recipe workflow", + internal = True, + expect_gen = generation, + ) + + +def test_api_key_auth_reports_the_version_the_key_was_valid_under(admin): + # The generation must come from the same transaction as the key check, or a + # revoked key could hand a route the post-reset generation and mint again. + raw, _row = storage.create_api_key(username = admin, name = "agent") + verified = storage.validate_api_key_with_credential(raw) + assert verified is not None + _user, secret = verified + generation = storage.credential_generation(secret) + + storage.update_password(admin, "new-password-456", revoke_refresh_tokens = True) + conn = storage.get_connection() + try: + conn.execute("DELETE FROM api_keys") + conn.commit() + finally: + conn.close() + + assert storage.validate_api_key(raw) is None + with pytest.raises(storage.CredentialRotated): + storage.create_api_key(username = admin, name = "after", expect_gen = generation) + + +def test_consuming_a_legacy_token_reports_the_pre_reset_credential(admin): + # An unstamped row has no generation to compare, so consume must read the + # credential inside the delete transaction rather than after committing it. + token = secrets.token_urlsafe(48) + expires_at = (datetime.now(timezone.utc) + timedelta(days = 7)).isoformat() + storage.save_refresh_token(token, admin, expires_at, secret_gen = None) + conn = storage.get_connection() + try: + conn.execute("UPDATE refresh_tokens SET secret_gen = NULL") + conn.commit() + finally: + conn.close() + + consumed = storage.consume_refresh_token(token) + assert consumed is not None + _username, _is_desktop, consumed_secret = consumed + + storage.update_password(admin, "new-password-456", revoke_refresh_tokens = True) + access = create_access_token(subject = admin, secret = consumed_secret) + with pytest.raises(jwt.InvalidTokenError): + jwt.decode(access, storage.get_jwt_secret(admin), algorithms = [ALGORITHM]) + + +def test_unstamped_legacy_tokens_still_verify(admin): + # Rows written before the secret_gen column existed must not log users out. + token = secrets.token_urlsafe(48) + expires_at = (datetime.now(timezone.utc) + timedelta(days = 7)).isoformat() + storage.save_refresh_token(token, admin, expires_at, secret_gen = None) + conn = storage.get_connection() + try: + conn.execute("UPDATE refresh_tokens SET secret_gen = NULL") + conn.commit() + finally: + conn.close() + + assert storage.verify_refresh_token(token) == (admin, False) diff --git a/studio/backend/tests/test_desktop_auth.py b/studio/backend/tests/test_desktop_auth.py index bc995b6a59..039bb5e3e6 100644 --- a/studio/backend/tests/test_desktop_auth.py +++ b/studio/backend/tests/test_desktop_auth.py @@ -134,6 +134,218 @@ def test_ensure_default_admin_loads_existing_bootstrap_after_restart(monkeypatch assert storage.get_bootstrap_password() == bootstrap_pw +def test_bootstrap_password_file_ends_with_a_newline(): + # Otherwise `cat` welds the passphrase onto the shell prompt. + storage.ensure_default_admin() + + # Bytes: read_text would decode CRLF back to "\n" and hide a CR. + raw = storage._BOOTSTRAP_PW_PATH.read_bytes() + + assert raw == storage.get_bootstrap_password().encode("utf-8") + b"\n" + + +def test_bootstrap_password_round_trips_across_a_restart_with_the_newline(): + storage.ensure_default_admin() + original = storage.get_bootstrap_password() + + storage._bootstrap_password = None + + assert storage.generate_bootstrap_password() == original + + +def test_upgrade_normalises_the_bootstrap_file(): + # Upgrade path: the admin row exists, so generate_bootstrap_password() never runs. + seed_user() + storage._BOOTSTRAP_PW_PATH.write_bytes(b"legacy-bootstrap-secret") + + storage.ensure_default_admin() + + assert storage._BOOTSTRAP_PW_PATH.read_bytes() == b"legacy-bootstrap-secret\n" + assert storage.get_bootstrap_password() == "legacy-bootstrap-secret" + + +@pytest.mark.parametrize( + "other", + [ + b"legacy-bootstrap-secret\r\n", # only an unreleased build wrote this + b"legacy-bootstrap-secret\r", + b"legacy-bootstrap-secret ", + ], +) +def test_only_an_exactly_unterminated_bootstrap_file_is_touched(other): + # Appending is safe only because it is restricted to the one released shape. + seed_user() + storage._BOOTSTRAP_PW_PATH.write_bytes(other) + + storage.ensure_default_admin() + + assert storage.get_bootstrap_password() == "legacy-bootstrap-secret" + assert storage._BOOTSTRAP_PW_PATH.read_bytes() == other + + +def test_upgrade_normalises_when_the_admin_row_is_missing(): + storage._BOOTSTRAP_PW_PATH.write_bytes(b"legacy-bootstrap-secret") + + assert storage.generate_bootstrap_password() == "legacy-bootstrap-secret" + assert storage._BOOTSTRAP_PW_PATH.read_bytes() == b"legacy-bootstrap-secret\n" + + +def test_a_well_formed_bootstrap_file_is_not_rewritten(): + seed_user() + storage._BOOTSTRAP_PW_PATH.write_bytes(b"legacy-bootstrap-secret\n") + mtime = storage._BOOTSTRAP_PW_PATH.stat().st_mtime_ns + + storage.ensure_default_admin() + + assert storage._BOOTSTRAP_PW_PATH.stat().st_mtime_ns == mtime + + +def test_migration_failure_does_not_break_startup(monkeypatch): + seed_user() + storage._BOOTSTRAP_PW_PATH.write_bytes(b"legacy-bootstrap-secret") + + real_open = storage.os.open + + def refuse(path, flags, *args, **kwargs): + if str(path) == str(storage._BOOTSTRAP_PW_PATH): + raise PermissionError("read-only auth dir") + return real_open(path, flags, *args, **kwargs) + + monkeypatch.setattr(storage.os, "open", refuse) + + storage.ensure_default_admin() + + assert storage.get_bootstrap_password() == "legacy-bootstrap-secret" + assert storage._BOOTSTRAP_PW_PATH.read_bytes() == b"legacy-bootstrap-secret" + + +def test_normalising_never_recreates_a_cleared_bootstrap_file(monkeypatch): + # A rename would resurrect revoked plaintext if the password changed after the read. + seed_user() + storage._BOOTSTRAP_PW_PATH.write_bytes(b"legacy-bootstrap-secret") + + real_open = storage.os.open + + def clear_then_open(path, flags, *args, **kwargs): + if str(path) == str(storage._BOOTSTRAP_PW_PATH): + storage._BOOTSTRAP_PW_PATH.unlink(missing_ok = True) + return real_open(path, flags, *args, **kwargs) + + monkeypatch.setattr(storage.os, "open", clear_then_open) + + assert storage._read_persisted_bootstrap_password() == "legacy-bootstrap-secret" + assert not storage._BOOTSTRAP_PW_PATH.exists() + + +def test_normalising_does_not_overwrite_a_rotated_bootstrap_file(monkeypatch): + seed_user() + storage._BOOTSTRAP_PW_PATH.write_bytes(b"legacy-bootstrap-secret") + + real_open = storage.os.open + + def rotate_then_open(path, flags, *args, **kwargs): + if str(path) == str(storage._BOOTSTRAP_PW_PATH): + storage._BOOTSTRAP_PW_PATH.write_bytes(b"brand-new-secret\n") + return real_open(path, flags, *args, **kwargs) + + monkeypatch.setattr(storage.os, "open", rotate_then_open) + + storage._read_persisted_bootstrap_password() + + # The append may add a second newline; the rotated credential must survive. + raw = storage._BOOTSTRAP_PW_PATH.read_bytes() + assert raw.strip() == b"brand-new-secret" + storage._bootstrap_password = None + assert storage._load_bootstrap_password() == "brand-new-secret" + + +def test_leading_whitespace_bootstrap_file_is_left_alone(monkeypatch): + # An in-place rewrite is not atomic, so only the exact unterminated shape is touched. + seed_user() + storage._BOOTSTRAP_PW_PATH.write_bytes(b" legacy-bootstrap-secret ") + + storage.ensure_default_admin() + + assert storage.get_bootstrap_password() == "legacy-bootstrap-secret" + assert storage._BOOTSTRAP_PW_PATH.read_bytes() == b" legacy-bootstrap-secret " + + +def test_normalising_opens_the_file_in_binary_mode(monkeypatch): + # Without O_BINARY, Windows text mode turns the written LF back into CRLF. + seed_user() + storage._BOOTSTRAP_PW_PATH.write_bytes(b"legacy-bootstrap-secret") + monkeypatch.setattr(storage.os, "O_BINARY", 0x8000, raising = False) + seen = [] + real_open = storage.os.open + + def spy(path, flags, *args, **kwargs): + if str(path) == str(storage._BOOTSTRAP_PW_PATH): + seen.append(flags) + return real_open(path, flags & ~0x8000, *args, **kwargs) + + monkeypatch.setattr(storage.os, "open", spy) + + storage.ensure_default_admin() + + assert seen and all(f & 0x8000 for f in seen), seen + + +def test_clearing_by_truncation_mid_normalisation_is_not_undone(monkeypatch): + # clear_bootstrap_password() truncates through its own descriptor when the unlink + # fails (Windows, while ours is open); the append must not restore the plaintext. + seed_user() + storage._BOOTSTRAP_PW_PATH.write_bytes(b"legacy-bootstrap-secret") + + real_open = storage.os.open + + def truncate_then_open(path, flags, *args, **kwargs): + fd = real_open(path, flags, *args, **kwargs) + if str(path) == str(storage._BOOTSTRAP_PW_PATH): + storage._BOOTSTRAP_PW_PATH.write_text("", encoding = "utf-8") + return fd + + monkeypatch.setattr(storage.os, "open", truncate_then_open) + + storage._read_persisted_bootstrap_password() + + # A lone newline over a cleared file still reads back as no password. + assert storage._BOOTSTRAP_PW_PATH.read_bytes().strip() == b"" + storage._bootstrap_password = None + assert storage._load_bootstrap_password() is None + + +def test_normalising_works_without_fchmod(monkeypatch): + # os.fchmod only reached Windows in 3.13; its absence must not raise. + seed_user() + storage._BOOTSTRAP_PW_PATH.write_bytes(b"legacy-bootstrap-secret") + monkeypatch.delattr(storage.os, "fchmod", raising = False) + + storage.ensure_default_admin() + + assert storage._BOOTSTRAP_PW_PATH.read_bytes() == b"legacy-bootstrap-secret\n" + assert storage.get_bootstrap_password() == "legacy-bootstrap-secret" + + +def test_persisting_the_bootstrap_password_is_atomic(monkeypatch, tmp_path): + # A partial write would destroy the only plaintext recovery credential. + storage._persist_bootstrap_password("original-secret") + + def boom(src, dst): + raise OSError("crash before replace") + + monkeypatch.setattr(storage.os, "replace", boom) + with pytest.raises(OSError): + storage._persist_bootstrap_password("new-secret") + + assert storage._BOOTSTRAP_PW_PATH.read_bytes() == b"original-secret\n" + leftovers = [ + p.name + for p in storage._BOOTSTRAP_PW_PATH.parent.iterdir() + if "bootstrap_password." in p.name + ] + assert leftovers == [] + + def test_ensure_default_admin_does_not_generate_for_empty_existing_bootstrap(): seed_user() storage._BOOTSTRAP_PW_PATH.write_text(" \n", encoding = "utf-8") @@ -233,7 +445,7 @@ def test_consume_refresh_token_second_call_returns_none(): storage.save_refresh_token(raw, storage.DEFAULT_ADMIN_USERNAME, expires) first = storage.consume_refresh_token(raw) - assert first == (storage.DEFAULT_ADMIN_USERNAME, False) + assert first[:2] == (storage.DEFAULT_ADMIN_USERNAME, False) second = storage.consume_refresh_token(raw) assert second is None @@ -262,7 +474,7 @@ def test_consume_refresh_token_concurrent_only_one_succeeds(tmp_path, monkeypatc successes = [r for r in results if r is not None] assert len(successes) == 1, f"expected exactly one consumer to win, got {len(successes)}" - assert successes[0] == (storage.DEFAULT_ADMIN_USERNAME, False) + assert successes[0][:2] == (storage.DEFAULT_ADMIN_USERNAME, False) def test_consume_refresh_token_expired_returns_none(): @@ -336,6 +548,28 @@ def test_local_recipe_token_authenticates_as_admin_for_web_user(loaded_local_mod assert asyncio.run(get_current_subject(credentials)) == storage.DEFAULT_ADMIN_USERNAME +def test_rotated_credential_job_start_is_401_not_500(loaded_local_model): + # A reset-password landing mid-request makes the workflow-key mint refuse. + # That must reach the client as a revoked credential, not an unhandled error. + from fastapi import HTTPException + + seed_user() + jobs_route = data_recipe_jobs_module() + stale_gen = storage.credential_generation(secrets.token_urlsafe(64)) + + with pytest.raises(storage.CredentialRotated): + jobs_route._inject_local_providers(local_recipe(), local_recipe_request("t"), stale_gen) + + def _boom(*_a, **_k): + raise storage.CredentialRotated("revoked") + + jobs_route._inject_local_providers = _boom + payload = SimpleNamespace(recipe = local_recipe(), run = {}) + with pytest.raises(HTTPException) as excinfo: + jobs_route.create_job(payload, local_recipe_request("t"), ("unsloth", stale_gen)) + assert excinfo.value.status_code == 401 + + def test_desktop_login_rejects_invalid_secret(): seed_user(must_change_password = False) client = auth_client() @@ -358,7 +592,7 @@ def test_write_desktop_secret_file_is_0600_on_unix(tmp_path): studio_cli._write_auth_secret(path, "desktop-secret") - assert path.read_text() == "desktop-secret" + assert path.read_bytes() == b"desktop-secret\n" if platform.system() != "Windows": assert oct(path.stat().st_mode & 0o777) == "0o600" @@ -368,18 +602,31 @@ def test_reset_password_removes_desktop_secret_files(tmp_path, monkeypatch): from unsloth_cli.commands import studio as studio_cli auth_dir = tmp_path / "auth" - auth_dir.mkdir() - (auth_dir / "auth.db").write_text("db") - (auth_dir / ".bootstrap_password").write_text("boot") - (auth_dir / ".desktop_secret").write_text("new") monkeypatch.setattr(studio_cli, "STUDIO_HOME", tmp_path) + secret = studio_cli._create_desktop_secret_in_cli() + studio_cli._write_auth_secret(auth_dir / studio_cli.DESKTOP_SECRET_FILE, secret) + (auth_dir / studio_cli.BOOTSTRAP_PASSWORD_FILE).write_text("boot") result = CliRunner().invoke(studio_cli.studio_app, ["reset-password"]) - assert result.exit_code == 0 - assert not (auth_dir / "auth.db").exists() - assert not (auth_dir / ".bootstrap_password").exists() - assert not (auth_dir / ".desktop_secret").exists() + assert result.exit_code == 0, result.output + # The DB survives on purpose: a running server keeps serving from its admin row. + assert (auth_dir / "auth.db").exists() + assert not (auth_dir / studio_cli.BOOTSTRAP_PASSWORD_FILE).exists() + assert not (auth_dir / studio_cli.DESKTOP_SECRET_FILE).exists() + + conn = studio_cli._connect_auth_db() + try: + surviving = conn.execute( + "SELECT COUNT(*) FROM app_secrets WHERE key IN (?, ?)", + ( + studio_cli.DESKTOP_SECRET_HASH_KEY, + studio_cli.DESKTOP_SECRET_CREATED_AT_KEY, + ), + ).fetchone()[0] + finally: + conn.close() + assert surviving == 0 def test_reset_password_removes_desktop_secret_files_without_db(tmp_path, monkeypatch): @@ -525,7 +772,8 @@ if result.exit_code != 0: capture_output = True, ) assert result.returncode == 0, result.stderr + result.stdout - secret = (auth_dir / ".desktop_secret").read_text() + # Strip like the src-tauri readers do. + secret = (auth_dir / ".desktop_secret").read_text().strip() assert secret.startswith("desktop-") conn = sqlite3.connect(auth_dir / "auth.db") @@ -633,7 +881,7 @@ def test_update_password_clears_desktop_secret(): assert storage.validate_desktop_secret(raw) == storage.DEFAULT_ADMIN_USERNAME changed = storage.update_password(storage.DEFAULT_ADMIN_USERNAME, "new-admin-password") - assert changed is True + assert changed assert storage.validate_desktop_secret(raw) is None @@ -642,7 +890,7 @@ def test_update_password_on_unknown_user_leaves_desktop_secret_intact(): raw = storage.create_desktop_secret() changed = storage.update_password("not-a-user", "irrelevant") - assert changed is False + assert not changed assert storage.validate_desktop_secret(raw) == storage.DEFAULT_ADMIN_USERNAME diff --git a/studio/backend/tests/test_llama_cpp_tool_loop.py b/studio/backend/tests/test_llama_cpp_tool_loop.py index cbd1b07505..7f59a2d681 100644 --- a/studio/backend/tests/test_llama_cpp_tool_loop.py +++ b/studio/backend/tests/test_llama_cpp_tool_loop.py @@ -1487,7 +1487,418 @@ def test_internal_reprompt_attempts_do_not_duplicate_visible_text(monkeypatch): content_texts = [event.get("text", "") for event in events if event.get("type") == "content"] assert content_texts == ["I will use render_html now."] + # Each retry restates the last, so the loop gives up: initial + 2 re-prompts. + assert len(payloads) == 3 < _MAX_REPROMPTS + 1 + + +def test_post_tool_stall_still_nudged_after_a_pre_tool_reprompt(monkeypatch): + """The post-tool nudge has its own budget, so an earlier stall can't spend it.""" + + streams = [ + [_sse({"content": "I will search the web now."}), _done()], + [ + _sse( + { + "tool_calls": [ + { + "index": 0, + "id": "call_first", + "type": "function", + "function": { + "name": "web_search", + "arguments": json.dumps({"query": "red square"}), + }, + } + ] + } + ), + _done(), + ], + [_sse({"content": "Let me summarize the results."}), _done()], + [_sse({"content": "Final answer: the square is red."}), _done()], + ] + payloads: list[dict] = [] + backend = _make_backend(monkeypatch, streams, payloads) + + calls: list[tuple[str, dict]] = [] + + def fake_execute_tool(name, arguments, **_kwargs): + calls.append((name, arguments)) + return "Search results: red is #f00." + + monkeypatch.setattr("core.inference.tools.execute_tool", fake_execute_tool) + + tools = [ + { + "type": "function", + "function": { + "name": "web_search", + "description": "Search the web.", + "parameters": { + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"], + }, + }, + } + ] + + events = list( + backend.generate_chat_completion_with_tools( + messages = [{"role": "user", "content": "Make a red square."}], + tools = tools, + max_tool_iterations = 2, + ) + ) + + assert len(payloads) == 4 + assert len(calls) == 1 + nudges = [ + message + for message in payloads[-1]["messages"] + if message.get("role") == "user" and "call web_search now" in message.get("content", "") + ] + assert len(nudges) == 2 + content_texts = [event.get("text", "") for event in events if event.get("type") == "content"] + assert content_texts[-1] == "Final answer: the square is red." + + +def test_post_tool_reprompt_budget_is_one(monkeypatch): + """The post-tool nudge fires once; a second stall is surrendered as the answer.""" + + streams = [ + [ + _sse( + { + "tool_calls": [ + { + "index": 0, + "id": "call_first", + "type": "function", + "function": { + "name": "web_search", + "arguments": json.dumps({"query": "red square"}), + }, + } + ] + } + ), + _done(), + ], + [_sse({"content": "Let me summarize the results."}), _done()], + [_sse({"content": "Now I will check the sources."}), _done()], + ] + payloads: list[dict] = [] + backend = _make_backend(monkeypatch, streams, payloads) + + monkeypatch.setattr( + "core.inference.tools.execute_tool", + lambda *_a, **_k: "Search results: red is #f00.", + ) + + tools = [ + { + "type": "function", + "function": { + "name": "web_search", + "description": "Search the web.", + "parameters": { + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"], + }, + }, + } + ] + + list( + backend.generate_chat_completion_with_tools( + messages = [{"role": "user", "content": "Make a red square."}], + tools = tools, + max_tool_iterations = 2, + ) + ) + + assert len(payloads) == 3 + + +def test_repeat_guard_resets_after_a_tool_runs(monkeypatch): + """A tool execution opens a new phase, so the same intent text is nudged again. + + Without the reset the pre-tool stall text still sits in the repeat tracker and + the identical post-tool stall is surrendered as the visible final answer. + """ + + stall = "I will search the web now." + streams = [ + [_sse({"content": stall}), _done()], + [ + _sse( + { + "tool_calls": [ + { + "index": 0, + "id": "call_first", + "type": "function", + "function": { + "name": "web_search", + "arguments": json.dumps({"query": "red square"}), + }, + } + ] + } + ), + _done(), + ], + [_sse({"content": stall}), _done()], + [_sse({"content": "Final answer: the square is red."}), _done()], + ] + payloads: list[dict] = [] + backend = _make_backend(monkeypatch, streams, payloads) + + monkeypatch.setattr( + "core.inference.tools.execute_tool", + lambda *_a, **_k: "Search results: red is #f00.", + ) + + tools = [ + { + "type": "function", + "function": { + "name": "web_search", + "description": "Search the web.", + "parameters": { + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"], + }, + }, + } + ] + + events = list( + backend.generate_chat_completion_with_tools( + messages = [{"role": "user", "content": "Make a red square."}], + tools = tools, + max_tool_iterations = 2, + ) + ) + + assert len(payloads) == 4 + content_texts = [event.get("text", "") for event in events if event.get("type") == "content"] + assert content_texts[-1] == "Final answer: the square is red." + + +def test_restatement_keeps_deletions_that_change_the_answer(): + """A dropped word can invert the meaning, so a subset is not a restatement.""" + + from core.inference.tool_call_parser import is_reprompt_restatement + from core.inference.llama_cpp import _should_suppress_forced_no_tool_output as suppress + + previous = "Now I think the feature is not supported in version 1." + corrected = "Now I think the feature is supported in version 1." + assert not is_reprompt_restatement(corrected, previous) + assert not suppress(corrected, previous) + + stall = "I'll search for that now." + assert is_reprompt_restatement(stall, stall) + assert is_reprompt_restatement("Understood. " + stall, "Understood, " + stall) + assert not is_reprompt_restatement(stall + " Tokyo.", stall) + + +def test_forced_turn_suppression_covers_obligation_phrasing(): + from core.inference.llama_cpp import _should_suppress_forced_no_tool_output as suppress + for stall in ( + "I need to use render_html now", + "Need to call web_search", + "I will summarize the results now", + "I have to run the search first", + "I should call web_search now", + "I should use render_html now", + # Plain modals take a bare infinitive, not the need|have|ought "to" group. + "I must call web_search now", + "I must use render_html now", + "I must run the search first", + # Subjectless plans open a new sentence just as often as a new line. + "Okay. Need to call web_search now.", + "Understood. Going to search now.", + # Subjectless modals, not just subjectless semi-modals. + "Must call web_search now.", + "Should search the web now.", + # A missing answer is not a final answer: the plan behind it is still a stall. + "I should call web_search because the answer is not in the provided context", + "I must run the search since the answer is unknown so far", + # A pivot with nothing behind it answers nothing. + "I should call web_search, though.", + "I need to run the search, but", + # A purpose clause is part of the plan, not a summary of results. + "I need to call web_search to summarize the results", + ): + assert suppress(stall), f"leaked {stall!r}" + + for answer in ( + "You need to install the package first.", + "The square is red.", + "Here is the summary of what I found.", + "Run `pip install unsloth` to get started.", + "I should mention that the square is red.", + # Obligation phrasing mid-sentence is prose that happens to name a tool. + "The API I should invoke is foo() because it supports streaming.", + "The tool I need to use is documented here.", + # "invoke"/"query" read as technical prose far more often than as a stall. + "I should invoke foo() because it supports streaming.", + "I should query the cache first for a faster path.", + "You should call your bank about the charge.", + # Second person is the user's obligation, not the model's plan. + "You must call your bank about the charge.", + "I must admit the square is red.", + # A plan that pivots to an answer must ship the answer with it. + "I should call web_search, but the answer is Tokyo.", + "I need to call web_search. The answer is Tokyo.", + "I should call web_search to confirm, but Tokyo is the capital of Japan.", + "I must run the search, however the result is already known: 42.", + ): + assert not suppress(answer), f"dropped {answer!r}" + + +def test_forced_turn_intent_lead_in_needs_a_restatement_to_be_dropped(): + """A bare intent match is a stall only when the retry restates the nudge. + + ``INTENT_SIGNAL`` fires on lead-ins that introduce a real answer ("Now I + have the results. ..."), so matching it alone would discard the answer. + """ + from core.inference.llama_cpp import _should_suppress_forced_no_tool_output as suppress + + stall = "I will summarize the results now" + answer = "Now I have the search results. The capital of Japan is Tokyo." + + # Restating the nudged text is still a stall. + assert suppress(stall, stall) + assert suppress("Understood. " + stall, "Understood, " + stall) + # Progress past the nudged text keeps the answer, lead-in and all. + assert not suppress(answer, stall) + assert not suppress("Step 3: done. Tokyo is the capital.", stall) + # Near-repeat is enough to stop nudging, never enough to drop the turn. + assert not suppress(stall + ": Tokyo.", stall) + # An obligation plan is a stall on its own, no previous text needed. + assert suppress("I must call web_search now", answer) + + +def test_forced_turn_answer_with_an_intent_lead_in_survives_after_a_tool(monkeypatch): + """The post-tool retry answers behind a lead-in; the answer must still ship. + + The nudge budget is spent, so the reply lands on the suppression branch. + ``INTENT_SIGNAL`` matches its "Now I ..." opener, and dropping it on that + alone left the user with the stall and no answer at all. + """ + + answer = "Now I have the results. The capital of Japan is Tokyo." + streams = [ + [ + _sse( + { + "tool_calls": [ + { + "index": 0, + "id": "call_first", + "type": "function", + "function": { + "name": "web_search", + "arguments": json.dumps({"query": "capital of Japan"}), + }, + } + ] + } + ), + _done(), + ], + [_sse({"content": "Let me summarize what I found."}), _done()], + [_sse({"content": answer}), _done()], + ] + payloads: list[dict] = [] + backend = _make_backend(monkeypatch, streams, payloads) + + monkeypatch.setattr( + "core.inference.tools.execute_tool", + lambda *_a, **_k: "Search results: Tokyo.", + ) + + tools = [ + { + "type": "function", + "function": { + "name": "web_search", + "description": "Search the web.", + "parameters": { + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"], + }, + }, + } + ] + + events = list( + backend.generate_chat_completion_with_tools( + messages = [{"role": "user", "content": "What is the capital of Japan?"}], + tools = tools, + max_tool_iterations = 2, + ) + ) + + assert len(payloads) == 3 + content_texts = [event.get("text", "") for event in events if event.get("type") == "content"] + assert content_texts[-1] == answer + + +def test_forced_turn_answer_with_an_intent_lead_in_survives_pre_tool(monkeypatch): + """Same guarantee once the pre-tool nudge budget is spent on distinct stalls.""" + + answer = "Now I see the data clearly. Tokyo is the capital." + streams = [ + [_sse({"content": text}), _done()] + for text in ( + "I will look that up for you.", + "Now I have the search results. The capital of Japan is Tokyo.", + "Now I can confirm it. Japan's capital city is Tokyo.", + answer, + ) + ] + payloads: list[dict] = [] + backend = _make_backend(monkeypatch, streams, payloads) + + def fake_execute_tool(name, arguments, **_kwargs): + raise AssertionError(f"unexpected tool execution: {name} {arguments}") + + monkeypatch.setattr("core.inference.tools.execute_tool", fake_execute_tool) + + tools = [ + { + "type": "function", + "function": { + "name": "web_search", + "description": "Search the web.", + "parameters": { + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"], + }, + }, + } + ] + + events = list( + backend.generate_chat_completion_with_tools( + messages = [{"role": "user", "content": "What is the capital of Japan?"}], + tools = tools, + max_tool_iterations = 2, + ) + ) + + # Initial turn plus the three pre-tool nudges. assert len(payloads) == _MAX_REPROMPTS + 1 + content_texts = [event.get("text", "") for event in events if event.get("type") == "content"] + assert content_texts[-1] == answer def test_forced_reprompt_plain_final_answer_is_visible(monkeypatch): @@ -2084,6 +2495,51 @@ def test_confirm_tool_calls_skips_gguf_rag_autoinject(monkeypatch): assert any(event.get("type") == "content" and event.get("text") == "Done." for event in events) +def test_rag_autoinject_counts_as_a_prior_tool_execution(monkeypatch): + """Autoinjected retrieval runs before the controller, so history stays empty. + + Without counting it the turn reads as pre-tool and gets the full re-prompt + budget, repeating the expensive retrieval the post-tool cap exists to stop. + """ + + stall = "I will summarize the retrieved passages now." + streams = [ + [_sse({"content": stall}), _done()], + [_sse({"content": "Still working on the summary."}), _done()], + [_sse({"content": "Final answer: the passages describe Tokyo."}), _done()], + ] + payloads: list[dict] = [] + backend = _make_backend(monkeypatch, streams, payloads) + + monkeypatch.setattr( + "core.inference.tools.build_rag_autoinject", + lambda *_a, **_k: { + "events": [], + "messages": [{"role": "user", "content": "Retrieved passage: Tokyo."}], + }, + ) + + events = list( + backend.generate_chat_completion_with_tools( + messages = [{"role": "user", "content": "summarize the docs"}], + tools = [{"type": "function", "function": {"name": "search_knowledge_base"}}], + max_tool_iterations = 2, + rag_scope = {"thread_id": "t1"}, + ) + ) + + # Initial turn plus one retry; read as pre-tool it would spend the full budget. + assert len(payloads) == 2, payloads + nudges = [ + message + for message in payloads[-1]["messages"] + if message.get("role") == "user" + and "call search_knowledge_base now" in message.get("content", "") + ] + assert len(nudges) == 1, nudges + assert events + + def test_confirm_tool_calls_deny_skips_gguf_tool_and_retry_can_execute(monkeypatch): same_call = _structured_tool_call("python", {"code": "print(1)"}, "call_py") streams = [ diff --git a/studio/backend/tests/test_password_prompt_backstop.py b/studio/backend/tests/test_password_prompt_backstop.py index 3c2c1956f9..6c22532532 100644 --- a/studio/backend/tests/test_password_prompt_backstop.py +++ b/studio/backend/tests/test_password_prompt_backstop.py @@ -247,8 +247,8 @@ def test_lifespan_honors_bootstrap_suppression_in_source(): def test_clear_bootstrap_password_truncates_when_unlink_fails(monkeypatch, tmp_path): # If the file cannot be unlinked (Windows AV / read-only auth dir), clear must # truncate it so its stale plaintext cannot be re-seeded by - # generate_bootstrap_password() after a later reset-password deletes auth.db, - # which would re-validate the revoked bootstrap password. + # generate_bootstrap_password() if auth.db is ever recreated, which would + # re-validate the revoked bootstrap password. import pathlib pw_path = tmp_path / ".bootstrap_password" diff --git a/studio/backend/tests/test_plan_classifier_accuracy.py b/studio/backend/tests/test_plan_classifier_accuracy.py new file mode 100644 index 0000000000..9144fb92e3 --- /dev/null +++ b/studio/backend/tests/test_plan_classifier_accuracy.py @@ -0,0 +1,96 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +"""An accuracy floor for the plan-without-action classifier, on real model output. + +The rest of the tool-loop suites pin behaviour on hand-written example sentences, +which is how the patterns here were tuned. That says nothing about how often the +classifier is right on what models actually emit, so this file scores it against a +corpus captured from local models (``tests/data/plan_vs_answer.jsonl``). + +How the corpus was built: three GGUF models (Qwen3-0.6B, Qwen3-1.7B, +Llama-3.2-1B-Instruct) were driven through llama-server with the real Studio tool +schemas over prompts spanning tool-requiring questions, questions needing no tool, +list-formatted answers, ambiguous requests, non-English, and follow-ups issued after +a tool had already run. Turns cut off by the token cap were dropped, since a +truncation is not a stall. + +Every turn here is a *finished answer*: the turn called no tool, and when the +production nudge was appended and the turn regenerated three times, not one retry +produced a tool call. A forceful re-prompt could not extract an action, so there was +no action left to take. Nudging these is wasted work, and in the GGUF loop the +retry's text can then be discarded, which costs the user a visible answer. + +Measured when this landed, over the 300 turns: + + tree nudged retry discarded + origin/main (pre-PR) 36 (12.0%) 60 (20.2%) + this PR 5 ( 1.7%) 1 ( 0.3%) + +The budgets below sit above the measured counts so that innocuous wording changes +do not fail the build, and far below the pre-PR counts so a real regression does. +A failure prints the offending turns: fix the pattern, or if the turn really is a +stall, correct its label here. +""" + +import json +from pathlib import Path + +from core.inference.llama_cpp import _should_suppress_forced_no_tool_output +from core.inference.tool_call_parser import is_short_intent_without_action + +DATA = Path(__file__).parent / "data" / "plan_vs_answer.jsonl" + +# Measured 5 of 300; pre-PR was 36. +NUDGE_BUDGET = 9 +# Measured 1 of 300; pre-PR was 60. Tighter, because this one destroys output. +DISCARD_BUDGET = 4 + + +def _corpus(): + with open(DATA, encoding = "utf-8") as fh: + return [json.loads(line) for line in fh if line.strip()] + + +def _report(rows, limit = 10): + lines = [] + for row in rows[:limit]: + text = " ".join(row["text"].split()) + lines.append( + f" [{row['model']}/{row['prompt_class']}] {row['prompt']!r}\n {text[:200]!r}" + ) + if len(rows) > limit: + lines.append(f" ... and {len(rows) - limit} more") + return "\n".join(lines) + + +def test_corpus_is_intact(): + """Guards the budgets: they mean nothing if the corpus silently shrinks.""" + corpus = _corpus() + assert len(corpus) == 300 + assert all(row["text"].strip() for row in corpus) + # Every row is a finished answer by construction. + assert all(row["retry_tool_calls"] == 0 for row in corpus) + + +def test_finished_answers_are_rarely_nudged(): + """A finished answer costs a whole extra generation when it is nudged.""" + nudged = [row for row in _corpus() if is_short_intent_without_action(row["text"])] + assert len(nudged) <= NUDGE_BUDGET, ( + f"{len(nudged)}/300 finished answers classified as plans " + f"(budget {NUDGE_BUDGET}):\n{_report(nudged)}" + ) + + +def test_finished_answers_are_not_discarded(): + """The retry's text is all the user gets, so discarding it is the worst case.""" + discarded = [ + row + for row in _corpus() + if row["retry_text"].strip() + and _should_suppress_forced_no_tool_output(row["retry_text"], row["text"]) + ] + assert len(discarded) <= DISCARD_BUDGET, ( + f"{len(discarded)}/300 finished retries would be discarded " + f"(budget {DISCARD_BUDGET}):\n{_report(discarded)}" + ) diff --git a/studio/backend/tests/test_safetensors_tool_loop.py b/studio/backend/tests/test_safetensors_tool_loop.py index 2e7e99fbba..4a7b3ece20 100644 --- a/studio/backend/tests/test_safetensors_tool_loop.py +++ b/studio/backend/tests/test_safetensors_tool_loop.py @@ -2232,6 +2232,33 @@ def test_reprompt_names_only_active_tools_not_hardcoded(): assert "python" not in reprompt["content"] +def test_reprompt_stops_when_the_retry_restates_the_stall(): + """A nudge answered with the same text has not worked; do not spend the budget.""" + + captured: list[list] = [] + stall = "I'll search for that now." + + def fake_single_turn(messages, active_tools = None): + captured.append(list(messages)) + yield stall # same forward-looking intent every time + + exec_fn = FakeExecuteTool([]) + _events = _collect_events( + run_safetensors_tool_loop( + single_turn = fake_single_turn, + messages = [{"role": "user", "content": "find X"}], + tools = [{"type": "function", "function": {"name": "search_knowledge_base"}}], + execute_tool = exec_fn, + auto_heal_tool_calls = True, + nudge_tool_calls = True, + max_tool_iterations = 3, + ) + ) + + # One nudge, then the repeat guard stops it: two generations, not MAX_ACT_REPROMPTS + 1. + assert len(captured) == 2, captured + + def test_reprompt_is_announced_on_the_status_channel(): # The re-prompted turn is hidden, so the badge is the only sign of life. # Blank still comes first: the route resets its text cursor only on that. @@ -3624,8 +3651,22 @@ class TestGGUFSafetensorsHealingParity: "Let me check", "I am going to call the tool", "First, I will explore", + "First, let's search the web", + "First, let us search the web", + # Imperative plans carry no pronoun; an action verb is enough. + "First, search the web for the latest release notes.", + "First, check the documentation.", + "First, analyze the attached data", + "The first step is to search the web", + "First, my plan is to search the web.", + "First: search the web for release notes.", + "First - search the web for release notes.", + "First \u2013 search the web for release notes.", + "First, our approach is to check the docs.", "Here's my plan", "Now I need to call web_search", + # The "let me know" exemption is scoped to "let me", not all direct intent. + "I will know the answer after I search the web", ): assert shared_re.search(phrase), f"missed {phrase!r}" assert shared_fn(phrase), f"helper missed {phrase!r}" @@ -3641,6 +3682,18 @@ class TestGGUFSafetensorsHealingParity: # force a tool-call re-prompt on it. "I will not search the web for that.", "I'll never call that tool.", + # Hands control back rather than announcing an action. + "Let me know if you need anything else.", + "First, the answer is 42", + "First, the result is 3.", + "First, it is 42", + "First, my answer is 42", + "The first line is blank.", + # Ordinal prose, not a plan. + "First place went to Alice", + "First class is available", + # Advice to the user, not work for this turn. + "First, install the package.", ): assert not shared_re.search(plain), f"wrongly fired on {plain!r}" assert not shared_fn(plain), f"helper wrongly fired on {plain!r}" @@ -3653,6 +3706,98 @@ class TestGGUFSafetensorsHealingParity: assert gguf_cap == sf_cap == shared_cap + def test_reprompt_repeat_keeps_punctuation_bearing_terms(self): + # Stripping all non-word chars collapsed "C++" and "C#" to "c", so different + # plans compared equal and the retry lost its nudge. + from core.inference.tool_call_parser import is_reprompt_repeat + assert not is_reprompt_repeat("I will search for C#.", "I will search for C++.") + # A leading mark is part of the term too. + assert not is_reprompt_repeat("I will search for .NET", "I will search for NET") + + def test_reprompt_repeat_respects_word_order(self): + # Set overlap scores a reordered query as identical, so the comparison is + # sequence-based. + from core.inference.tool_call_parser import is_reprompt_repeat + + assert not is_reprompt_repeat( + "I will search for dogs not cats", "I will search for cats not dogs" + ) + assert is_reprompt_repeat( + "I will search for cats not dogs", "I will search for cats not dogs" + ) + assert is_reprompt_repeat("I will search for C++!", "I will search for C++.") + + def test_reprompt_repeat_keeps_a_changed_query_token(self): + # One corrected token in a long plan is a new attempt; at the old 0.85 bar it + # scored ~0.87 and cost the model its remaining nudge. + from core.inference.tool_call_parser import is_reprompt_repeat + + before = "I will search the web for the latest CUDA version 12.4 driver release notes" + after = "I will search the web for the latest CUDA version 12.5 driver release notes" + assert not is_reprompt_repeat(after, before) + assert is_reprompt_repeat(before, before) + + def test_reprompt_repeat_keeps_standalone_operator_tokens(self): + # A marks-only token stripped to nothing, so a bounded correction compared + # equal to the unbounded original. + from core.inference.tool_call_parser import is_reprompt_repeat, is_reprompt_restatement + + loose = "Now I think the value is 5" + bounded = "Now I think the value is < 5" + assert not is_reprompt_repeat(bounded, loose) + assert not is_reprompt_restatement(bounded, loose) + + def test_reprompt_repeat_keeps_a_changed_token_in_a_long_plan(self): + # Every similarity ratio is length-dependent: one changed token scored 0.98 + # across 54 tokens, so long corrected plans lost their nudge. + from core.inference.tool_call_parser import is_reprompt_repeat + + words = [f"token{index}" for index in range(54)] + corrected = list(words) + corrected[20] = "revised" + assert not is_reprompt_repeat(" ".join(corrected), " ".join(words)) + assert is_reprompt_repeat(" ".join(words), " ".join(words)) + + def test_reprompt_repeat_keeps_articles_that_name_a_target(self): + # "The Who" and "Who" are different searches, so articles are not filler. + from core.inference.tool_call_parser import is_reprompt_repeat + assert not is_reprompt_repeat( + "I will search for The Who discography", + "I will search for Who discography", + ) + + def test_reprompt_repeat_keeps_filler_words_that_name_a_target(self): + # No word is reliably filler: dropping "ok"/"the" to absorb rewording also + # absorbed the search target. Reordered filler now reads as a new attempt, + # which costs one nudge out of the cap and never strands a plan. + from core.inference.tool_call_parser import is_reprompt_repeat + assert not is_reprompt_repeat( + "I will search for OK Go discography", + "I will search for Go discography", + ) + assert not is_reprompt_repeat( + "I will now summarize the findings", + "I will summarize the findings now", + ) + + def test_reprompt_repeat_detects_restated_answers(self): + # A nudge answered with the same text again has not worked; stop there. + from core.inference.tool_call_parser import is_reprompt_repeat + + same = "I will summarize what I found." + assert is_reprompt_repeat(same, same) + assert is_reprompt_repeat("I WILL summarize what I found!", same) + assert is_reprompt_repeat( + "The summary is ready, please let me know if you need anything else", + "The summary is ready. Please let me know if you need anything else!", + ) + + # No previous text, or genuinely different progress, keeps the nudge. + assert not is_reprompt_repeat(same, "") + assert not is_reprompt_repeat("Tokyo is 18C and cloudy right now.", same) + # Short texts must not collide on incidental word overlap. + assert not is_reprompt_repeat("Let me check.", "Let me search.") + class TestLoopControl: def test_cancel_event_breaks_loop(self): @@ -4182,9 +4327,11 @@ class TestPlanWithoutActionReprompt: # final answer and no further turn is generated. from core.inference.tool_call_parser import MAX_ACT_REPROMPTS - stall = "Let me look into it first." + # Distinct stalls: identical ones stop at the repeat guard, never reaching the cap. + stalls = [f"Let me look into detail {i} first." for i in range(MAX_ACT_REPROMPTS)] + stall = stalls[-1] turns = [["I'll search the web for that."]] - turns += [[stall]] * MAX_ACT_REPROMPTS + turns += [[s] for s in stalls] turns += [["SHOULD NOT APPEAR"]] generations = {"count": 0} diff --git a/studio/backend/tests/test_studio_pid_files.py b/studio/backend/tests/test_studio_pid_files.py new file mode 100644 index 0000000000..df2c8e87f8 --- /dev/null +++ b/studio/backend/tests/test_studio_pid_files.py @@ -0,0 +1,568 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +"""Per-port PID files, so `unsloth studio stop` can find every server. + +Imports run.py directly, so run under the Unsloth venv. +""" + +from __future__ import annotations + +import os +import sys +from pathlib import Path +from types import SimpleNamespace + +import pytest + +_BACKEND = Path(__file__).resolve().parents[1] +if str(_BACKEND) not in sys.path: + sys.path.insert(0, str(_BACKEND)) + +import run # noqa: E402 + +# Captured before the autouse fixture stubs them, for the tests that exercise them. +_REAL_IS_STUDIO_BACKEND = run._pid_is_studio_backend +_REAL_PID_ALIVE = run._pid_alive + + +@pytest.fixture(autouse = True) +def isolated_root(tmp_path, monkeypatch): + monkeypatch.setattr(run, "_studio_root", lambda: tmp_path) + monkeypatch.setattr(run, "_PID_FILE", tmp_path / "studio.pid") + monkeypatch.setattr(run, "_OWN_PID_FILE", None) + monkeypatch.setattr(run, "_pid_alive", lambda pid: True) + monkeypatch.setattr(run, "_pid_is_studio_backend", lambda pid, created_times = (): True) + yield + + +def _files(tmp_path): + return sorted(p.name for p in tmp_path.glob("studio-*.pid")) + + +def _pid_of(path): + return path.read_text(encoding = "utf-8").splitlines()[0] + + +def test_write_pid_file_records_port_and_pid(tmp_path): + run._write_pid_file(8901) + + assert _files(tmp_path) == [f"studio-8901-{os.getpid()}.pid"] + assert _pid_of(tmp_path / f"studio-8901-{os.getpid()}.pid") == str(os.getpid()) + + +def test_write_pid_file_records_the_start_time(tmp_path): + # Pins the record to this process, so a reused PID isn't mistaken for it. + run._write_pid_file(8901) + + record = run._read_pid_record(tmp_path / f"studio-8901-{os.getpid()}.pid") + + assert record[0] == os.getpid() + assert record[1] == pytest.approx(run._process_create_time(os.getpid())) + + +def test_write_pid_file_keeps_the_legacy_file_a_bare_pid(tmp_path): + # An older CLI's `stop` reads studio.pid and expects only digits. + run._write_pid_file(8901) + + assert (tmp_path / "studio.pid").read_text(encoding = "utf-8") == str(os.getpid()) + + +def test_second_port_does_not_clobber_the_first(tmp_path): + (tmp_path / "studio-8901-8550.pid").write_text("8550", encoding = "utf-8") + + run._write_pid_file(8902) + + assert _pid_of(tmp_path / "studio-8901-8550.pid") == "8550" + assert (tmp_path / f"studio-8902-{os.getpid()}.pid").exists() + + +def test_same_port_on_two_binds_does_not_clobber(tmp_path): + # 127.0.0.1:8888 and ::1:8888 can both listen; one file per port would lose one. + (tmp_path / "studio-8888-8550.pid").write_text("8550", encoding = "utf-8") + + run._write_pid_file(8888) + + assert len(_files(tmp_path)) == 2 + + +def test_remove_pid_file_only_removes_our_own(tmp_path, monkeypatch): + run._write_pid_file(8901) + (tmp_path / "studio-8902-8600.pid").write_text("8600", encoding = "utf-8") + # Nothing to hand the legacy pointer to, so it goes away with us. + monkeypatch.setattr(run, "_pid_alive", lambda pid: pid == os.getpid()) + + run._remove_pid_file() + + assert _files(tmp_path) == ["studio-8902-8600.pid"] + assert not (tmp_path / "studio.pid").exists() + + +def test_the_legacy_pointer_moves_to_a_live_sibling(tmp_path): + # Only one server owns studio.pid. Deleting it on our way out would leave an + # older CLI, which reads nothing else, unable to stop the sibling still up. + run._write_pid_file(8901) + (tmp_path / "studio-8902-8600.pid").write_text("8600", encoding = "utf-8") + + run._remove_pid_file() + + assert (tmp_path / "studio.pid").read_text(encoding = "utf-8").strip() == "8600" + + +def test_the_legacy_pointer_is_not_handed_to_a_dead_sibling(tmp_path, monkeypatch): + run._write_pid_file(8901) + (tmp_path / "studio-8902-8600.pid").write_text("8600", encoding = "utf-8") + monkeypatch.setattr(run, "_pid_is_studio_backend", lambda pid, created_times = (): False) + + run._remove_pid_file() + + assert not (tmp_path / "studio.pid").exists() + + +def test_remove_pid_file_leaves_a_reused_entry_alone(tmp_path): + run._write_pid_file(8901) + own = tmp_path / f"studio-8901-{os.getpid()}.pid" + own.write_text("999999", encoding = "utf-8") + + run._remove_pid_file() + + assert own.read_text(encoding = "utf-8") == "999999" + + +def test_windows_liveness_does_not_call_every_pid_alive(monkeypatch): + # os.kill(pid, 0) raises OSError for every pid on Windows, so without the + # tasklist fallback a stale record would block its port forever. + import subprocess + + monkeypatch.setattr(run, "_pid_alive", _REAL_PID_ALIVE) + monkeypatch.setitem(sys.modules, "psutil", None) + monkeypatch.setattr(sys, "platform", "win32") + monkeypatch.setattr( + subprocess, "run", lambda *a, **k: SimpleNamespace(stdout = '"python.exe","8550",...') + ) + + assert run._pid_alive(8550) is True + assert run._pid_alive(9999) is False + + +def test_windows_liveness_keeps_the_record_when_tasklist_fails(monkeypatch): + # Unconfirmed must mean keep, matching the CLI's _pid_alive. Pruning a live + # server's record lets the next launch fall back past it and strand it, which + # is the bug this file exists to fix; a stale record costs one clear abort. + import subprocess + + def _boom(*a, **k): + raise OSError("tasklist missing") + + monkeypatch.setattr(run, "_pid_alive", _REAL_PID_ALIVE) + monkeypatch.setitem(sys.modules, "psutil", None) + monkeypatch.setattr(sys, "platform", "win32") + monkeypatch.setattr(subprocess, "run", _boom) + + assert run._pid_alive(8550) is True + + +def test_read_pid_record_parses_pid_time_and_address(tmp_path): + (tmp_path / "r.pid").write_text("8550\n111.5\n127.0.0.1", encoding = "utf-8") + + assert run._read_pid_record(tmp_path / "r.pid") == (8550, 111.5, "127.0.0.1") + + +def test_read_pid_record_tolerates_a_bare_pid(tmp_path): + (tmp_path / "r.pid").write_text("8550", encoding = "utf-8") + + assert run._read_pid_record(tmp_path / "r.pid") == (8550, None, None) + + +def test_read_pid_record_rejects_pid_zero_and_init(tmp_path): + # kill(0) signals our whole process group. + (tmp_path / "zero.pid").write_text("0", encoding = "utf-8") + (tmp_path / "init.pid").write_text("1", encoding = "utf-8") + + assert run._read_pid_record(tmp_path / "zero.pid") is None + assert run._read_pid_record(tmp_path / "init.pid") is None + + +def test_read_pid_record_rejects_a_corrupt_file(tmp_path): + (tmp_path / "r.pid").write_text("not-a-pid", encoding = "utf-8") + + assert run._read_pid_record(tmp_path / "r.pid") is None + + +def test_graceful_shutdown_drops_the_record_last(monkeypatch): + # Cleanup can take seconds while the server is still alive. Dropping the record + # first leaves a retried `stop` or a new launch unable to find it. + order = [] + monkeypatch.setattr(run, "_remove_pid_file", lambda: order.append("remove_record")) + + class _Server: + def __setattr__(self, name, value): + order.append("release_socket") + + run._graceful_shutdown(_Server()) + + assert order == ["release_socket", "remove_record"] + + +def test_own_studio_on_port_is_found_without_psutil(tmp_path, monkeypatch): + # psutil is optional; a listener scan finds nothing without it, so detection + # must come from our own records or we silently start a duplicate. + monkeypatch.setitem(sys.modules, "psutil", None) + (tmp_path / "studio-8901-8550.pid").write_text("8550\n\n127.0.0.1", encoding = "utf-8") + + assert run._own_studio_on_port(8901, "127.0.0.1") == 8550 + + +def test_no_record_for_the_port_means_no_own_studio(tmp_path): + # jupyter-lab on 8888 must keep the fallback, not abort the launch. + (tmp_path / "studio-8901-8550.pid").write_text("8550", encoding = "utf-8") + + assert run._own_studio_on_port(8888, "127.0.0.1") is None + + +def test_own_studio_on_port_prunes_a_dead_record(tmp_path, monkeypatch): + monkeypatch.setattr(run, "_pid_alive", lambda pid: False) + (tmp_path / "studio-8901-8550.pid").write_text("8550", encoding = "utf-8") + + assert run._own_studio_on_port(8901, "127.0.0.1") is None + assert not (tmp_path / "studio-8901-8550.pid").exists() + + +def test_a_reused_pid_is_not_treated_as_our_studio(tmp_path, monkeypatch): + # Stale record + the OS handing that PID to something else must not abort. + monkeypatch.setattr(run, "_pid_is_studio_backend", lambda pid, created_times = (): False) + (tmp_path / "studio-8901-8550.pid").write_text("8550", encoding = "utf-8") + + assert run._own_studio_on_port(8901, "127.0.0.1") is None + + +def test_an_unverifiable_record_still_blocks_a_duplicate(tmp_path, monkeypatch): + # Can't tell: refusing with a clear message beats a silent second instance. + monkeypatch.setattr(run, "_pid_is_studio_backend", lambda pid, created_times = (): True) + (tmp_path / "studio-8901-8550.pid").write_text("8550", encoding = "utf-8") + + assert run._own_studio_on_port(8901, "127.0.0.1") == 8550 + + +def test_start_time_mismatch_rejects_a_reused_pid(monkeypatch): + monkeypatch.setattr(run, "_pid_is_studio_backend", _REAL_IS_STUDIO_BACKEND) + monkeypatch.setattr(run, "_process_create_time", lambda pid: 999.0) + + assert run._pid_is_studio_backend(8550, [111.5]) is False + assert run._pid_is_studio_backend(8550, [999.0]) is True + + +def test_a_stale_record_does_not_veto_a_live_server_sharing_the_pid(monkeypatch): + # Crash leaves studio-8888-1234.pid, the OS reuses 1234 for a new server on + # another port. Keeping only the first timestamp would reject the live one. + monkeypatch.setattr(run, "_pid_is_studio_backend", _REAL_IS_STUDIO_BACKEND) + monkeypatch.setattr(run, "_process_create_time", lambda pid: 999.0) + + assert run._pid_is_studio_backend(1234, [111.5, 999.0]) is True + assert run._pid_is_studio_backend(1234, [111.5, 222.5]) is False + + +def test_a_stale_record_on_another_port_does_not_hide_a_live_server(tmp_path, monkeypatch): + # 1234 was reused: the stale 8888 record must not stop us seeing 9000. + monkeypatch.setattr(run, "_pid_is_studio_backend", _REAL_IS_STUDIO_BACKEND) + monkeypatch.setattr(run, "_process_create_time", lambda pid: 999.0) + (tmp_path / "studio-8888-1234.pid").write_text("1234\n111.5\n", encoding = "utf-8") + (tmp_path / "studio-9000-1234.pid").write_text("1234\n999.0\n", encoding = "utf-8") + + assert run._own_studio_on_port(8888, "127.0.0.1") is None + assert run._own_studio_on_port(9000, "127.0.0.1") == 1234 + + +def test_a_start_time_is_the_only_thing_that_disproves_a_record(monkeypatch): + monkeypatch.setattr(run, "_pid_is_studio_backend", _REAL_IS_STUDIO_BACKEND) + monkeypatch.setattr(run, "_process_create_time", lambda pid: 999.0) + + assert run._pid_is_studio_backend(8550, [999.0]) is True + assert run._pid_is_studio_backend(8550, [111.5]) is False + + +def test_a_bare_run_py_command_line_is_not_rejected(monkeypatch): + # `cd studio/backend && python run.py --port 8901` has no "studio" or "unsloth" + # in argv. Guessing from the command line called that "not ours". + monkeypatch.setattr(run, "_pid_is_studio_backend", _REAL_IS_STUDIO_BACKEND) + + class _FakeProcess: + def __init__(self, pid): + self.pid = pid + + def cmdline(self): + return ["python", "run.py", "--port", "8901"] + + def create_time(self): + return 111.5 + + monkeypatch.setitem(sys.modules, "psutil", SimpleNamespace(Process = _FakeProcess)) + + assert run._pid_is_studio_backend(8550) is True + + +def test_an_untimed_legacy_record_is_trusted(monkeypatch): + # `python run.py --port 8901` has no telltale argv, so guessing from the + # command line rejected real servers. Only a start time can disprove one. + monkeypatch.setattr(run, "_pid_is_studio_backend", _REAL_IS_STUDIO_BACKEND) + monkeypatch.setattr(run, "_process_create_time", lambda pid: 999.0) + + assert run._pid_is_studio_backend(8550) is True + assert run._pid_is_studio_backend(8550, [None]) is True + + +def test_the_untimed_legacy_record_does_not_cancel_a_timed_one(monkeypatch): + # Mirrors _pid_is_studio_server in the CLI. An untimed record carries no + # information, so it must not overrule a start time that says "not ours" -- + # every current server writes one of each, which made the check inert. + monkeypatch.setattr(run, "_pid_is_studio_backend", _REAL_IS_STUDIO_BACKEND) + monkeypatch.setattr(run, "_process_create_time", lambda pid: 999.0) + + assert run._pid_is_studio_backend(8550, [111.5, None]) is False + assert run._pid_is_studio_backend(8550, [111.5, 999.0]) is True + + +def test_a_legacy_server_on_the_port_is_recognised(tmp_path, monkeypatch): + # Pre-upgrade servers wrote only studio.pid. Falling back past one strands it + # and then overwrites its record. + monkeypatch.setattr(run, "_get_pid_on_port", lambda p: (8550, "python")) + (tmp_path / "studio.pid").write_text("8550", encoding = "utf-8") + + assert run._own_studio_on_port(8901, "127.0.0.1") == 8550 + + +def test_a_legacy_record_for_a_different_listener_falls_back(tmp_path, monkeypatch): + # jupyter holds the port; the legacy server is elsewhere. Keep falling back. + monkeypatch.setattr(run, "_get_pid_on_port", lambda p: (117, "jupyter-lab")) + (tmp_path / "studio.pid").write_text("8550", encoding = "utf-8") + + assert run._own_studio_on_port(8901, "127.0.0.1") is None + + +def test_an_unknowable_listener_treats_the_legacy_record_as_ours(tmp_path, monkeypatch): + # No psutil: _get_pid_on_port can't say. Refusing beats a silent duplicate. + monkeypatch.setattr(run, "_get_pid_on_port", lambda p: None) + (tmp_path / "studio.pid").write_text("8550", encoding = "utf-8") + + assert run._own_studio_on_port(8901, "127.0.0.1") == 8550 + + +def test_a_dead_legacy_record_falls_back(tmp_path, monkeypatch): + monkeypatch.setattr(run, "_pid_alive", lambda pid: False) + monkeypatch.setattr(run, "_get_pid_on_port", lambda p: None) + (tmp_path / "studio.pid").write_text("8550", encoding = "utf-8") + + assert run._own_studio_on_port(8901, "127.0.0.1") is None + + +def test_a_stale_per_port_record_does_not_mask_a_legacy_server(tmp_path, monkeypatch): + # Crashed current build left studio-8901-8550.pid; 8550 was then reused by a + # pre-upgrade server recorded only in studio.pid. The stale record must not + # count as "port already known" and send us falling back past the live one. + monkeypatch.setattr(run, "_pid_is_studio_backend", _REAL_IS_STUDIO_BACKEND) + monkeypatch.setattr(run, "_process_create_time", lambda pid: 999.0) + monkeypatch.setattr(run, "_get_pid_on_port", lambda p: (8550, "python")) + (tmp_path / "studio-8901-8550.pid").write_text("8550\n111.5\n127.0.0.1", encoding = "utf-8") + (tmp_path / "studio.pid").write_text("8550", encoding = "utf-8") + + assert run._own_studio_on_port(8901, "127.0.0.1") == 8550 + + +def test_a_current_server_elsewhere_does_not_block_a_foreign_port(tmp_path, monkeypatch): + # Current builds write studio.pid too. Without psutil the legacy check can't + # see the listener, so it must not claim our 8901 server holds jupyter's 8888. + monkeypatch.setattr(run, "_get_pid_on_port", lambda p: None) + (tmp_path / "studio-8901-5000.pid").write_text("5000\n\n127.0.0.1", encoding = "utf-8") + (tmp_path / "studio.pid").write_text("5000", encoding = "utf-8") + + assert run._own_studio_on_port(8888, "127.0.0.1") is None + + +def test_a_per_port_record_is_preferred_over_the_legacy_one(tmp_path, monkeypatch): + monkeypatch.setattr(run, "_get_pid_on_port", lambda p: (8550, "python")) + (tmp_path / "studio-8901-8600.pid").write_text("8600\n\n127.0.0.1", encoding = "utf-8") + (tmp_path / "studio.pid").write_text("8550", encoding = "utf-8") + + assert run._own_studio_on_port(8901, "127.0.0.1") == 8600 + + +def test_our_studio_on_another_bind_address_does_not_abort(tmp_path): + # Our server holds ::1:8889; binding 127.0.0.1:8889 is not a conflict with us, + # so fall through to the next port instead of refusing. + (tmp_path / "studio-8889-8550.pid").write_text("8550\n\n::1", encoding = "utf-8") + + assert run._own_studio_on_port(8889, "127.0.0.1") is None + assert run._own_studio_on_port(8889, "::1") == 8550 + + +def test_address_matching(tmp_path): + assert run._addresses_collide("0.0.0.0", "127.0.0.1", 8889) is True + assert run._addresses_collide("127.0.0.1", "0.0.0.0", 8889) is True + assert run._addresses_collide("127.0.0.1", "127.0.0.1", 8889) is True + assert run._addresses_collide("::1", "127.0.0.1", 8889) is False + # An unrecorded address is unknown, so assume a conflict. + assert run._addresses_collide(None, "127.0.0.1", 8889) is True + + +def test_a_hostname_resolves_the_same_way_the_bind_does(tmp_path): + # `localhost` and the address _is_port_free actually binds must agree, or a + # recorded server is missed and a duplicate starts. + recorded = ",".join(sorted(run._bind_addresses("localhost", 8889))) + + assert run._addresses_collide(recorded, "localhost", 8889) is True + + +def test_a_hostname_records_every_address_it_resolves_to(tmp_path): + # `localhost` binds 127.0.0.1 AND ::1. Recording only the first lets a later + # launch on the other literal miss us and start a duplicate. + addrs = run._bind_addresses("localhost", 8889) + recorded = ",".join(sorted(addrs)) + + for literal in addrs: + assert run._addresses_collide(recorded, literal, 8889) is True + + +def test_a_multi_address_record_matches_either_literal(tmp_path): + recorded = "127.0.0.1,::1" + + assert run._addresses_collide(recorded, "127.0.0.1", 8889) is True + assert run._addresses_collide(recorded, "::1", 8889) is True + assert run._addresses_collide("127.0.0.1", "::1", 8889) is False + + +def test_fallback_aborts_on_our_own_server_further_up_the_range(tmp_path, monkeypatch): + # jupyter holds 8888, our server holds 8889: skipping to 8890 is the duplicate. + (tmp_path / "studio-8889-8550.pid").write_text("8550\n\n127.0.0.1", encoding = "utf-8") + monkeypatch.setattr(run, "_is_port_free", lambda host, p: p >= 8890) + + with pytest.raises(SystemExit) as excinfo: + run._find_free_port("127.0.0.1", 8889, avoid_own_studio = True) + + assert excinfo.value.code == 1 + + +def test_fallback_still_skips_foreign_processes(tmp_path, monkeypatch): + # No record for 8889, so the blocker is not ours: keep falling back. + monkeypatch.setattr(run, "_is_port_free", lambda host, p: p >= 8890) + + assert run._find_free_port("127.0.0.1", 8889, avoid_own_studio = True) == 8890 + + +def test_the_requested_port_is_kept_when_it_is_free(monkeypatch): + monkeypatch.setattr(run, "_is_port_free", lambda host, p: True) + + assert run._resolve_port("127.0.0.1", 8888) == 8888 + + +def test_our_own_server_on_the_requested_port_aborts_rather_than_falling_back( + tmp_path, monkeypatch +): + # The reported bug: 8888 is ours, so falling back to 8889 is the duplicate + # that leaves 8888 serving with nothing recording it. + monkeypatch.setattr(run, "_is_port_free", lambda host, p: p != 8888) + (tmp_path / "studio-8888-8550.pid").write_text("8550\n\n127.0.0.1", encoding = "utf-8") + + with pytest.raises(SystemExit) as excinfo: + run._resolve_port("127.0.0.1", 8888) + + assert excinfo.value.code == 1 + + +def test_a_foreign_process_on_the_requested_port_still_falls_back(monkeypatch): + # jupyter-lab on 8888 must not stop Unsloth starting on 8889. + monkeypatch.setattr(run, "_is_port_free", lambda host, p: p != 8888) + + assert run._resolve_port("127.0.0.1", 8888) == 8889 + + +def test_a_caller_that_reads_the_port_back_keeps_the_plain_fallback(tmp_path, monkeypatch): + # api-only callers (the desktop app via TAURI_PORT, `studio run` via + # app.state.server_port) follow us to the new port, so aborting there only + # turns a working launch into a crash the desktop app reports as "stopped + # unexpectedly". Both servers are still recorded, so `stop` finds them. + monkeypatch.setattr(run, "_is_port_free", lambda host, p: p != 8888) + (tmp_path / "studio-8888-8550.pid").write_text("8550\n\n127.0.0.1", encoding = "utf-8") + + assert run._resolve_port("127.0.0.1", 8888, avoid_own_studio = False) == 8889 + + +def test_the_recorded_address_is_every_address_the_bind_resolves_to(tmp_path): + # The only test that runs the writer with a real host. Recording `host` + # verbatim, or dropping the line, passes every other test here and silently + # stops matching a launch that spells the same interface differently. + run._write_pid_file(8901, "localhost") + + record = run._read_pid_record(tmp_path / f"studio-8901-{os.getpid()}.pid") + + assert record[2] is not None, "no bind address recorded" + assert set(record[2].split(",")) == run._bind_addresses("localhost", 8901) + + +def test_a_server_started_on_a_hostname_is_found_again_by_ip(tmp_path): + run._write_pid_file(8901, "localhost") + + for literal in run._bind_addresses("localhost", 8901): + assert run._own_studio_on_port(8901, literal) == os.getpid() + + +def test_bind_addresses_keeps_every_family_a_hostname_resolves_to(monkeypatch): + # Independent oracle: the sibling test derives its expectation from this + # function's own output, so dropping a family would pass it. + import socket + monkeypatch.setattr( + socket, + "getaddrinfo", + lambda *a, **k: [ + (socket.AF_INET, socket.SOCK_STREAM, 6, "", ("127.0.0.1", 8889)), + (socket.AF_INET6, socket.SOCK_STREAM, 6, "", ("::1", 8889, 0, 0)), + ], + ) + + assert run._bind_addresses("localhost", 8889) == {"127.0.0.1", "::1"} + + +def test_the_legacy_file_is_written_even_when_the_per_port_record_fails(tmp_path, monkeypatch): + # A studio root that cannot take a new entry used to leave the server + # recorded nowhere at all, so the CLI could not stop it. studio.pid is an + # overwrite of an existing path, so it can still succeed and must be tried. + blocked = tmp_path / "not-a-directory" + blocked.write_text("", encoding = "utf-8") + monkeypatch.setattr( + run, "_pid_file_for_port", lambda port: blocked / f"studio-{port}-{os.getpid()}.pid" + ) + + run._write_pid_file(8901, "127.0.0.1") + + assert (tmp_path / "studio.pid").read_text(encoding = "utf-8") == str(os.getpid()) + assert run._OWN_PID_FILE is None + + +def test_a_record_whose_pid_is_not_ascii_digits_is_discarded(tmp_path): + # A superscript two passes isdigit() but int() rejects it, so that gate alone + # let a ValueError escape into every caller of _read_pid_record. + (tmp_path / "r.pid").write_text("²", encoding = "utf-8") + + assert run._read_pid_record(tmp_path / "r.pid") is None + + +def test_the_legacy_file_is_not_taken_from_a_live_server(tmp_path): + # A pre-upgrade server is recorded in studio.pid and nowhere else, so a + # second launch overwriting it is exactly what strands it. That is the + # orphan this file exists to prevent, reached from the other direction. + (tmp_path / "studio.pid").write_text("8550", encoding = "utf-8") + + run._write_pid_file(8902, "127.0.0.1") + + assert (tmp_path / "studio.pid").read_text(encoding = "utf-8") == "8550" + assert (tmp_path / f"studio-8902-{os.getpid()}.pid").exists() + + +def test_the_legacy_file_is_taken_over_from_a_dead_server(tmp_path, monkeypatch): + # A stale record must not keep the pointer forever, or an older CLI could + # never stop anything again. + monkeypatch.setattr(run, "_pid_alive", lambda pid: False) + (tmp_path / "studio.pid").write_text("8550", encoding = "utf-8") + + run._write_pid_file(8902, "127.0.0.1") + + assert (tmp_path / "studio.pid").read_text(encoding = "utf-8") == str(os.getpid()) diff --git a/studio/frontend/src/features/chat/components/deep-research-composer-button.tsx b/studio/frontend/src/features/chat/components/deep-research-composer-button.tsx index 03a7d7cc5f..e4857d1603 100644 --- a/studio/frontend/src/features/chat/components/deep-research-composer-button.tsx +++ b/studio/frontend/src/features/chat/components/deep-research-composer-button.tsx @@ -14,7 +14,8 @@ import { } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { cn } from "@/lib/utils"; -import { ChevronDownIcon, XIcon } from "lucide-react"; +import { ChevronDownStandardIcon } from "@/lib/chevron-icons"; +import { XIcon } from "lucide-react"; import { type KeyboardEvent, useState } from "react"; import { useChatRuntimeStore } from "../stores/chat-runtime-store"; import type { ResearchWebsitePolicy } from "../types/research"; @@ -24,7 +25,12 @@ function normalizeDomain(raw: string): string | null { if (!value || /[\\\s]/.test(value)) return null; try { const url = new URL(value.includes("://") ? value : `https://${value}`); - if (!/^https?:$/.test(url.protocol) || url.username || url.password || url.port) { + if ( + !/^https?:$/.test(url.protocol) || + url.username || + url.password || + url.port + ) { return null; } return url.hostname @@ -99,7 +105,9 @@ function DomainList({ type="button" className="text-muted-foreground transition-colors hover:text-foreground" aria-label={`Remove ${domain}`} - onClick={() => onChange(values.filter((value) => value !== domain))} + onClick={() => + onChange(values.filter((value) => value !== domain)) + } > @@ -129,7 +137,9 @@ export function DeepResearchComposerButton({ onConfigure: () => void; }) { const enabled = useChatRuntimeStore((state) => state.deepResearchEnabled); - const setEnabled = useChatRuntimeStore((state) => state.setDeepResearchEnabled); + const setEnabled = useChatRuntimeStore( + (state) => state.setDeepResearchEnabled, + ); if (!enabled) return null; @@ -158,9 +168,12 @@ export function DeepResearchComposerButton({ Deep research - - - + {/* Same caret as the other composer pills, so the arrows match. */} + ); } @@ -173,7 +186,9 @@ export function DeepResearchWebsiteAccessDialog({ onOpenChange: (open: boolean) => void; }) { const policy = useChatRuntimeStore((state) => state.researchWebsitePolicy); - const setPolicy = useChatRuntimeStore((state) => state.setResearchWebsitePolicy); + const setPolicy = useChatRuntimeStore( + (state) => state.setResearchWebsitePolicy, + ); return ( @@ -201,41 +216,40 @@ function DeepResearchWebsiteAccessContent({ return ( - - Website access - - Control which websites the next Deep Research run can search and - read. Limits are enforced by the server and shared with the research - model. - - -
- setDraft({ ...draft, allowedDomains })} - /> - setDraft({ ...draft, blockedDomains })} - /> -
- - - - + + Website access + + Control which websites the next Deep Research run can search and read. + Limits are enforced by the server and shared with the research model. + + +
+ setDraft({ ...draft, allowedDomains })} + /> + setDraft({ ...draft, blockedDomains })} + /> +
+ + + +
); } diff --git a/tests/studio/test_cli_studio_stop_windows.py b/tests/studio/test_cli_studio_stop_windows.py index 2267d7feda..cef7cc6db7 100644 --- a/tests/studio/test_cli_studio_stop_windows.py +++ b/tests/studio/test_cli_studio_stop_windows.py @@ -44,9 +44,12 @@ def _load_pid_alive(platform: str, fake_run = None): # ── AST: stop() must not use the broken bare liveness probe ────────────────── -def test_stop_does_not_use_bare_oskill_liveness_probe(): - """stop() must not call os.kill(pid, 0) -- it crashes on Windows.""" - stop_src = _func_source("stop") +# `stop` delegates signalling to `_signal_stop`, so guarding only `stop` would +# let os.kill(pid, 0) come back one function along and still pass. +@pytest.mark.parametrize("func", ["stop", "_signal_stop"]) +def test_stop_does_not_use_bare_oskill_liveness_probe(func): + """The signalling path must not call os.kill(pid, 0) -- WinError 87 on Windows.""" + stop_src = _func_source(func) tree = ast.parse(stop_src) for call in ast.walk(tree): if not isinstance(call, ast.Call): @@ -62,14 +65,17 @@ def test_stop_does_not_use_bare_oskill_liveness_probe(): sig = call.args[1] if isinstance(sig, ast.Constant) and sig.value == 0: raise AssertionError( - "stop() still uses os.kill(pid, 0); it raises WinError 87 on " - "Windows. Use the cross-platform _pid_alive() helper instead." + f"{func}() still uses os.kill(pid, 0); it raises WinError 87 " + "on Windows. Use the cross-platform _pid_alive() helper." ) def test_pid_alive_helper_is_defined_and_used_by_stop(): assert "def _pid_alive(" in _SOURCE, "_pid_alive helper missing" assert "_pid_alive(pid)" in _func_source("stop"), "stop() must use _pid_alive" + # The kill itself moved into _signal_stop; keep both ends of the path pinned. + assert "def _signal_stop(" in _SOURCE, "_signal_stop helper missing" + assert "taskkill" in _func_source("_signal_stop") # The helper must special-case Windows via tasklist (os.kill(pid,0) is invalid there). helper = _func_source("_pid_alive") assert 'sys.platform == "win32"' in helper diff --git a/tests/studio/test_studio_pid_file_contract.py b/tests/studio/test_studio_pid_file_contract.py new file mode 100644 index 0000000000..23ace706b5 --- /dev/null +++ b/tests/studio/test_studio_pid_file_contract.py @@ -0,0 +1,73 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. + +"""run.py writes the Studio PID files; `unsloth studio stop` globs for them. + +Nothing else ties the writer's filename to the reader's glob, and each side's own +tests hardcode the names they expect, so a rename on either side alone leaves both +suites green while `stop` silently finds nothing. `unsloth_cli/tests/` also runs +in no workflow, so this lives here, where the repo CPU job discovers it. + +AST + exec of the writer, so no backend dependency stack is imported. +""" + +import ast +import os +import sys +from pathlib import Path + +_ROOT = Path(__file__).resolve().parents[2] +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +_RUN_SRC = (_ROOT / "studio" / "backend" / "run.py").read_text(encoding = "utf-8") + + +def _func_source(source: str, name: str) -> str: + for node in ast.walk(ast.parse(source)): + if isinstance(node, ast.FunctionDef) and node.name == name: + return ast.get_source_segment(source, node) + raise AssertionError(f"function {name!r} not found") + + +def _backend_pid_path(root: Path, port: int) -> Path: + """The path run.py's own _pid_file_for_port builds, without importing run.py.""" + ns = {"os": os, "Path": Path, "_studio_root": lambda: root} + exec(_func_source(_RUN_SRC, "_pid_file_for_port"), ns) + return ns["_pid_file_for_port"](port) + + +def test_stop_finds_a_pid_file_named_the_way_the_backend_writes_it(tmp_path, monkeypatch): + from unsloth_cli.commands import studio as cli + + path = _backend_pid_path(tmp_path, 8901) + # The same three-line body _write_pid_file emits (create_time is blank when + # psutil is unavailable, and the CLI must tolerate that). + path.write_text(f"{os.getpid()}\n\n127.0.0.1", encoding = "utf-8") + + monkeypatch.setattr(cli, "STUDIO_HOME", tmp_path) + monkeypatch.setattr(cli, "_PID_FILE", tmp_path / "studio.pid") + + assert [pid for pid, _times, _files in cli._pid_file_entries()] == [os.getpid()] + + +def test_the_legacy_file_stays_a_bare_pid_an_older_cli_can_parse(tmp_path, monkeypatch): + # An older `unsloth studio stop` reads studio.pid and requires str.isdigit(), + # so the compatibility file must never gain the extra metadata lines. + ns = { + "os": os, + "Path": Path, + "_studio_root": lambda: tmp_path, + "_PID_FILE": tmp_path / "studio.pid", + "_pid_file_for_port": lambda port: _backend_pid_path(tmp_path, port), + "_process_create_time": lambda pid: None, + "_bind_addresses": lambda host, port: {host}, + # _write_pid_file consults these before taking over studio.pid. + "_read_pid_record": lambda path: None, + "_pid_alive": lambda pid: False, + "_OWN_PID_FILE": None, + } + exec(_func_source(_RUN_SRC, "_write_pid_file"), ns) + ns["_write_pid_file"](8901, "127.0.0.1") + + assert (tmp_path / "studio.pid").read_text(encoding = "utf-8").strip().isdigit() diff --git a/tests/test_mapper_no_duplicate_keys.py b/tests/test_mapper_no_duplicate_keys.py new file mode 100644 index 0000000000..42e49415fd --- /dev/null +++ b/tests/test_mapper_no_duplicate_keys.py @@ -0,0 +1,53 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. + +"""Guard against duplicate keys in the ``__INT_TO_FLOAT_MAPPER`` registry. + +Duplicate keys in the dict literal silently overwrite earlier entries. +We inspect the source with ``ast`` to ensure there are no duplicates. +""" + +import ast +import os + +MAPPER_PATH = os.path.join(os.path.dirname(__file__), os.pardir, "unsloth", "models", "mapper.py") + + +def _duplicate_int_to_float_keys(): + with open(MAPPER_PATH, encoding = "utf-8") as f: + tree = ast.parse(f.read(), MAPPER_PATH) + + for node in ast.walk(tree): + if not isinstance(node, ast.Assign): + continue + for target in node.targets: + # Private names are mangled at code generation, which ``ast.parse`` + # never reaches, so the identifier reads exactly as written. + if isinstance(target, ast.Name) and target.id == "__INT_TO_FLOAT_MAPPER": + if not isinstance(node.value, ast.Dict): + continue + # mapper.py reads the nested per-precision dicts directly, so + # check every dict. Count per dict: "16" and "8" legitimately + # repeat across sibling entries. + duplicates = {} + for mapping in ast.walk(node.value): + if not isinstance(mapping, ast.Dict): + continue + seen = set() + for k in mapping.keys: + if not (isinstance(k, ast.Constant) and isinstance(k.value, str)): + continue + if k.value in seen: + duplicates.setdefault(k.value, []).append(k.lineno) + seen.add(k.value) + return duplicates + raise AssertionError("Could not find the __INT_TO_FLOAT_MAPPER dict literal in mapper.py") + + +def test_int_to_float_mapper_has_no_duplicate_keys(): + duplicates = _duplicate_int_to_float_keys() + assert not duplicates, ( + "Duplicate keys in __INT_TO_FLOAT_MAPPER silently overwrite earlier " + "entries and corrupt model resolution. Remove the redundant " + f"literal(s), key -> line number(s) in mapper.py: {duplicates}" + ) diff --git a/unsloth/models/mapper.py b/unsloth/models/mapper.py index 4558bb0f28..747b3bb986 100644 --- a/unsloth/models/mapper.py +++ b/unsloth/models/mapper.py @@ -94,10 +94,6 @@ __INT_TO_FLOAT_MAPPER = \ "unsloth/llama-2-7b-chat", "meta-llama/Llama-2-7b-chat-hf", ), - "unsloth/llama-2-7b-chat-bnb-4bit" : ( - "unsloth/llama-2-7b-chat", - "meta-llama/Llama-2-7b-chat-hf", - ), "unsloth/Mixtral-8x7B-v0.1-unsloth-bnb-4bit" : ( "unsloth/Mixtral-8x7B-v0.1", "mistralai/Mixtral-8x7B-v0.1", diff --git a/unsloth_cli/commands/studio.py b/unsloth_cli/commands/studio.py index 9fd264ddf5..560df4aea2 100644 --- a/unsloth_cli/commands/studio.py +++ b/unsloth_cli/commands/studio.py @@ -19,7 +19,7 @@ import urllib.error import urllib.request from datetime import datetime, timezone from pathlib import Path -from typing import List, Literal, Optional +from typing import List, Literal, Optional, Sequence import typer from unsloth_cli import _studio_deps @@ -483,9 +483,12 @@ def _write_auth_secret(path: Path, secret: str) -> None: os.chmod(tmp_path, 0o600) except OSError: pass - with os.fdopen(fd, "w", encoding = "utf-8") as f: + # newline pins LF: text mode writes CRLF on Windows, and `$(cat ...)` + # strips the LF but leaves the CR glued to the credential. + with os.fdopen(fd, "w", encoding = "utf-8", newline = "\n") as f: fd = -1 - f.write(secret) + # Newline so `cat` doesn't run it into the shell prompt; readers strip. + f.write(secret + "\n") os.replace(tmp_path, path) except Exception: if fd >= 0: @@ -502,6 +505,8 @@ def _connect_auth_db() -> sqlite3.Connection: auth_dir = STUDIO_HOME / "auth" auth_dir.mkdir(parents = True, exist_ok = True) conn = sqlite3.connect(auth_dir / "auth.db") + # A live server writes this DB while the CLI runs; the default lock wait is zero. + conn.execute("PRAGMA busy_timeout=5000") # Mirror backend storage.get_connection: this path can create auth/ and # auth.db (the pre-exposure gate writes here first), and sqlite3.connect # makes the DB 0644 under a 022 umask. Keep both private. @@ -529,7 +534,8 @@ def _connect_auth_db() -> sqlite3.Connection: token_hash TEXT NOT NULL, username TEXT NOT NULL, expires_at TEXT NOT NULL, - is_desktop INTEGER NOT NULL DEFAULT 0 + is_desktop INTEGER NOT NULL DEFAULT 0, + secret_gen TEXT ); """ ) @@ -564,6 +570,8 @@ def _connect_auth_db() -> sqlite3.Connection: refresh_columns = {row[1] for row in conn.execute("PRAGMA table_info(refresh_tokens)")} if "is_desktop" not in refresh_columns: conn.execute("ALTER TABLE refresh_tokens ADD COLUMN is_desktop INTEGER NOT NULL DEFAULT 0") + if "secret_gen" not in refresh_columns: + conn.execute("ALTER TABLE refresh_tokens ADD COLUMN secret_gen TEXT") conn.commit() return conn @@ -697,12 +705,30 @@ def _bootstrap_deadline_active() -> bool: return True -def _cli_update_password(conn: sqlite3.Connection, username: str, new_password: str) -> None: +def _generate_reset_password() -> str: + """Readable 4-word passphrase; the user has to type this one back in.""" + try: + import diceware + return diceware.get_passphrase( + options = diceware.handle_options(args = ["-n", "4", "-d", "", "-c"]) + ) + except Exception: + return secrets.token_urlsafe(24) + + +def _cli_update_password( + conn: sqlite3.Connection, + username: str, + new_password: str, + *, + revoke_api_keys: bool = False, +) -> None: """CLI mirror of backend update_password + change-password route effects. One transaction: rehash, rotate the JWT secret, clear must_change_password, - revoke refresh tokens (PR #6651 finding), and drop the desktop secret. File - cleanup happens after commit; a failed unlink must not roll the change back. + revoke refresh tokens (PR #6651 finding), drop the desktop secret, and (for a + reset) the API keys the old credential could have minted. File cleanup happens + after commit; a failed unlink must not roll the change back. """ password_salt, password_hash = _hash_password(new_password) with conn: @@ -719,6 +745,8 @@ def _cli_update_password(conn: sqlite3.Connection, username: str, new_password: "DELETE FROM app_secrets WHERE key IN (?, ?)", (DESKTOP_SECRET_HASH_KEY, DESKTOP_SECRET_CREATED_AT_KEY), ) + if revoke_api_keys: + conn.execute("DELETE FROM api_keys") for stale in (BOOTSTRAP_PASSWORD_FILE, DESKTOP_SECRET_FILE): stale_path = STUDIO_HOME / "auth" / stale try: @@ -728,8 +756,8 @@ def _cli_update_password(conn: sqlite3.Connection, username: str, new_password: # change back. But a locked-yet-writable file (Windows AV, read-only # auth dir) must be truncated: otherwise its stale plaintext survives # and generate_bootstrap_password() would re-validate this revoked - # credential after a later reset-password deletes auth.db. Mirrors - # backend clear_bootstrap_password(). + # credential if auth.db is ever recreated. Mirrors backend + # clear_bootstrap_password(). try: stale_path.write_text("", encoding = "utf-8") cleared = True @@ -787,8 +815,8 @@ def _apply_supplied_password_before_launch(supplied_password: "str | None") -> N if not row[2]: typer.echo( "Error: an Unsloth admin password is already set; --password only sets " - "the initial password. Run `unsloth studio reset-password` first " - "(or change it in the UI).", + "the initial password. Change it in the UI, or run `unsloth studio " + "reset-password` for a new one.", err = True, ) raise typer.Exit(1) @@ -2237,6 +2265,9 @@ def run( # Headless serving prints its own URL/API-key banner; the Tauri-only # TAURI_PORT line would corrupt that machine-parseable output. emit_tauri_port = False, + # We read the bound port back below, so a fallback past another Studio is + # safe here and keeps side-by-side model runs working. + abort_if_own_studio = False, ) # Forward the frontend validated before the gate (in-venv path). if resolved_frontend is not None: @@ -2396,6 +2427,7 @@ def run( # ── unsloth studio stop ─────────────────────────────────────────────── _PID_FILE = STUDIO_HOME / "studio.pid" +PID_FILE_GLOB = "studio-*.pid" def _pid_alive(pid: int) -> bool: @@ -2425,58 +2457,210 @@ def _pid_alive(pid: int) -> bool: return True -@studio_app.command() -def stop(): - """Stop a running Unsloth Studio server. +def _parse_pid_record(text: str) -> "tuple[int, float | None] | None": + """Parse ``pid`` / optional ``create_time`` from PID file contents.""" + lines = text.splitlines() + if not lines or not lines[0].strip().isdigit(): + return None + try: + # isdigit() is not enough: "²".isdigit() is True but int() rejects it. + pid = int(lines[0].strip()) + except ValueError: + return None + # kill(0) signals our whole process group; kill(1) is init. Never either. + if pid < 2: + return None + created = None + if len(lines) > 1: + try: + created = float(lines[1].strip()) + except ValueError: + created = None + return pid, created - Reads the PID from ~/.unsloth/studio/studio.pid and sends SIGTERM - (or TerminateProcess on Windows) to shut it down gracefully. + +def _read_pid_record(path: Path) -> "tuple[int, float | None] | None": + """Parse ``pid`` / optional ``create_time`` from a PID file.""" + try: + text = path.read_text(encoding = "utf-8") + except (OSError, UnicodeDecodeError): + return None + return _parse_pid_record(text) + + +def _unlink_quietly(path: Path) -> None: + """Drop a record without letting one bad file end the loop. + + An undeletable record must not stop us reaching the other servers -- that is + the orphan this command exists to prevent. """ + try: + path.unlink(missing_ok = True) + except OSError as e: + typer.echo(f"Could not remove PID file {path.name}: {e}", err = True) + + +def _report_unreadable(paths: "list[Path]") -> None: + """Say which servers we could not reach, since `stop` is about to exit 1.""" + names = ", ".join(sorted(p.name for p in paths)) + typer.echo( + f"Could not read {len(paths)} PID file(s): {names}. A server recorded " + f"there may still be running; re-run with permission to read " + f"{STUDIO_HOME} to stop it.", + err = True, + ) + + +def _pid_file_entries( + unreadable: "list[Path] | None" = None, +) -> "list[tuple[int, list[float | None], list[Path]]]": + """(pid, create_times, files) per recorded server, including the legacy studio.pid. + + Paths that could not be read are appended to `unreadable` when given, so the + caller can tell "nothing is running" apart from "something is running and we + could not see it". + + Grouped by PID: a server writes both its per-port file and studio.pid, and + signalling twice would hit the SIG_DFL the first SIGTERM installs, hard-killing + it mid-shutdown. Every recorded time is kept -- a stale file and a live server + can share a PID, and the stale one must not veto the live one. + """ + by_pid: "dict[int, tuple[list[float | None], list[Path]]]" = {} + try: + paths = sorted(STUDIO_HOME.glob(PID_FILE_GLOB)) + [_PID_FILE] + except OSError: + paths = [_PID_FILE] + seen = set() + for path in paths: + if path in seen or not path.is_file(): + continue + seen.add(path) + try: + text = path.read_text(encoding = "utf-8") + except (OSError, UnicodeDecodeError) as e: + # Unreadable is not the same as invalid. A root-owned record, or one + # caught mid-write, still belongs to a live server, and deleting it + # strands that server -- the bug this command exists to fix. + typer.echo(f"Cannot read PID file {path.name}: {e}", err = True) + if unreadable is not None: + unreadable.append(path) + continue + record = _parse_pid_record(text) + if record is None: + typer.echo(f"Ignoring invalid PID file {path.name}") + _unlink_quietly(path) + continue + pid, created = record + created_times, files = by_pid.setdefault(pid, ([], [])) + created_times.append(created) + files.append(path) + return [(pid, times, files) for pid, (times, files) in by_pid.items()] + + +def _pid_is_studio_server(pid: int, created_times: "Sequence[float | None]" = ()) -> bool: + """False only when a recorded start time proves this PID is a different process. + + Any recorded time matching is enough -- a stale record must not veto a live + server that reused the PID. Records with no time at all (a legacy studio.pid, + or a server started without psutil) cannot be checked, so they are trusted: + the old `stop` signalled with no checks at all, and skipping a live server is + the orphan bug this exists to fix. + + An untimed record sitting *alongside* a timed one carries no information, so + it must not cancel the timed one either. Every current server writes both a + timed per-port record and an untimed studio.pid, so letting the untimed half + win made this check inert exactly where it matters and let `stop` SIGTERM an + unrelated process that had inherited the PID. + """ + known = [c for c in created_times if c is not None] + if not known: + return True + try: + import psutil + actual = psutil.Process(pid).create_time() + except Exception: + return True + return any(abs(actual - c) < 1.0 for c in known) + + +def _signal_stop(pid: int) -> "str | None": + """SIGTERM (or taskkill) the pid. Returns an error string, or None on success.""" import signal as _signal - if not _PID_FILE.is_file(): - typer.echo("No running Unsloth server found (no PID file).") - raise typer.Exit(0) - - pid_text = _PID_FILE.read_text(encoding = "utf-8").strip() - if not pid_text.isdigit(): - typer.echo(f"Invalid PID file contents: {pid_text}") - _PID_FILE.unlink(missing_ok = True) - raise typer.Exit(1) - - pid = int(pid_text) - - # Check if still alive (os.kill(pid, 0) is invalid on Windows -- see _pid_alive). - if not _pid_alive(pid): - typer.echo(f"Unsloth server (PID {pid}) is not running. Cleaning up stale PID file.") - _PID_FILE.unlink(missing_ok = True) - raise typer.Exit(0) - - # Send SIGTERM (graceful shutdown) or TerminateProcess on Windows + if pid < 2: + return f"refusing to signal PID {pid}" try: if sys.platform == "win32": # /T also stops llama-server children, which otherwise keep GPU and port. subprocess.run(["taskkill", "/PID", str(pid), "/T", "/F"], check = True) else: os.kill(pid, _signal.SIGTERM) - typer.echo(f"Sent shutdown signal to Unsloth server (PID {pid}).") except ProcessLookupError: - typer.echo(f"Unsloth server (PID {pid}) already exited.") - _PID_FILE.unlink(missing_ok = True) - raise typer.Exit(0) + return None except Exception as e: - typer.echo(f"Failed to stop Unsloth server (PID {pid}): {e}", err = True) - raise typer.Exit(1) + return str(e) + return None - # Wait briefly for the process to exit and clean up. + +@studio_app.command() +def stop(): + """Stop every running Unsloth Studio server for this STUDIO_HOME. + + The port fallback can leave more than one running, so stop them all. + """ + unreadable: "list[Path]" = [] + entries = _pid_file_entries(unreadable) + if not entries: + if unreadable: + # Reporting success here would be a lie: the records we could not + # read are kept, and the servers behind them are still serving. + _report_unreadable(unreadable) + raise typer.Exit(1) + typer.echo("No running Unsloth server found (no PID file).") + raise typer.Exit(0) + + signalled, failed = [], [] + for pid, created_times, paths in entries: + if not _pid_alive(pid) or not _pid_is_studio_server(pid, created_times): + for path in paths: + _unlink_quietly(path) + continue + error = _signal_stop(pid) + if error is not None: + failed.append((pid, error)) + typer.echo(f"Failed to stop Unsloth server (PID {pid}): {error}", err = True) + continue + typer.echo(f"Sent shutdown signal to Unsloth server (PID {pid}).") + signalled.append((pid, paths)) + + if not signalled and not failed: + if unreadable: + _report_unreadable(unreadable) + raise typer.Exit(1) + typer.echo("No running Unsloth server found (cleaned up stale PID files).") + raise typer.Exit(0) + + pending = list(signalled) for _ in range(10): + if not pending: + break time.sleep(0.5) - if not _pid_alive(pid): - _PID_FILE.unlink(missing_ok = True) - typer.echo("Unsloth server stopped.") - raise typer.Exit(0) + for entry in list(pending): + pid, paths = entry + if not _pid_alive(pid): + for path in paths: + _unlink_quietly(path) + pending.remove(entry) - typer.echo("Unsloth server is shutting down (may take a few seconds).") + stopped = len(signalled) - len(pending) + if stopped: + typer.echo(f"Unsloth server{'s' if stopped > 1 else ''} stopped ({stopped}).") + for pid, _paths in pending: + typer.echo(f"Unsloth server (PID {pid}) is shutting down (may take a few seconds).") + if unreadable: + _report_unreadable(unreadable) + if failed or unreadable: + raise typer.Exit(1) # ── unsloth studio setup / update ───────────────────────────────────── @@ -2890,59 +3074,33 @@ def provision_desktop_auth(): def reset_password(): """Reset the Unsloth admin password. - Deletes the auth database so that a fresh admin account with a new - random password is created on the next server start. The Unsloth - server must be restarted after running this command. + Rotates the credential in place: a running Unsloth accepts the new password on + its next request, so there is nothing to restart. Shared /p preview links are + not revoked -- rotate those in Settings if the old password leaked. """ - auth_dir = STUDIO_HOME / "auth" - db_file = auth_dir / "auth.db" - stale_files = [ - auth_dir / BOOTSTRAP_PASSWORD_FILE, - auth_dir / DESKTOP_SECRET_FILE, - ] - had_db = db_file.exists() - - # Delete auth.db FIRST and prove it is gone before touching the seeded - # credential files. If it cannot be removed (a running Unsloth or Windows - # holds it open, or a read-only auth dir), abort with the credential files - # untouched: deleting them while an un-resettable DB (must_change_password=1) - # survives would lock a forgotten-password reset out of any recovery - # credential. Failing here leaves a consistent, still-recoverable state. + new_password = _generate_reset_password() try: - db_file.unlink(missing_ok = True) - except OSError as exc: + conn = _connect_auth_db() + except (OSError, sqlite3.Error) as exc: typer.echo( - f"Error: could not delete the auth database ({exc}). Stop any running " - "Unsloth and retry; no credential files were changed.", + f"Error: could not open the auth database ({exc}). Check that " + f"{STUDIO_HOME / 'auth'} is writable; if auth.db itself is unreadable, stop " + "Unsloth, delete it, and start again to re-seed.", err = True, ) raise typer.Exit(1) - # The DB is gone, so the next start re-seeds. Invalidate the seeded plaintext - # credential files so that re-seed generates a FRESH password instead of - # reusing a stale one: unlink only ignores FileNotFoundError, so a - # locked/undeletable file (Windows AV, read-only dir) would otherwise survive - # and generate_bootstrap_password() would read it back and re-validate the - # credential this reset revoked. Truncate on unlink failure; if a file can be - # neither removed nor truncated, fail closed -- the DB is already gone, so a - # surviving plaintext would be reused, and the user must remove it manually. - for path in stale_files: - try: - path.unlink(missing_ok = True) - except OSError: - try: - path.write_text("", encoding = "utf-8") - except OSError as exc: - typer.echo( - f"Error: could not remove or clear {path.name} ({exc}); delete " - "it manually before restarting Unsloth or the old password may " - "be reused.", - err = True, - ) - raise typer.Exit(1) + try: + _ensure_cli_default_admin(conn) + _cli_update_password(conn, DEFAULT_ADMIN_USERNAME, new_password, revoke_api_keys = True) + except (OSError, sqlite3.Error) as exc: + typer.echo(f"Error: could not reset the password ({exc}).", err = True) + raise typer.Exit(1) + finally: + conn.close() - if not had_db: - typer.echo("No auth database found -- nothing to reset.") - raise typer.Exit(0) - - typer.echo("Auth database deleted. Restart Unsloth Studio to get a new password.") + typer.echo(f"New password for '{DEFAULT_ADMIN_USERNAME}': {new_password}") + typer.echo( + "Sessions and API keys revoked. A running Unsloth takes it on the next request, " + "though repeated failed logins can hold the rate limit shut for up to a minute." + ) diff --git a/unsloth_cli/tests/conftest.py b/unsloth_cli/tests/conftest.py new file mode 100644 index 0000000000..bb42914e69 --- /dev/null +++ b/unsloth_cli/tests/conftest.py @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +"""Shared fixtures for the unsloth_cli tests.""" + +import sys +import types + +import pytest + + +@pytest.fixture +def stub_tool_policy_state(monkeypatch): + """Stub the backend's `state.tool_policy`, which run() imports in-venv. + + It lives under studio/backend, so it only imports once something has put + that directory on sys.path. Tests that reach the in-venv branch of run() + used to get that for free from whichever file ran earlier and did it as a + side effect, which made them pass only in a full-directory run. + """ + state_mod = types.ModuleType("state") + tp_mod = types.ModuleType("state.tool_policy") + tp_mod.set_tool_policy = lambda *a, **k: None + state_mod.tool_policy = tp_mod + monkeypatch.setitem(sys.modules, "state", state_mod) + monkeypatch.setitem(sys.modules, "state.tool_policy", tp_mod) diff --git a/unsloth_cli/tests/test_studio_password_prompt.py b/unsloth_cli/tests/test_studio_password_prompt.py index 6e9a2c1d52..753c22edc2 100644 --- a/unsloth_cli/tests/test_studio_password_prompt.py +++ b/unsloth_cli/tests/test_studio_password_prompt.py @@ -251,7 +251,7 @@ def test_studio_default_prompt_rejects_current_password(monkeypatch, tmp_path): studio_mod = _studio() events = _install_prompt_env(monkeypatch, tmp_path, interactive = True) _seed_auth(studio_mod) - bootstrap_pw = (tmp_path / "auth" / studio_mod.BOOTSTRAP_PASSWORD_FILE).read_text() + bootstrap_pw = (tmp_path / "auth" / studio_mod.BOOTSTRAP_PASSWORD_FILE).read_text().strip() _invoke_studio_default(monkeypatch, events, ["--secure"]) @@ -626,6 +626,12 @@ def test_studio_default_in_venv_broken_backend_exits_before_stripping_bootstrap( # Pretend we are already inside the studio venv, with a broken backend. monkeypatch.setattr(sys, "prefix", str(tmp_path / "unsloth_studio")) + # A built dist is not present in a fresh clone. The missing-frontend gate + # runs first and has its own test below; stub it so this one reaches the + # backend check it is actually about. + monkeypatch.setattr( + studio_mod, "_find_frontend_dist", lambda: Path("/fake/studio/frontend/dist") + ) def _boom(): raise ImportError("cannot import backend run.py") @@ -957,7 +963,9 @@ def test_run_reexec_forwards_resolved_frontend_on_public_launch(monkeypatch, tmp exec_argv = [argv for kind, argv in events if kind == "exec"][0] assert "--frontend" in exec_argv, exec_argv - assert exec_argv[exec_argv.index("--frontend") + 1] == "/fake/studio/frontend/dist", exec_argv + # str(Path(...)), not the literal: Windows renders it with backslashes. + expected_dist = str(Path("/fake/studio/frontend/dist")) + assert exec_argv[exec_argv.index("--frontend") + 1] == expected_dist, exec_argv def test_run_non_tty_persists_seeded_admin_on_fresh_home(monkeypatch, tmp_path): @@ -1032,50 +1040,173 @@ def test_bootstrap_deadline_active_mirrors_backend_parsing(monkeypatch, raw, exp assert studio_mod._bootstrap_deadline_active() is expected -def test_reset_password_truncates_locked_bootstrap_after_db_delete(monkeypatch, tmp_path): - # reset-password deletes auth.db first, then invalidates the seeded credential - # files. A locked/undeletable .bootstrap_password must be truncated so its - # stale plaintext cannot be re-seeded (generate_bootstrap_password reuses a - # non-empty file), while the reset still succeeds. - import pathlib - - studio_mod = _studio() - monkeypatch.setattr(studio_mod, "STUDIO_HOME", tmp_path) - _seed_auth(studio_mod) - auth_dir = tmp_path / "auth" - bootstrap_file = auth_dir / studio_mod.BOOTSTRAP_PASSWORD_FILE - db_file = auth_dir / "auth.db" - assert bootstrap_file.exists() and db_file.exists() - assert bootstrap_file.read_text().strip() - - _real_unlink = pathlib.Path.unlink - - def _boom_unlink(self, *a, **k): - if self.name == studio_mod.BOOTSTRAP_PASSWORD_FILE: - raise OSError("locked") - return _real_unlink(self, *a, **k) - - monkeypatch.setattr(pathlib.Path, "unlink", _boom_unlink) - +def _reset_password_cli(studio_mod): import typer as _typer app = _typer.Typer() app.command()(studio_mod.reset_password) - result = CliRunner().invoke(app, [], catch_exceptions = True) + return CliRunner().invoke(app, [], catch_exceptions = True) + + +def _password_works(studio_mod, candidate): + conn = studio_mod._connect_auth_db() + try: + row = conn.execute( + "SELECT password_salt, password_hash FROM auth_user WHERE username = ?", + (studio_mod.DEFAULT_ADMIN_USERNAME,), + ).fetchone() + finally: + conn.close() + return studio_mod._pbkdf2_hex(candidate, row[0].encode("utf-8")) == row[1] + + +def _printed_password(result): + line = next(l for l in result.output.splitlines() if l.startswith("New password for")) + return line.split(": ", 1)[1].strip() + + +def test_reset_password_rotates_in_place_without_deleting_the_db(monkeypatch, tmp_path): + # The DB survives, so a running server keeps its admin row and the new password. + studio_mod = _studio() + monkeypatch.setattr(studio_mod, "STUDIO_HOME", tmp_path) + _seed_auth(studio_mod) + db_file = tmp_path / "auth" / "auth.db" + before = _auth_state(studio_mod) + + result = _reset_password_cli(studio_mod) assert result.exit_code == 0, result.output - assert not db_file.exists() - # The locked file survives, but truncated -- no reusable plaintext. - assert bootstrap_file.exists() - assert bootstrap_file.read_text() == "" + assert db_file.exists() + after = _auth_state(studio_mod) + assert after["password_hash"] != before["password_hash"] + assert after["jwt_secret"] != before["jwt_secret"] + assert _password_works(studio_mod, _printed_password(result)) + + +def test_reset_password_waits_out_a_concurrent_writer(monkeypatch, tmp_path): + # The CLI now writes while the server does; without a busy_timeout this fails. + import threading + import time + + studio_mod = _studio() + monkeypatch.setattr(studio_mod, "STUDIO_HOME", tmp_path) + _seed_auth(studio_mod) + released = threading.Event() + + def hold_write_lock(): + conn = sqlite3.connect(_auth_db(tmp_path)) + conn.execute("BEGIN IMMEDIATE") + conn.execute( + "INSERT INTO refresh_tokens (token_hash, username, expires_at) " + "VALUES ('held', 'unsloth', '2099-01-01T00:00:00')" + ) + time.sleep(0.5) + conn.rollback() + conn.close() + released.set() + + holder = threading.Thread(target = hold_write_lock) + holder.start() + time.sleep(0.1) + result = _reset_password_cli(studio_mod) + holder.join() + + assert released.is_set() + assert result.exit_code == 0, result.output + assert _password_works(studio_mod, _printed_password(result)) + + +def test_reset_password_revokes_sessions_and_api_keys(monkeypatch, tmp_path): + # Deleting auth.db used to drop these implicitly. + studio_mod = _studio() + monkeypatch.setattr(studio_mod, "STUDIO_HOME", tmp_path) + _seed_auth(studio_mod) + conn = studio_mod._connect_auth_db() + conn.execute( + "INSERT INTO api_keys (username, key_prefix, key_hash, name, created_at) " + "VALUES (?, 'sk-x', 'hash', 'k', '2026-01-01T00:00:00')", + (studio_mod.DEFAULT_ADMIN_USERNAME,), + ) + conn.commit() + conn.close() + + assert _reset_password_cli(studio_mod).exit_code == 0 + + conn = studio_mod._connect_auth_db() + try: + assert conn.execute("SELECT COUNT(*) FROM api_keys").fetchone()[0] == 0 + assert conn.execute("SELECT COUNT(*) FROM refresh_tokens").fetchone()[0] == 0 + finally: + conn.close() + + +def test_reset_password_leaves_the_account_ready_to_log_in(monkeypatch, tmp_path): + # must_change_password stays 0 on purpose: at 1 a running server injects its + # startup-cached (now wrong) bootstrap password into the login page. + studio_mod = _studio() + monkeypatch.setattr(studio_mod, "STUDIO_HOME", tmp_path) + _seed_auth(studio_mod) + + assert _reset_password_cli(studio_mod).exit_code == 0 + + assert _auth_state(studio_mod)["must_change_password"] == 0 + assert not (tmp_path / "auth" / studio_mod.BOOTSTRAP_PASSWORD_FILE).exists() + + +def test_reset_password_seeds_the_admin_when_no_db_exists(monkeypatch, tmp_path): + studio_mod = _studio() + monkeypatch.setattr(studio_mod, "STUDIO_HOME", tmp_path) + + result = _reset_password_cli(studio_mod) + + assert result.exit_code == 0, result.output + assert _password_works(studio_mod, _printed_password(result)) + + +def test_reset_password_reports_an_unwritable_auth_dir(monkeypatch, tmp_path): + # _connect_auth_db creates auth/ before it opens SQLite, so a read-only Unsloth + # home raises OSError, not sqlite3.Error. + import pathlib + + studio_mod = _studio() + monkeypatch.setattr(studio_mod, "STUDIO_HOME", tmp_path) + + def _boom_mkdir(self, *a, **k): + raise PermissionError("read-only") + + monkeypatch.setattr(pathlib.Path, "mkdir", _boom_mkdir) + + result = _reset_password_cli(studio_mod) + + assert result.exit_code == 1, result.output + assert not isinstance(result.exception, OSError) + combined = (result.output or "") + (getattr(result, "stderr", "") or "") + assert "could not open the auth database" in combined.lower() + + +def test_reset_password_reports_an_unreadable_db(monkeypatch, tmp_path): + # Deleting a corrupt DB here would revive the bug: a running server would be + # left with no admin row, rejecting the correct password until restarted. + studio_mod = _studio() + monkeypatch.setattr(studio_mod, "STUDIO_HOME", tmp_path) + auth_dir = tmp_path / "auth" + auth_dir.mkdir() + (auth_dir / "auth.db").write_text("not a database") + + result = _reset_password_cli(studio_mod) + + assert result.exit_code == 1, result.output + assert (auth_dir / "auth.db").exists() + combined = (result.output or "") + (getattr(result, "stderr", "") or "") + assert "could not open the auth database" in combined.lower() def test_cli_update_password_truncates_locked_bootstrap_after_change(monkeypatch, tmp_path): # After a CLI/interactive password change the seeded .bootstrap_password is # deleted. If it cannot be unlinked but is still writable (locked file / # read-only dir), it must be TRUNCATED so its stale plaintext cannot be - # re-seeded by generate_bootstrap_password() after a later reset-password - # deletes auth.db. The change is already committed, so it must NOT roll back. + # re-seeded by generate_bootstrap_password() if auth.db is ever recreated. The + # change is already committed, so it must NOT roll back. import pathlib studio_mod = _studio() @@ -1103,88 +1234,6 @@ def test_cli_update_password_truncates_locked_bootstrap_after_change(monkeypatch assert bootstrap_file.read_text() == "" -def test_reset_password_fails_closed_when_db_cannot_be_deleted(monkeypatch, tmp_path): - # If auth.db cannot be removed (running Unsloth / Windows lock, read-only dir), - # reset must abort BEFORE touching the credential files -- deleting them while - # an un-resettable must_change_password=1 DB survives would lock a - # forgotten-password reset out with no recovery credential. - import pathlib - - studio_mod = _studio() - monkeypatch.setattr(studio_mod, "STUDIO_HOME", tmp_path) - _seed_auth(studio_mod) - auth_dir = tmp_path / "auth" - bootstrap_file = auth_dir / studio_mod.BOOTSTRAP_PASSWORD_FILE - db_file = auth_dir / "auth.db" - assert bootstrap_file.exists() and db_file.exists() - - _real_unlink = pathlib.Path.unlink - - def _boom_unlink(self, *a, **k): - if self.name == "auth.db": - raise OSError("database is locked") - return _real_unlink(self, *a, **k) - - monkeypatch.setattr(pathlib.Path, "unlink", _boom_unlink) - - import typer as _typer - - app = _typer.Typer() - app.command()(studio_mod.reset_password) - result = CliRunner().invoke(app, [], catch_exceptions = True) - - assert result.exit_code == 1, result.output - # DB still there; credential files untouched (no lockout, no half-done reset). - assert db_file.exists() - assert bootstrap_file.exists() - assert bootstrap_file.read_text().strip() - combined = (result.output or "") + (getattr(result, "stderr", "") or "") - assert "could not delete the auth database" in combined.lower() - - -def test_reset_password_fails_closed_when_credential_cannot_be_invalidated(monkeypatch, tmp_path): - # If a seeded credential file can be neither unlinked nor truncated, reset must - # fail closed: auth.db is already gone, so a surviving plaintext would be - # re-seeded and re-validate the revoked password. - import pathlib - - studio_mod = _studio() - monkeypatch.setattr(studio_mod, "STUDIO_HOME", tmp_path) - _seed_auth(studio_mod) - auth_dir = tmp_path / "auth" - bootstrap_file = auth_dir / studio_mod.BOOTSTRAP_PASSWORD_FILE - db_file = auth_dir / "auth.db" - assert bootstrap_file.exists() and db_file.exists() - - _real_unlink = pathlib.Path.unlink - _real_write_text = pathlib.Path.write_text - - def _boom_unlink(self, *a, **k): - if self.name == studio_mod.BOOTSTRAP_PASSWORD_FILE: - raise OSError("locked") - return _real_unlink(self, *a, **k) - - def _boom_write_text(self, *a, **k): - if self.name == studio_mod.BOOTSTRAP_PASSWORD_FILE: - raise OSError("read-only") - return _real_write_text(self, *a, **k) - - monkeypatch.setattr(pathlib.Path, "unlink", _boom_unlink) - monkeypatch.setattr(pathlib.Path, "write_text", _boom_write_text) - - import typer as _typer - - app = _typer.Typer() - app.command()(studio_mod.reset_password) - result = CliRunner().invoke(app, [], catch_exceptions = True) - - assert result.exit_code == 1, result.output - # auth.db was deleted first; the un-invalidatable file is reported for manual removal. - assert not db_file.exists() - combined = (result.output or "") + (getattr(result, "stderr", "") or "") - assert "delete it manually" in combined.lower() - - def test_connect_auth_db_creates_private_files(monkeypatch, tmp_path): # Fresh install: the CLI gate writes the password hash + JWT secret before # the backend ever runs, so this path must apply the same 0700/0600 modes @@ -1204,6 +1253,37 @@ def test_connect_auth_db_creates_private_files(monkeypatch, tmp_path): assert stat.S_IMODE((auth_dir / "auth.db").stat().st_mode) == 0o600 +def test_write_auth_secret_terminates_the_file_with_a_newline(monkeypatch, tmp_path): + # Shared by .bootstrap_password and .desktop_secret; every reader strips. + studio_mod = _studio() + path = tmp_path / ".desktop_secret" + + studio_mod._write_auth_secret(path, "desktop-abc123") + + # Bytes: read_text would decode CRLF back to "\n" and hide a CR. + assert path.read_bytes() == b"desktop-abc123\n" + + +def test_seeded_bootstrap_file_ends_with_a_newline(monkeypatch, tmp_path): + studio_mod = _studio() + monkeypatch.setattr(studio_mod, "STUDIO_HOME", tmp_path) + _seed_auth(studio_mod) + + raw = (tmp_path / "auth" / studio_mod.BOOTSTRAP_PASSWORD_FILE).read_bytes() + + assert raw.endswith(b"\n") and not raw.endswith(b"\r\n") + + conn = sqlite3.connect(_auth_db(tmp_path)) + try: + salt, pwd_hash = conn.execute( + "SELECT password_salt, password_hash FROM auth_user WHERE username = ?", + (studio_mod.DEFAULT_ADMIN_USERNAME,), + ).fetchone() + finally: + conn.close() + assert studio_mod._pbkdf2_hex(raw.decode("utf-8").strip(), salt.encode("utf-8")) == pwd_hash + + # ── non-interactive --password / UNSLOTH_STUDIO_PASSWORD / stdin ────── @@ -1284,7 +1364,7 @@ def test_studio_default_password_must_differ_fails_closed(monkeypatch, tmp_path) studio_mod = _studio() events = _install_prompt_env(monkeypatch, tmp_path, interactive = True) _seed_auth(studio_mod) - bootstrap_pw = (tmp_path / "auth" / studio_mod.BOOTSTRAP_PASSWORD_FILE).read_text() + bootstrap_pw = (tmp_path / "auth" / studio_mod.BOOTSTRAP_PASSWORD_FILE).read_text().strip() result = _invoke_studio_default(monkeypatch, events, ["--secure", "--password", bootstrap_pw]) @@ -1368,30 +1448,3 @@ def test_studio_default_password_applies_on_headless_wildcard_no_tunnel(monkeypa assert after["must_change_password"] == 0 assert after["password_hash"] != before["password_hash"] assert "--password" not in _exec_argv(events) - - -def test_reset_password_then_password_roundtrip(monkeypatch, tmp_path): - # After reset-password wipes the DB, the next start re-seeds a fresh admin - # that again requires a change, so --password can set a new initial password. - import typer - - studio_mod = _studio() - monkeypatch.setattr(studio_mod, "STUDIO_HOME", tmp_path) - _seed_auth(studio_mod) - conn = studio_mod._connect_auth_db() - studio_mod._cli_update_password(conn, studio_mod.DEFAULT_ADMIN_USERNAME, "first-password-1") - conn.close() - assert _auth_state(studio_mod)["must_change_password"] == 0 - - # reset-password deletes the auth DB + seeded credential files. - try: - studio_mod.reset_password() - except typer.Exit: - pass - assert not (tmp_path / "auth" / "auth.db").exists() - - # A restart re-seeds (ensure_default_admin, must_change=1); --password sets anew. - events = _install_prompt_env(monkeypatch, tmp_path, interactive = True) - _invoke_studio_default(monkeypatch, events, ["--secure", "--password", "second-password-2"]) - assert [kind for kind, _ in events] == ["exec"], events - assert _auth_state(studio_mod)["must_change_password"] == 0 diff --git a/unsloth_cli/tests/test_studio_run_parallel_flag.py b/unsloth_cli/tests/test_studio_run_parallel_flag.py index f1a4e69b81..9a3260d699 100644 --- a/unsloth_cli/tests/test_studio_run_parallel_flag.py +++ b/unsloth_cli/tests/test_studio_run_parallel_flag.py @@ -606,14 +606,14 @@ def test_studio_default_exposes_parallel_option(): assert "--parallel" in decls assert "--n-parallel" in decls assert ( - getattr(opt, "default", None) == 1 - ), "studio_default --parallel must default to 1 (pre-PR); `run` is 4" + getattr(opt, "default", None) == studio_mod._PARALLEL_DEFAULT_PLAIN + ), "studio_default --parallel must use _PARALLEL_DEFAULT_PLAIN" assert getattr(opt, "min", None) == 1 assert getattr(opt, "max", None) == 64 @pytest.mark.parametrize("value", [1, 4, 8, 64]) -def test_in_venv_path_passes_parallel_to_run_server(monkeypatch, value): +def test_in_venv_path_passes_parallel_to_run_server(monkeypatch, value, stub_tool_policy_state): """In-venv path must forward --parallel to run_server(llama_parallel_slots=N), not the old hardcoded 4.""" studio_mod = _load_run_command() @@ -679,7 +679,6 @@ def test_api_only_option_is_registered(): "extra,present", [ (["--api-only"], True), - (["--secure", "--api-only"], True), # secure headless path ([], False), ], ) @@ -691,8 +690,25 @@ def test_reexec_forwards_api_only(monkeypatch, extra, present): assert ("--api-only" in argv) is present, argv +def test_secure_api_only_is_refused_before_any_reexec(monkeypatch, tmp_path): + """`--secure --api-only` used to re-exec; the pre-exposure gate now refuses + it, because api-only has no login page and the bootstrap deadline does not + apply, so the seeded password could never be changed.""" + studio_mod = _load_run_command() + monkeypatch.setattr(studio_mod, "STUDIO_HOME", tmp_path) + + result, captured = _invoke_run(monkeypatch, _BASE + ["--secure", "--api-only"]) + + assert captured == [], captured + assert result.exit_code != 0 + combined = (result.output or "") + (getattr(result, "stderr", "") or "") + assert "default admin password was never changed" in combined.lower() + + @pytest.mark.parametrize("extra,expected", [(["--api-only"], True), ([], False)]) -def test_in_venv_path_passes_api_only_to_run_server(monkeypatch, extra, expected): +def test_in_venv_path_passes_api_only_to_run_server( + monkeypatch, extra, expected, stub_tool_policy_state +): """In-venv path must forward --api-only to run_server(api_only=...).""" studio_mod = _load_run_command() diff --git a/unsloth_cli/tests/test_studio_secure_flag.py b/unsloth_cli/tests/test_studio_secure_flag.py index 2a67aad95a..118f227949 100644 --- a/unsloth_cli/tests/test_studio_secure_flag.py +++ b/unsloth_cli/tests/test_studio_secure_flag.py @@ -244,7 +244,7 @@ class _RunServerCaptured(SystemExit): self.kwargs = dict(kwargs) -def test_run_in_venv_passes_secure_and_forces_host(monkeypatch, tmp_path): +def test_run_in_venv_passes_secure_and_forces_host(monkeypatch, tmp_path, stub_tool_policy_state): import types studio_mod = _studio() @@ -261,6 +261,11 @@ def test_run_in_venv_passes_secure_and_forces_host(monkeypatch, tmp_path): fake_venv = tmp_path / "unsloth_studio" monkeypatch.setattr(sys, "prefix", str(fake_venv)) + # A built dist is not present in a fresh clone, and without it the public + # launch gate exits before run_server is ever reached. + monkeypatch.setattr( + studio_mod, "_find_frontend_dist", lambda: Path("/fake/studio/frontend/dist") + ) from unsloth_cli import _tool_policy as _tp_mod diff --git a/unsloth_cli/tests/test_studio_stop.py b/unsloth_cli/tests/test_studio_stop.py new file mode 100644 index 0000000000..74e34d4fa1 --- /dev/null +++ b/unsloth_cli/tests/test_studio_stop.py @@ -0,0 +1,530 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +"""`unsloth studio stop` must stop every server it started. + +With one PID file the second launch overwrote the first entry, so stop killed +the newer server, claimed success, and left the older one serving. +""" + +from __future__ import annotations + +import sys +from pathlib import Path +from types import SimpleNamespace + +import pytest +from typer.testing import CliRunner + + +_REPO_ROOT = Path(__file__).resolve().parents[2] +if str(_REPO_ROOT) not in sys.path: + sys.path.insert(0, str(_REPO_ROOT)) + + +def _studio(): + from unsloth_cli.commands import studio as _studio_mod + return _studio_mod + + +# Captured before _install stubs it, for the tests that exercise it. +_REAL_IS_STUDIO_SERVER = _studio()._pid_is_studio_server + + +def _install( + monkeypatch, + tmp_path, + *, + alive, + killed = None, +): + """Point the CLI at tmp_path and fake process liveness.""" + studio_mod = _studio() + monkeypatch.setattr(studio_mod, "STUDIO_HOME", tmp_path) + monkeypatch.setattr(studio_mod, "_PID_FILE", tmp_path / "studio.pid") + monkeypatch.setattr(studio_mod.time, "sleep", lambda _s: None) + + live = set(alive) + killed = killed if killed is not None else [] + + monkeypatch.setattr(studio_mod, "_pid_alive", lambda pid: pid in live) + monkeypatch.setattr(studio_mod, "_pid_is_studio_server", lambda pid, created_times = (): True) + + def fake_kill(pid, _sig): + killed.append(pid) + live.discard(pid) + + monkeypatch.setattr(studio_mod.os, "kill", fake_kill) + monkeypatch.setattr(sys, "platform", "linux") + return studio_mod, live, killed + + +def _write_pid(tmp_path, name, pid): + (tmp_path / name).write_text(str(pid), encoding = "utf-8") + + +def _run_stop(studio_mod): + import typer as _typer + + app = _typer.Typer() + app.add_typer(studio_mod.studio_app, name = "studio") + return CliRunner().invoke(app, ["studio", "stop"]) + + +def test_stop_kills_every_recorded_server(monkeypatch, tmp_path): + studio_mod, _live, killed = _install(monkeypatch, tmp_path, alive = {8550, 8600}) + _write_pid(tmp_path, "studio-8901-8550.pid", 8550) + _write_pid(tmp_path, "studio-8902-8600.pid", 8600) + + result = _run_stop(studio_mod) + + assert result.exit_code == 0, result.output + assert sorted(killed) == [8550, 8600] + assert not list(tmp_path.glob("studio-*.pid")) + + +def test_stop_does_not_leave_the_older_instance_running(monkeypatch, tmp_path): + # The reported symptom: stop claimed success while instance A kept serving. + studio_mod, live, _killed = _install(monkeypatch, tmp_path, alive = {8550, 8600}) + _write_pid(tmp_path, "studio-8901-8550.pid", 8550) + _write_pid(tmp_path, "studio-8902-8600.pid", 8600) + + result = _run_stop(studio_mod) + + assert result.exit_code == 0, result.output + assert live == set() + + +def test_stop_signals_each_server_once(monkeypatch, tmp_path): + # A server writes its per-port file AND studio.pid. It stays alive while it + # shuts down gracefully, so a second SIGTERM would hit the SIG_DFL the first + # one installs and hard-kill it mid-cleanup. + studio_mod = _studio() + monkeypatch.setattr(studio_mod, "STUDIO_HOME", tmp_path) + monkeypatch.setattr(studio_mod, "_PID_FILE", tmp_path / "studio.pid") + monkeypatch.setattr(studio_mod.time, "sleep", lambda _s: None) + monkeypatch.setattr(studio_mod, "_pid_alive", lambda pid: True) + monkeypatch.setattr(studio_mod, "_pid_is_studio_server", lambda pid, created_times = (): True) + killed = [] + monkeypatch.setattr(studio_mod.os, "kill", lambda pid, _sig: killed.append(pid)) + monkeypatch.setattr(sys, "platform", "linux") + _write_pid(tmp_path, "studio-8901-8550.pid", 8550) + _write_pid(tmp_path, "studio.pid", 8550) + + result = _run_stop(studio_mod) + + assert result.exit_code == 0, result.output + assert killed == [8550] + assert result.output.lower().count("sent shutdown signal") == 1 + + +def test_stop_removes_every_stale_file_for_one_pid(monkeypatch, tmp_path): + studio_mod, _live, killed = _install(monkeypatch, tmp_path, alive = set()) + _write_pid(tmp_path, "studio-8901-8550.pid", 8550) + _write_pid(tmp_path, "studio.pid", 8550) + + result = _run_stop(studio_mod) + + assert result.exit_code == 0, result.output + assert killed == [] + assert not list(tmp_path.glob("*.pid")) + + +def test_stop_does_not_signal_a_reused_pid(monkeypatch, tmp_path): + # Crash leaves a per-port file behind, the OS hands that PID to something + # else: stop must drop the record, not SIGTERM an unrelated process. + studio_mod, _live, killed = _install(monkeypatch, tmp_path, alive = {8550}) + monkeypatch.setattr(studio_mod, "_pid_is_studio_server", lambda pid, created_times = (): False) + _write_pid(tmp_path, "studio-8901-8550.pid", 8550) + + result = _run_stop(studio_mod) + + assert result.exit_code == 0, result.output + assert killed == [] + assert not (tmp_path / "studio-8901-8550.pid").exists() + + +def test_stop_signals_a_live_server_whose_pid_has_a_stale_record(monkeypatch, tmp_path): + # Crash leaves studio-8888-8550.pid, the OS reuses 8550 for a new server on + # another port. The stale timestamp must not veto the live one. + studio_mod, _live, killed = _install(monkeypatch, tmp_path, alive = {8550}) + monkeypatch.setattr(studio_mod, "_pid_is_studio_server", _REAL_IS_STUDIO_SERVER) + + class _FakeProcess: + def __init__(self, pid): + self.pid = pid + + def create_time(self): + return 999.0 + + monkeypatch.setitem(sys.modules, "psutil", SimpleNamespace(Process = _FakeProcess)) + (tmp_path / "studio-8888-8550.pid").write_text("8550\n111.5", encoding = "utf-8") + (tmp_path / "studio-9000-8550.pid").write_text("8550\n999.0", encoding = "utf-8") + + result = _run_stop(studio_mod) + + assert result.exit_code == 0, result.output + assert killed == [8550] + assert not list(tmp_path.glob("studio-*.pid")) + + +def test_a_bare_run_py_command_line_is_not_rejected(monkeypatch): + # `cd studio/backend && python run.py --port 8901` has no "studio" or "unsloth" + # in argv. Guessing from the command line deleted its record without stopping it. + studio_mod = _studio() + + class _FakeProcess: + def __init__(self, pid): + self.pid = pid + + def cmdline(self): + return ["python", "run.py", "--port", "8901"] + + def create_time(self): + return 111.5 + + monkeypatch.setitem(sys.modules, "psutil", SimpleNamespace(Process = _FakeProcess)) + + assert studio_mod._pid_is_studio_server(8550) is True + + +def test_an_untimed_record_is_trusted(monkeypatch): + # A legacy `python run.py --port 8901` has no telltale argv, and the in-venv + # path runs in-process. Guessing from the command line rejected real servers. + studio_mod = _studio() + + assert studio_mod._pid_is_studio_server(8550) is True + assert studio_mod._pid_is_studio_server(8550, [None]) is True + + +def test_an_unverifiable_record_is_still_stopped(monkeypatch): + # psutil is not a base CLI dependency, so the CLI meets timestamped records it + # cannot check. The old `stop` signalled with no checks at all -- skipping one + # would leave a live server running, the orphan bug this exists to fix. + studio_mod = _studio() + monkeypatch.setitem(sys.modules, "psutil", None) + + assert studio_mod._pid_is_studio_server(8550, [111.5]) is True + assert studio_mod._pid_is_studio_server(8550, [None]) is True + + +def test_stop_signals_a_timestamped_record_without_psutil(monkeypatch, tmp_path): + # Multiple servers on different ports: only the newest is also in studio.pid, + # so the earlier ones are timestamp-only and must still be stopped. + studio_mod, _live, killed = _install(monkeypatch, tmp_path, alive = {8550}) + monkeypatch.setattr(studio_mod, "_pid_is_studio_server", _REAL_IS_STUDIO_SERVER) + monkeypatch.setitem(sys.modules, "psutil", None) + (tmp_path / "studio-8901-8550.pid").write_text("8550\n111.5", encoding = "utf-8") + + result = _run_stop(studio_mod) + + assert result.exit_code == 0, result.output + assert killed == [8550] + assert not (tmp_path / "studio-8901-8550.pid").exists() + + +def test_the_untimed_legacy_record_does_not_cancel_a_timed_one(monkeypatch): + # Every current server writes BOTH a timed per-port record and an untimed + # studio.pid, so letting the untimed half win made this check inert exactly + # where it matters: after a crash and a PID reuse, `stop` SIGTERMed whatever + # unrelated process had inherited the PID. An untimed record carries no + # information, so it must not overrule a start time that says "not ours". + studio_mod = _studio() + + class _FakeProcess: + def __init__(self, pid): + self.pid = pid + + def create_time(self): + return 999.0 + + monkeypatch.setitem(sys.modules, "psutil", SimpleNamespace(Process = _FakeProcess)) + + assert studio_mod._pid_is_studio_server(8550, [111.5, None]) is False + assert studio_mod._pid_is_studio_server(8550, [111.5]) is False + # A matching time still wins over a stale sibling record. + assert studio_mod._pid_is_studio_server(8550, [111.5, 999.0]) is True + assert studio_mod._pid_is_studio_server(8550, [None, None]) is True + + +def test_stop_does_not_signal_a_reused_pid_recorded_in_both_files(monkeypatch, tmp_path): + # End to end for the case above: a crashed server left studio-8901-8550.pid + # and studio.pid, and 8550 now belongs to something else entirely. + studio_mod, _live, killed = _install(monkeypatch, tmp_path, alive = {8550}) + monkeypatch.setattr(studio_mod, "_pid_is_studio_server", _REAL_IS_STUDIO_SERVER) + + class _FakeProcess: + def __init__(self, pid): + self.pid = pid + + def create_time(self): + return 999.0 + + monkeypatch.setitem(sys.modules, "psutil", SimpleNamespace(Process = _FakeProcess)) + (tmp_path / "studio-8901-8550.pid").write_text("8550\n111.5\n127.0.0.1", encoding = "utf-8") + (tmp_path / "studio.pid").write_text("8550", encoding = "utf-8") + + result = _run_stop(studio_mod) + + assert result.exit_code == 0, result.output + assert killed == [] + assert not list(tmp_path.glob("*.pid")) + + +def test_pid_identity_check_trusts_the_record_without_psutil(monkeypatch): + # No psutil: fall back to trusting the record rather than never stopping. + studio_mod = _studio() + monkeypatch.setitem(sys.modules, "psutil", None) + + assert studio_mod._pid_is_studio_server(8550) is True + + +def test_pid_identity_check_uses_the_recorded_start_time(monkeypatch): + studio_mod = _studio() + + class _FakeProcess: + def __init__(self, pid): + self.pid = pid + + def create_time(self): + return 111.5 + + monkeypatch.setitem(sys.modules, "psutil", SimpleNamespace(Process = _FakeProcess)) + + assert studio_mod._pid_is_studio_server(8550, [111.5]) is True + assert studio_mod._pid_is_studio_server(8550, [999.0]) is False + + +def test_stop_drops_a_record_whose_start_time_no_longer_matches(monkeypatch, tmp_path): + # The PID was reused: same number, different process. + studio_mod, _live, killed = _install(monkeypatch, tmp_path, alive = {8550}) + monkeypatch.setattr(studio_mod, "_pid_is_studio_server", _REAL_IS_STUDIO_SERVER) + + class _FakeProcess: + def __init__(self, pid): + self.pid = pid + + def create_time(self): + return 999.0 + + monkeypatch.setitem(sys.modules, "psutil", SimpleNamespace(Process = _FakeProcess)) + (tmp_path / "studio-8901-8550.pid").write_text("8550\n111.5", encoding = "utf-8") + + result = _run_stop(studio_mod) + + assert result.exit_code == 0, result.output + assert killed == [] + assert not (tmp_path / "studio-8901-8550.pid").exists() + # Dropped for the start-time mismatch, not because the record looked corrupt. + assert "invalid pid file" not in result.output.lower() + + +def test_stop_reads_the_legacy_single_pid_file(monkeypatch, tmp_path): + studio_mod, _live, killed = _install(monkeypatch, tmp_path, alive = {4242}) + _write_pid(tmp_path, "studio.pid", 4242) + + result = _run_stop(studio_mod) + + assert result.exit_code == 0, result.output + assert killed == [4242] + assert not (tmp_path / "studio.pid").exists() + + +def test_stop_reports_nothing_running_without_pid_files(monkeypatch, tmp_path): + studio_mod, _live, _killed = _install(monkeypatch, tmp_path, alive = set()) + + result = _run_stop(studio_mod) + + assert result.exit_code == 0, result.output + assert "no running unsloth server" in result.output.lower() + + +def test_stop_cleans_stale_pid_files_without_claiming_a_stop(monkeypatch, tmp_path): + studio_mod, _live, killed = _install(monkeypatch, tmp_path, alive = set()) + _write_pid(tmp_path, "studio-8901-8550.pid", 8550) + + result = _run_stop(studio_mod) + + assert result.exit_code == 0, result.output + assert killed == [] + assert not (tmp_path / "studio-8901-8550.pid").exists() + assert "stopped" not in result.output.lower() + + +def test_stop_does_not_claim_a_stop_while_a_server_is_still_alive(monkeypatch, tmp_path): + # SIGTERM delivered but it never exits: don't claim a stop, keep the file. + studio_mod = _studio() + monkeypatch.setattr(studio_mod, "STUDIO_HOME", tmp_path) + monkeypatch.setattr(studio_mod, "_PID_FILE", tmp_path / "studio.pid") + monkeypatch.setattr(studio_mod.time, "sleep", lambda _s: None) + monkeypatch.setattr(studio_mod, "_pid_alive", lambda pid: True) + monkeypatch.setattr(studio_mod, "_pid_is_studio_server", lambda pid, created_times = (): True) + monkeypatch.setattr(studio_mod.os, "kill", lambda pid, sig: None) + monkeypatch.setattr(sys, "platform", "linux") + _write_pid(tmp_path, "studio-8901-8550.pid", 8550) + + result = _run_stop(studio_mod) + + assert result.exit_code == 0, result.output + assert "shutting down" in result.output.lower() + assert "stopped" not in result.output.lower() + assert (tmp_path / "studio-8901-8550.pid").exists() + + +def test_stop_continues_after_one_server_fails_to_stop(monkeypatch, tmp_path): + studio_mod = _studio() + monkeypatch.setattr(studio_mod, "STUDIO_HOME", tmp_path) + monkeypatch.setattr(studio_mod, "_PID_FILE", tmp_path / "studio.pid") + monkeypatch.setattr(studio_mod.time, "sleep", lambda _s: None) + live = {8550, 8600} + monkeypatch.setattr(studio_mod, "_pid_alive", lambda pid: pid in live) + monkeypatch.setattr(studio_mod, "_pid_is_studio_server", lambda pid, created_times = (): True) + + def fake_kill(pid, _sig): + if pid == 8550: + raise PermissionError("not permitted") + live.discard(pid) + + monkeypatch.setattr(studio_mod.os, "kill", fake_kill) + monkeypatch.setattr(sys, "platform", "linux") + _write_pid(tmp_path, "studio-8901-8550.pid", 8550) + _write_pid(tmp_path, "studio-8902-8600.pid", 8600) + + result = _run_stop(studio_mod) + + combined = (result.output or "") + (getattr(result, "stderr", "") or "") + assert result.exit_code == 1, combined + assert 8600 not in live + assert "8550" in combined + + +def test_stop_never_signals_pid_zero_or_init(monkeypatch, tmp_path): + # os.kill(0, SIGTERM) hits our whole process group -- the shell and its jobs. + studio_mod, _live, killed = _install(monkeypatch, tmp_path, alive = {0, 1}) + _write_pid(tmp_path, "studio-8901-0.pid", 0) + _write_pid(tmp_path, "studio-8902-1.pid", 1) + + result = _run_stop(studio_mod) + + assert result.exit_code == 0, result.output + assert killed == [] + assert not list(tmp_path.glob("*.pid")) + + +def test_signal_stop_refuses_pid_zero_or_init(monkeypatch, tmp_path): + studio_mod, _live, killed = _install(monkeypatch, tmp_path, alive = {0, 1}) + + assert studio_mod._signal_stop(0) is not None + assert studio_mod._signal_stop(1) is not None + assert killed == [] + + +def test_stop_discards_a_corrupt_pid_file(monkeypatch, tmp_path): + studio_mod, _live, _killed = _install(monkeypatch, tmp_path, alive = set()) + (tmp_path / "studio-8901-8550.pid").write_text("not-a-pid", encoding = "utf-8") + + result = _run_stop(studio_mod) + + assert result.exit_code == 0, result.output + assert not (tmp_path / "studio-8901-8550.pid").exists() + + +def test_stop_keeps_a_record_it_cannot_read(monkeypatch, tmp_path): + # A root-owned record, or one caught mid-write, still belongs to a live + # server. Deleting it is `stop` manufacturing the orphan it exists to fix. + studio_mod, _live, killed = _install(monkeypatch, tmp_path, alive = {8550}) + path = tmp_path / "studio-8901-8550.pid" + path.write_text("8550", encoding = "utf-8") + real_read_text = Path.read_text + + def deny(self, *args, **kwargs): + if self == path: + raise PermissionError(13, "Permission denied") + return real_read_text(self, *args, **kwargs) + + monkeypatch.setattr(Path, "read_text", deny) + + result = _run_stop(studio_mod) + + assert path.exists(), "an unreadable record must not be deleted" + assert "cannot read" in (result.output + (result.stderr or "")).lower() + + +def test_stop_does_not_claim_success_when_the_only_record_is_unreadable(monkeypatch, tmp_path): + # A server started under sudo leaves a record we cannot read. Printing "no + # running server" and exiting 0 tells the user the opposite of the truth. + studio_mod, _live, killed = _install(monkeypatch, tmp_path, alive = {8550}) + path = tmp_path / "studio-8901-8550.pid" + path.write_text("8550", encoding = "utf-8") + real_read_text = Path.read_text + + def deny(self, *args, **kwargs): + if self == path: + raise PermissionError(13, "Permission denied") + return real_read_text(self, *args, **kwargs) + + monkeypatch.setattr(Path, "read_text", deny) + + result = _run_stop(studio_mod) + + assert result.exit_code == 1, "an unreachable server is not a successful stop" + output = result.output + (result.stderr or "") + assert "no running unsloth server" not in output.lower() + assert killed == [] + + +def test_stop_reports_failure_when_one_record_is_unreadable_but_another_stops( + monkeypatch, tmp_path +): + # Stopping the servers we can see is still a partial result, and exiting 0 + # would hide the one we could not. + studio_mod, _live, killed = _install(monkeypatch, tmp_path, alive = {8550, 8600}) + _write_pid(tmp_path, "studio-8901-8550.pid", 8550) + hidden = tmp_path / "studio-8902-8600.pid" + hidden.write_text("8600", encoding = "utf-8") + real_read_text = Path.read_text + + def deny(self, *args, **kwargs): + if self == hidden: + raise PermissionError(13, "Permission denied") + return real_read_text(self, *args, **kwargs) + + monkeypatch.setattr(Path, "read_text", deny) + + result = _run_stop(studio_mod) + + assert killed == [8550], "the readable server must still be stopped" + assert result.exit_code == 1 + assert hidden.exists() + + +def test_stop_reaches_every_server_when_one_record_cannot_be_removed(monkeypatch, tmp_path): + # One undeletable stale record must not end the loop before the live servers. + studio_mod, _live, killed = _install(monkeypatch, tmp_path, alive = {8600}) + _write_pid(tmp_path, "studio-8901-8550.pid", 8550) # dead -> stop prunes it + _write_pid(tmp_path, "studio-8902-8600.pid", 8600) # live -> stop signals it + real_unlink = Path.unlink + + def deny(self, *args, **kwargs): + if self.name == "studio-8901-8550.pid": + raise PermissionError(13, "Permission denied") + return real_unlink(self, *args, **kwargs) + + monkeypatch.setattr(Path, "unlink", deny) + + result = _run_stop(studio_mod) + + assert killed == [8600], "the live server must still be signalled" + assert result.exit_code == 0, result.output + + +def test_a_record_whose_pid_is_not_ascii_digits_is_discarded(monkeypatch, tmp_path): + # A superscript two passes isdigit() but int() rejects it, so that gate alone + # let a ValueError escape _read_pid_record and abort the whole command. + studio_mod, _live, _killed = _install(monkeypatch, tmp_path, alive = set()) + (tmp_path / "studio-8901-1.pid").write_text("²", encoding = "utf-8") + + result = _run_stop(studio_mod) + + assert result.exit_code == 0, result.output + assert not (tmp_path / "studio-8901-1.pid").exists()