diff --git a/.github/workflows/studio-inference-smoke.yml b/.github/workflows/studio-inference-smoke.yml index 02f55ad1b0..3a8d68f88f 100644 --- a/.github/workflows/studio-inference-smoke.yml +++ b/.github/workflows/studio-inference-smoke.yml @@ -468,7 +468,10 @@ jobs: "stream": False, "temperature": 0.0, "seed": SEED, - "max_tokens": 120, + # tool_choice='required' constrains the grammar so the + # model emits the JSON tool_call envelope directly; 96 is + # plenty for `{"city":"Paris"}` plus the wrapping fields. + "max_tokens": 96, }) assert status == 200, f"tool call status {status}: {data}" choice = data["choices"][0] @@ -483,6 +486,8 @@ jobs: # 123 * 456 = 56088. The agentic loop streams SSE; we # accumulate the assistant text and look for the answer. We # accept "56088" or "56,088" since the model may format it. + # 320 tokens covers the tool_call + tool result + brief + # natural-language answer; 600 was 2x what the model needs. content = post_sse("/v1/chat/completions", { "messages": [{"role": "user", "content": "What is 123 * 456? Use the python tool to compute it and tell me the number."}], "enable_tools": True, @@ -490,29 +495,20 @@ jobs: "session_id": "ci-tool-calling-py", "temperature": 0.0, "seed": SEED, - "max_tokens": 600, + "max_tokens": 320, }) assert "56088" in content or "56,088" in content, ( f"expected 56088 in python-tool answer, got: {content!r}" ) print(f"[tools] PASS python tool ({len(content)} chars)") - # ── 3. Server-side bash (terminal) tool ────────────────────── - content = post_sse("/v1/chat/completions", { - "messages": [{"role": "user", "content": "Use the terminal tool to run `echo hello-bash-tool` and tell me the exact output."}], - "enable_tools": True, - "enabled_tools": ["terminal"], - "session_id": "ci-tool-calling-bash", - "temperature": 0.0, - "seed": SEED, - "max_tokens": 600, - }) - assert "hello-bash-tool" in content, ( - f"expected 'hello-bash-tool' in terminal-tool answer, got: {content!r}" - ) - print(f"[tools] PASS bash/terminal tool ({len(content)} chars)") + # NOTE: the dedicated "Server-side bash (terminal) tool" axis + # was dropped in favour of the python axis above. Both share + # the same server-side agentic-loop wiring (only the registry + # entry differs); the python axis is the canonical proof. + # Saves one SSE round (~30 s on macos, ~12 s on linux/windows). - # ── 4. Server-side web_search tool ─────────────────────────── + # ── 3. Server-side web_search tool ─────────────────────────── # DuckDuckGo is flaky from CI runners and small Qwen3.5-2B # may not actually search. Only assert that the SSE stream # opens and yields any data; HTTP / parser failures already @@ -525,13 +521,13 @@ jobs: "session_id": "ci-tool-calling-web", "temperature": 0.0, "seed": SEED, - "max_tokens": 400, + "max_tokens": 192, }) print(f"[tools] PASS web_search stream ({len(content)} chars)") except Exception as exc: print(f"[tools] WARN web_search probe failed (non-blocking): {exc}") - # ── 5. Thinking on / off ───────────────────────────────────── + # ── 4. Thinking on / off ───────────────────────────────────── # Studio strips think blocks from message.content for tools-mode # responses, so we toggle plain chat (no enable_tools) and look # at the surfaced reasoning_content / message.thinking field. @@ -542,7 +538,10 @@ jobs: "enable_thinking": enable, "temperature": 0.0, "seed": SEED, - "max_tokens": 300, + # 17 is small; 160 tokens is plenty of room for either + # "Yes, 17 is prime" + brief reasoning or a short + # ...+answer. 300 was overkill. + "max_tokens": 160, }) assert status == 200 msg = data["choices"][0]["message"] diff --git a/.github/workflows/studio-mac-inference-smoke.yml b/.github/workflows/studio-mac-inference-smoke.yml index 4104398013..d613507c89 100644 --- a/.github/workflows/studio-mac-inference-smoke.yml +++ b/.github/workflows/studio-mac-inference-smoke.yml @@ -485,10 +485,11 @@ jobs: "stream": False, "temperature": TEMP, "seed": SEED, - # Was 600; trimmed to keep total runtime under timeout. # tool_choice='required' constrains the grammar so the - # model emits a tool_call quickly when it works at all. - "max_tokens": 300, + # model emits a tool_call quickly when it works at all; + # 128 tokens is enough for `{"city":"Paris"}` plus the + # JSON envelope. + "max_tokens": 128, }, timeout = 180) assert status == 200, f"tool call status {status}: {data}" choice = data["choices"][0] @@ -531,7 +532,7 @@ jobs: "session_id": "ci-tool-calling-py", "temperature": TEMP, "seed": SEED, - "max_tokens": 256, + "max_tokens": 128, }, timeout = 180) if "56088" in content or "56,088" in content: print(f"[tools] PASS python tool ({len(content)} chars, found 56088)") @@ -543,25 +544,15 @@ jobs: f"model didn't return 56088 -- Mac quant drift" ) - # ── 3. Server-side bash (terminal) tool ────────────────────── - content = post_sse("/v1/chat/completions", { - "messages": [{"role": "user", "content": "Use the terminal tool to run `echo hello-bash-tool` and tell me the exact output."}], - "enable_tools": True, - "enabled_tools": ["terminal"], - "session_id": "ci-tool-calling-bash", - "temperature": TEMP, - "seed": SEED, - "max_tokens": 256, - }, timeout = 180) - if "hello-bash-tool" in content: - print(f"[tools] PASS bash/terminal tool ({len(content)} chars)") - else: - print( - f"[tools] WARN terminal tool: SSE OK ({len(content)} chars) but " - f"model didn't echo 'hello-bash-tool' -- Mac quant drift" - ) + # NOTE: the dedicated "Server-side bash (terminal) tool" axis + # was dropped in favour of the python axis above. Both share + # the SAME server-side agentic loop wiring (only the registry + # entry differs); the python axis is the canonical proof. On + # macos-14 the duplicated SSE round was the dominant cost in + # this step, so collapsing the two saves ~30-60 s wallclock + # without losing distinct coverage. - # ── 4. Server-side web_search tool ─────────────────────────── + # ── 3. Server-side web_search tool ─────────────────────────── # DuckDuckGo is flaky from CI runners and small Qwen3.5-2B # may not actually search. Only assert that the SSE stream # opens and yields any data; HTTP / parser failures already @@ -574,13 +565,13 @@ jobs: "session_id": "ci-tool-calling-web", "temperature": TEMP, "seed": SEED, - "max_tokens": 200, + "max_tokens": 96, }, timeout = 180) print(f"[tools] PASS web_search stream ({len(content)} chars)") except Exception as exc: print(f"[tools] WARN web_search probe failed (non-blocking): {exc}") - # ── 5. Thinking on / off ───────────────────────────────────── + # ── 4. Thinking on / off ───────────────────────────────────── # Studio strips think blocks from message.content for tools-mode # responses, so we toggle plain chat (no enable_tools) and look # at the surfaced reasoning_content / message.thinking field. @@ -591,9 +582,11 @@ jobs: "enable_thinking": enable, "temperature": TEMP, "seed": SEED, - # Was 300; trimmed to keep total job runtime within - # the 25-minute timeout on macos-14 free runners. - "max_tokens": 150, + # 80 tokens lands within the 25-minute job timeout + # on the macos-14 free runner. 17 is small; this is + # plenty of room for either "Yes" + brief reasoning + # or a degenerate empty completion. + "max_tokens": 80, }, timeout = 180) assert status == 200 msg = data["choices"][0]["message"] diff --git a/.github/workflows/studio-windows-inference-smoke.yml b/.github/workflows/studio-windows-inference-smoke.yml index 32a86d239d..e81cf2944c 100644 --- a/.github/workflows/studio-windows-inference-smoke.yml +++ b/.github/workflows/studio-windows-inference-smoke.yml @@ -88,6 +88,32 @@ jobs: HF_HUB_ENABLE_HF_TRANSFER=1 \ hf download "$GGUF_REPO" "$GGUF_FILE" + - name: Pre-install Windows tweaks (npm 11 + Defender exclusions) + shell: pwsh + # See studio-windows-update-smoke.yml for the full rationale. + # tl;dr: setup.ps1 needs npm >=11 to skip a 35 s winget Node + # reinstall, and Defender's real-time scan dominates the + # frontend / uv-pip-extract steps. + run: | + $ProgressPreference = 'SilentlyContinue' + Write-Host "npm version before upgrade: $(npm -v)" + npm install -g 'npm@^11' 2>&1 | Out-Host + Write-Host "npm version after upgrade: $(npm -v)" + foreach ($p in @( + "$env:USERPROFILE\.unsloth", + "$env:USERPROFILE\AppData\Local\uv", + "$env:GITHUB_WORKSPACE\studio\frontend\node_modules", + "$env:GITHUB_WORKSPACE\studio\frontend\dist" + )) { + try { + if (-not (Test-Path $p)) { New-Item -ItemType Directory -Force -Path $p | Out-Null } + Add-MpPreference -ExclusionPath $p -ErrorAction Stop + Write-Host "Defender exclusion added: $p" + } catch { + Write-Host "Defender exclusion skipped ($($_.Exception.Message)): $p" + } + } + - name: Install Studio (--local, --no-torch) shell: pwsh env: @@ -353,6 +379,32 @@ jobs: HF_HUB_ENABLE_HF_TRANSFER=1 \ hf download "$GGUF_REPO" "$GGUF_FILE" + - name: Pre-install Windows tweaks (npm 11 + Defender exclusions) + shell: pwsh + # See studio-windows-update-smoke.yml for the full rationale. + # tl;dr: setup.ps1 needs npm >=11 to skip a 35 s winget Node + # reinstall, and Defender's real-time scan dominates the + # frontend / uv-pip-extract steps. + run: | + $ProgressPreference = 'SilentlyContinue' + Write-Host "npm version before upgrade: $(npm -v)" + npm install -g 'npm@^11' 2>&1 | Out-Host + Write-Host "npm version after upgrade: $(npm -v)" + foreach ($p in @( + "$env:USERPROFILE\.unsloth", + "$env:USERPROFILE\AppData\Local\uv", + "$env:GITHUB_WORKSPACE\studio\frontend\node_modules", + "$env:GITHUB_WORKSPACE\studio\frontend\dist" + )) { + try { + if (-not (Test-Path $p)) { New-Item -ItemType Directory -Force -Path $p | Out-Null } + Add-MpPreference -ExclusionPath $p -ErrorAction Stop + Write-Host "Defender exclusion added: $p" + } catch { + Write-Host "Defender exclusion skipped ($($_.Exception.Message)): $p" + } + } + - name: Install Studio (--local, --no-torch) shell: pwsh env: @@ -534,7 +586,10 @@ jobs: "stream": False, "temperature": TEMP, "seed": SEED, - "max_tokens": 600, + # tool_choice='required' constrains the grammar so the + # model emits the JSON tool_call envelope directly; 128 + # is plenty for `{"city":"Paris"}` plus the wrapping. + "max_tokens": 128, }) assert status == 200, f"tool call status {status}: {data}" choice = data["choices"][0] @@ -554,6 +609,8 @@ jobs: ) # ── 2. Server-side python tool ─────────────────────────────── + # 320 tokens covers tool_call + result + brief answer; 600 + # was 2x what the model needs. content = post_sse("/v1/chat/completions", { "messages": [{"role": "user", "content": "What is 123 * 456? Use the python tool to compute it and tell me the number."}], "enable_tools": True, @@ -561,7 +618,7 @@ jobs: "session_id": "ci-tool-calling-py", "temperature": TEMP, "seed": SEED, - "max_tokens": 600, + "max_tokens": 320, }) if "56088" in content or "56,088" in content: print(f"[tools] PASS python tool ({len(content)} chars, found 56088)") @@ -572,30 +629,13 @@ jobs: f"model didn't return 56088 -- model output drift" ) - # ── 3. Server-side bash (terminal) tool ────────────────────── - # On Windows the terminal tool resolves to the system shell - # (cmd.exe wrapper) and `echo hello-bash-tool` works the same - # way it does on POSIX. The model still has to choose to - # invoke the tool; assert non-empty SSE if it doesn't. - content = post_sse("/v1/chat/completions", { - "messages": [{"role": "user", "content": "Use the terminal tool to run `echo hello-bash-tool` and tell me the exact output."}], - "enable_tools": True, - "enabled_tools": ["terminal"], - "session_id": "ci-tool-calling-bash", - "temperature": TEMP, - "seed": SEED, - "max_tokens": 600, - }) - if "hello-bash-tool" in content: - print(f"[tools] PASS terminal tool ({len(content)} chars)") - else: - assert content, "terminal tool: SSE stream empty" - print( - f"[tools] WARN terminal tool: SSE OK ({len(content)} chars) but " - f"model didn't echo 'hello-bash-tool' -- model output drift" - ) + # NOTE: the dedicated "Server-side bash (terminal) tool" axis + # was dropped in favour of the python axis above. Both share + # the same server-side agentic-loop wiring (only the registry + # entry differs); the python axis is the canonical proof. + # Saves one SSE round (~12 s on windows-latest). - # ── 4. Server-side web_search tool ─────────────────────────── + # ── 3. Server-side web_search tool ─────────────────────────── # DuckDuckGo can be flaky from CI runners; only assert that # the SSE stream opens and yields any data. try: @@ -606,13 +646,13 @@ jobs: "session_id": "ci-tool-calling-web", "temperature": TEMP, "seed": SEED, - "max_tokens": 400, + "max_tokens": 192, }) print(f"[tools] PASS web_search stream ({len(content)} chars)") except Exception as exc: print(f"[tools] WARN web_search probe failed (non-blocking): {exc}") - # ── 5. Thinking on / off ───────────────────────────────────── + # ── 4. Thinking on / off ───────────────────────────────────── def thinking_call(enable): status, data = post("/v1/chat/completions", { "messages": [{"role": "user", "content": "Briefly: is 17 prime?"}], @@ -620,7 +660,10 @@ jobs: "enable_thinking": enable, "temperature": TEMP, "seed": SEED, - "max_tokens": 300, + # 17 is small; 160 tokens is plenty for either "Yes" + # + brief reasoning or a short ... + + # answer. 300 was overkill. + "max_tokens": 160, }) assert status == 200 msg = data["choices"][0]["message"] @@ -709,6 +752,32 @@ jobs: HF_HUB_ENABLE_HF_TRANSFER=1 \ hf download "$GGUF_REPO" "$MMPROJ_FILE" + - name: Pre-install Windows tweaks (npm 11 + Defender exclusions) + shell: pwsh + # See studio-windows-update-smoke.yml for the full rationale. + # tl;dr: setup.ps1 needs npm >=11 to skip a 35 s winget Node + # reinstall, and Defender's real-time scan dominates the + # frontend / uv-pip-extract steps. + run: | + $ProgressPreference = 'SilentlyContinue' + Write-Host "npm version before upgrade: $(npm -v)" + npm install -g 'npm@^11' 2>&1 | Out-Host + Write-Host "npm version after upgrade: $(npm -v)" + foreach ($p in @( + "$env:USERPROFILE\.unsloth", + "$env:USERPROFILE\AppData\Local\uv", + "$env:GITHUB_WORKSPACE\studio\frontend\node_modules", + "$env:GITHUB_WORKSPACE\studio\frontend\dist" + )) { + try { + if (-not (Test-Path $p)) { New-Item -ItemType Directory -Force -Path $p | Out-Null } + Add-MpPreference -ExclusionPath $p -ErrorAction Stop + Write-Host "Defender exclusion added: $p" + } catch { + Write-Host "Defender exclusion skipped ($($_.Exception.Message)): $p" + } + } + - name: Install Studio (--local, --no-torch) shell: pwsh env: