Commit graph

5,428 commits

Author SHA1 Message Date
Daniel Han
dee1b68b6d Studio: round 7b -- tighten device-login log filter + harden timeout kill
Two more P1 follow-ups from the round 7 reviewer pass:

1. Device-login log filter no longer leaks sensitive lines.

   The old `_safe_to_forward` used unanchored substring matches like
   `"logged in"`, so a line such as

     Not logged in: refresh_token=rt_LEAK auth.json=/home/u/.codex/auth.json

   slipped through the safe-vocabulary filter and was streamed to the
   browser. A malicious codex shim earlier on PATH can print that
   line trivially, defeating the "opaque output stays in backend
   logs" safety guarantee the route claimed.

   Round 7b fix: anchored regex set (must start with one of the
   known upstream phrases) plus an explicit blocklist for
   refresh_token / access_token / api_key / secret / auth.json / the
   codex config dir / "not logged in" / "not authenticated". A line
   that matches the blocklist is dropped regardless of which safe
   pattern would otherwise have accepted it. New tests reconstruct
   the regex set inline and assert both the leak cases drop and the
   clean upstream phrases pass.

2. `_run_cli` timeout cleanup no longer 500s on a kill race.

   `_run_cli` would call `proc.kill()` after `os.killpg(pid, SIGTERM)`
   reaped the process group. If the SIGTERM landed first, the
   subsequent `proc.kill()` raised `ProcessLookupError` and bubbled
   out of `_run_cli`, turning `/api/codex/status` into a 500 during
   a timeout race. The device-login cleanup at codex_provider.py
   already wraps the same destructive call in a try / except. Mirror
   that exception guard here so the two timeout paths behave the
   same.
2026-05-25 14:45:26 +00:00
pre-commit-ci[bot]
8cef479423 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-05-25 14:43:25 +00:00
Daniel Han
f05517ac79 Studio: round 7 Codex hardening (cross-wrapper env scrub + device URL allowlist)
Two P1 fixes from the round 7 reviewer pass:

1. _ScrubbedEnvAsyncCodex no longer leaks secrets across overlapping
   sessions.

   The fallback env-scrub wrapper (used when the installed SDK build
   does not accept AppServerConfig(env=...)) refcounts deleted env
   vars under a shared lock so concurrent fan-out workers do not
   restore Studio's secrets while a peer is still inside SDK startup.
   The round 6 implementation enumerated keys to scrub via
   _codex_sdk_env_override() which only returns keys currently in
   os.environ. If wrapper A had already deleted HF_TOKEN before
   wrapper B entered, B's overrides dict no longer contained HF_TOKEN,
   B never bumped the refcount for it, and A's exit restored
   HF_TOKEN into the process env while B was still running -- so
   B's spawned codex app-server inherited the secret.

   Round 7 fix: under the lock, the union of (a) the current
   overrides dict and (b) every key still refcounted by an earlier
   wrapper is the set of keys this session must scrub. Originals are
   tracked module-level rather than per-instance so the last wrapper
   to release a key always restores the right pre-scrub value
   regardless of who first saw it. New regression test reproduces
   the leak against the round 6 code (asserts refcount == 2 after
   B enters; old code records 1) and locks the fix in.

2. Device-auth verification URL is now host-allowlisted.

   The codex login --device-auth output parser pulled any https URL
   matching /device, /activate, or /verify out of the CLI's stdout
   and emitted it as a device_url event. The frontend rendered that
   URL as an "Open verification page" CTA the user can click. A
   compromised codex shim earlier on PATH could print
   https://evil.example/activate?code=ABCD and Studio would surface
   the phishing link verbatim, even though every other login output
   line goes through a strict safe-vocabulary filter.

   Round 7 fix: device_url events only fire for URLs whose host is
   on a small allowlist (auth.openai.com / chatgpt.com over https).
   Anything else is logged at warn and dropped. Tests cover the
   known-good upstream URLs, several attacker patterns (lookalike
   subdomains, http downgrade, javascript:), and garbage input.
2026-05-25 14:43:10 +00:00
Daniel Han
26799d9a18 Studio: pre-check Codex default models when creating the connection
First-run UX bug surfaced by an end-to-end probe: after the user adds
the "OpenAI Codex (local CLI)" connection from Settings -> Connections
-> Add provider, every model in the form starts UNCHECKED. The user
saves, returns to the chat composer, opens the model picker -- and
the "Connected" tab is missing because no Codex models are enabled.
The user has to re-open the connection, tick at least one model, save
again, then return to chat. Two round-trips to make a feature work
that the rest of the UI already advertises as installed.

Anthropic / OpenAI / OpenRouter all keep their explicit-opt-in
defaults because the model choice has billing and capability impact.
Codex is the local CLI on the same machine -- the SDK accepts any
model id, the "default_models" list is the SDK's curated shortlist,
and gating chat behind a manual click adds friction without buying
anything. Pre-checking all default models for Codex (and only Codex)
makes the path "click Add -> click Save -> chat works" survive a
single click sequence, matching the empirical setup we walked
through in the e2e probe.
2026-05-25 14:23:05 +00:00
Daniel Han
b7862388fa Studio: render Codex parallel-calls synthesis AFTER the per-tab blocks
End-to-end probe of a 2-way parallel-calls Codex turn revealed the
visible assistant message looked like:

  <synthesis text>

  [Codex tab 1/2]
  <tab 1 text>

  [Codex tab 2/2]
  <tab 2 text>

  --- Synthesis ---

The synthesis came first (no label), then the tabs, then a trailing
"--- Synthesis ---" line with nothing below it. The header comment in
chat-adapter.ts said the intent was "[tabs] ... [synthesis]" but the
implementation prepended cumulativeText (which carries the synthesis
deltas the backend emits after codex_gather) before the tabs.

Fix: split renderCodexBuffer into renderCodexTabsBlock (pure per-tab
rendering, no trailing divider) and reorder renderFullContent so when
Codex fan-out is active the output is:

  [Codex tab 1/N]
  <tab 1>

  [Codex tab N/N]
  <tab N>

  --- Synthesis ---

  <synthesis text from cumulativeText>

The non-Codex case still returns cumulativeText unchanged.
2026-05-25 14:06:19 +00:00
Daniel Han
c755d00c1f Studio: cross-platform CI matrix for Codex provider tests
Adds a small (ubuntu-latest + macos-14 + windows-latest) x (3.11, 3.13)
matrix that runs tests/test_codex_provider.py on every push touching
the Codex code or this workflow. The existing studio-backend-ci.yml
already runs the full backend test suite on ubuntu across Py 3.10-3.13
but never on macOS / Windows, so cross-platform regressions in the
codex_bin / sys.modules / importlib gates would not be caught before
shipping. macOS coverage matters because Studio's MLX path attracts
Apple Silicon users, Windows because Studio ships a Tauri desktop
build there. Concurrency cancel-in-progress so each new push
supersedes the previous run, paths filter so unrelated changes do not
re-trigger.
2026-05-25 14:01:42 +00:00
Daniel Han
2eaf1bbd31 Studio: pass codex_bin to AppServerConfig so PATH-only codex installs work
Reproduces with pip install openai-codex --no-deps (lightweight install
that skips the pinned openai-codex-cli-bin runtime) or any host where
the codex CLI is installed via npm i -g @openai/codex / Homebrew /
manual download. Studio constructs AsyncCodex(config=AppServerConfig(
env=...)) without codex_bin, so the SDK runs _installed_codex_path
which 'from codex_cli_bin import bundled_codex_path' and raises
FileNotFoundError: Unable to locate the pinned Codex runtime. Install
the published SDK build with its openai-codex-cli-bin dependency, or
set AppServerConfig.codex_bin explicitly. -- even though a perfectly
good codex is on PATH and was the binary the availability probe
already verified.

Fix: resolve shutil.which("codex") and pass it as
AppServerConfig(codex_bin=...). The PR's availability probe already
returns that exact path in /api/codex/status.cli_path, so we are
giving the SDK back the binary the user can see in the Connections
form. Falls back to AppServerConfig(env=...) (no codex_bin) when the
SDK build does not accept the kwarg yet, and falls back to PATH lookup
returning None on hosts without codex on PATH (in which case the
availability probe would have reported installed=false and Studio
never gets here).

Test fixture: also inject the fake module under openai_codex (the
canonical name the production importer prefers) so the test does not
silently exercise the real SDK on developer venvs that have
pip install openai-codex already done.

End-to-end verified live: pip install -e openai/codex sdk/python plus
Studio with codex CLI on PATH yielded ROUND_TRIP_OK streaming for
gpt-5.4-mini through the OpenAI-compat completions route.
2026-05-25 13:47:16 +00:00
Daniel Han
2874abbfec Studio: fail closed when Codex SDK cannot enforce safety pins
Round 6 reviewer noted that the warn-and-proceed path in
`_start_thread_with_system` is "failing open" on a server-side
chat surface: an SDK rev that does not expose ApprovalMode or
SandboxMode would log a warning then call `thread_start(model=...)`
with NO safety kwargs, letting the model run under the SDK's
`auto_review` default. For a route that takes a user-controlled
prompt and can spawn shell commands or file writes, that is the
wrong tradeoff.

Now fails closed: when `_safe_thread_safety_kwargs()` returns the
empty dict the helper raises `CodexUnavailableError`, which the
route layer translates to a 503 with a clear error message telling
the operator to upgrade `openai_codex` (or set the explicit
override env var). The error message names the override so users
who hit this on a pre-release alpha can opt in with eyes open
rather than discovering the unsafe default after the fact.

`UNSLOTH_CODEX_ALLOW_UNSAFE_DEFAULTS=1` is the deliberately-verbose
escape hatch. Variable name long and explicit so it does not creep
into production environments by accident, kept on the codex
subprocess safe-list so the round 6 SDK env-scrub wrapper does not
delete it before the gate sees it.

Tests: 50 cases total (was 49). The previous old-SDK test was
renamed and replaced by two new ones:
- `test_thread_start_fails_closed_when_safety_unavailable` asserts
  the raise fires and `thread_start` is never called.
- `test_thread_start_allows_unsafe_defaults_with_explicit_opt_in`
  asserts the override env var lets the request through and
  `thread_start` runs without the safety kwargs (with a logged
  warning).
The `_install_fake_codex_sdk` helper now injects fake ApprovalMode
and SandboxMode by default so the general translation tests do
not need to opt into the override; the two round-6b tests above
pass `with_safety_enums=False` to exercise the fail-closed branch.
2026-05-24 17:45:20 +00:00
Daniel Han
8ee60019a4 Studio: round 6 Codex hardening (5 follow-ups)
Reviewer round 6 surfaced five real follow-ups on top of rounds 5
through 5g. Each one is a fix for an asymmetric guard or a wrong-
shape lookup in the new Codex provider code:

1. Re-gate `installed=True` on having BOTH the SDK AND a `codex`
   binary on PATH. Round 5 widened the gate to SDK-only, but the
   login route still shells out to the binary, so an SDK-only host
   would surface a Codex row whose Sign-in button immediately
   failed with "codex CLI not found on PATH". The canonical
   `openai-codex` package depends on `openai-codex-cli-bin` which
   places the shim on PATH for free, so the common install still
   lights up; the gate just refuses to advertise a provider Studio
   cannot actually drive end-to-end.

2. `_safe_thread_safety_kwargs` now also probes `.api` and
   `.generated.v2_all` for `SandboxMode`. Upstream `openai_codex`
   exports `ApprovalMode` at the top level but `SandboxMode` lives
   under `openai_codex.generated.v2_all`. The previous lookup
   returned `{}` on the canonical SDK install, so every thread_start
   ran with the unsafe `auto_review` default. Submodule probe
   resolves the canonical layout and keeps backwards-compat with
   builds that DID re-export at the top level.

3. `_coerce_text` now applies the answer-event-type filter on the
   object path too. The upstream SDK emits typed payload classes
   like `CommandExecutionOutputDelta`, `FileChangeDelta`,
   `ToolCallDelta`, `PatchApplyDelta`, etc., all of which carry a
   `.delta` string of local stdout / file paths / tool args. The
   dict path already filtered these out; the object path used to
   return `.delta` unconditionally, so a real SDK install could
   leak tool output into the visible chat reply.

4. `_ScrubbedEnvAsyncCodex` is now process-wide concurrency-safe
   AND fails-closed if the SDK constructor raises:
   - Refcount each scrubbed key under an `asyncio.Lock` so a fan-out
     wrapper that exits early cannot restore a secret while another
     wrapper is still inside SDK startup (round 6 reproduced this:
     wrapper A exited, wrapper B's SDK saw the restored HF_TOKEN).
   - Move `_async_codex_cls()` and its `__aenter__` INSIDE a
     try/except in `__aenter__`; on failure, run the release path
     so the scrubbed env vars are restored even though `__aexit__`
     never fires for the failed construction.

5. `_run_cli` now detaches into its own process group via
   `start_new_session=True` (Unix) / `CREATE_NEW_PROCESS_GROUP`
   (Windows) and kills the whole group on timeout, matching the
   protected path in `stream_codex_device_login`. A shimmed
   `codex login status` that forks a helper and blocks no longer
   leaves the child running after we killed the parent.

Frontend follow-up: chat-adapter now routes every rendered yield
through a `renderFullContent()` helper so the Codex per-tab text
accumulated in earlier `_toolEvent` frames is preserved when the
synthesis content delta arrives. Previously the next regular
content yield rebuilt `parts` from `cumulativeText` alone and the
tab section vanished from the final assistant message.

Tests: 49 cases total (was 47). New regressions:
- `installed_requires_both_cli_and_sdk` (round 6 revert).
- `safety_kwargs_finds_sandbox_mode_in_submodule` (canonical SDK
  layout where `SandboxMode` is in `.generated.v2_all`).
- `scrubbed_env_construction_failure_restores_env` (no permanent
  env leak when the SDK constructor raises).
- Expanded `coerce_text_drops_non_answer_event_types` to also
  exercise the object-shape code path with `CommandExecutionOutputDelta`,
  `FileChangeDelta`, `ToolCallDelta`, `PatchApplyDelta`,
  `PlanUpdateDelta`, `AgentReasoningDelta`, plus the
  positive `AgentMessageDelta` allow-through.
2026-05-24 17:30:01 +00:00
Daniel Han
aa258b983d Studio: account for every Codex turn in fan-out usage chunk
The fan-out path used to report usage as if a single Codex call had
run: `prompt_tokens = max(1, len(prompt)//4)` and
`completion_tokens = len(synthesis)//4`. In reality it had spawned
N parallel worker turns (each carrying the same prompt) plus a
synthesis turn that re-sent the prompt and every tab's output. For
`parallel_calls=20` that meant the cost / context widget under-
reported the request by roughly 20x.

Now sums:

- `prompt_tokens ≈ (N * prompt + synthesis_prompt) / 4` where
  `synthesis_prompt = sum(tab_outputs) + prompt`.
- `completion_tokens ≈ (sum(tab_output_chars) + synthesis_chars) / 4`.

Tests: new regression
`test_parallel_usage_accounts_for_all_calls` runs a 4-way fan-out
against a fake SDK with deterministic chunk lengths and asserts
the reported usage scales with N, not the single-call shape.
2026-05-24 17:11:42 +00:00
Daniel Han
cb0680ebaa Studio: per-tab buffer for Codex fan-out so chunks cannot interleave
The Codex parallel-calls fan-out emits N independent SSE streams
concurrently, so a chunk for tab 2 can arrive between two chunks
for tab 1. The previous chat-adapter logic appended every chunk
into a single `cumulativeText` buffer in arrival order, which made
tab 1's text show up under tab 2's header (or vice versa) whenever
the workers raced. With four or more parallel calls the rendered
output became unreadable.

Replaced the append-on-arrival path with per-tab buffers keyed by
`tab_id`, plus a `renderCodexBuffer()` helper that rebuilds the
Codex block from scratch on every fan-out event:

- `codex_tab_open` allocates an empty buffer for the tab id.
- `codex_tab_chunk` appends only into that tab's buffer.
- `codex_tab_error` records the error string against the tab id.
- `codex_tab_close` marks the tab finished (visual separator).
- `codex_gather` flips a flag that draws the `--- Synthesis ---`
  divider; the synthesis text itself still arrives as a normal
  content delta on the same stream and so is not duplicated here.

The block is re-rendered in `tab_id` order on every event, so the
final output is deterministic regardless of arrival interleaving.
2026-05-24 17:07:00 +00:00
Daniel Han
f97a800d5b Studio: round 5e Codex hardening (4 follow-ups)
1. parallel_calls validator now clamps instead of 422-rejecting.
   The Pydantic schema was `int Field(ge=1, le=20)`, which was a
   regression from the pre-PR OpenAI-extra behaviour: a non-Codex
   client that sent the field with a legacy value like 0 (or a
   stray string from a misconfigured wrapper) now got a 422 even
   though the route silently ignores the field on every non-Codex
   provider. Replaced with a `field_validator(mode="before")` that
   coerces any input to the [1, 20] range, keeping the schema docs
   self-documenting while accepting legacy inputs.

2. Buffered Codex result with `final_response=None` no longer
   leaks `TurnResult(...)` Python object repr into the chat. The
   upstream SDK documents `TurnResult.final_response` as nullable
   for turns that perform tool work without producing a final
   assistant message; the previous `... or str(result)` fallback
   would render the repr as visible assistant text. New
   `_buffered_result_text` helper returns the empty string in that
   case so the stream finishes cleanly with no extra content
   chunk. Same fix applied to `_run_codex_synthesis`.

3. Device-login SSE no longer forwards arbitrary subprocess output
   to the browser. The previous code yielded every CLI line under
   `{type:"log"}`, which on a shimmed binary could leak refresh
   tokens, auth JSON, or local config paths into the authenticated
   stream. Filtered to a known-safe vocabulary ("Welcome to
   Codex", "Initializing", "Successfully logged in", etc.).
   `device_url` and `device_code` events still fire as before.

4. CodexLoginButton no longer calls `window.open` from inside an
   awaited SSE handler. Browser popup blockers (Firefox, Safari,
   Chrome strict) silently block popups triggered outside a fresh
   user gesture, so the auto-open was unreliable. Replaced with a
   prominent "Open verification page" button styled as an anchor;
   the click handler is a real user gesture and is never blocked.
   The URL string is still shown below the button for copy/paste.

Tests: 46 cases total (was 43). New regressions cover the
parallel_calls clamp path on three garbage inputs, the buffered
TurnResult-with-None-final repr leak guard, and the device-login
log filter (asserts refresh tokens / auth.json paths are dropped
while known-safe progress lines pass through).
2026-05-24 16:59:46 +00:00
Daniel Han
9b4bd11c9f Studio: surface Codex parallel-calls in the connections dialog
The `codexParallelCalls` field on `ExternalProviderConfig` was wired
through `chat-adapter.ts` (it is serialised over the wire as
`parallel_calls`) but the connections dialog never set or restored
it. With no UI input and no persistence path, the value was always
left as `undefined` after a reload, the adapter fell back to
`?? 1`, and the Codex fan-out path stayed permanently dormant from
the UI even though the backend supported it.

Three plumbing fixes:

1. Add a "Parallel calls" number input to the Codex form section,
   bounded to [1, CODEX_MAX_PARALLEL_CALLS]. Clamped on every key
   stroke so a hand-edited entry cannot exceed the backend cap.
2. Persist the value on `addProvider`, `saveProviderEdits`, and
   restore it on `editProvider` -- gated on `isCodexProviderType`
   so other providers cannot accidentally carry the field.
3. Preserve the value through `syncedProviders` rebuild on backend
   re-sync. The backend row does not store the fan-out width (it is
   local-only), so we copy it from the existing in-memory entry.

Form reset clears the field back to the default so opening "Add
connection" after editing a Codex provider does not pre-fill an
unrelated value.
2026-05-24 16:49:00 +00:00
Daniel Han
8c1c63a64d Studio: surface Codex final agent message when no deltas stream
The canonical openai_codex SDK can complete a turn successfully
without emitting any `message.delta` events: the final assistant
text arrives only as an `ItemCompletedNotification` whose item is
an `agentMessage`. Before this change `_stream_thread_run` would
loop through the stream, see no delta text, return, and Studio
would emit only the empty usage + stop + `[DONE]` frames -- the
user sees a blank reply for what was actually a complete answer.

Track agent-message texts collected during the stream loop and, if
no streamed deltas came through, yield the last one before
returning. The buffered `thread.run()` fallback is still gated by
the existing `emitted_any` flag so it never replays a turn that
already executed side effects (file writes, shell commands).

`_completed_agent_message_text` accepts both the upstream object
shape (`ItemCompletedNotification(item.root.text=...)`) and the
dict shape pre-release builds and tests use, so it works across SDK
revs without an explicit version gate.

Tests: new regression
`test_empty_stream_falls_back_to_completed_agent_message` exercises
a fake SDK whose `turn().stream()` yields only an `item.completed`
event with an `agentMessage`; the test asserts the final text
reaches the visible chat output and that the buffered `run()` path
is NOT re-executed.
2026-05-24 16:44:22 +00:00
Daniel Han
c5289f0249 Studio: pin Codex thread approvals + sandbox to safe defaults
The upstream openai_codex SDK defaults `approval_mode` to
`ApprovalMode.auto_review` (described in the SDK docs as "automatically
execute tools when permission escalations occur, without user
intervention") and leaves `sandbox` unset. Studio drives Codex from
a server-side chat request with no per-action approval UI, so leaving
those at the SDK defaults would let a model decide on its own to run
shell commands, write files, or hit the network on the operator's
machine.

This wires every `thread_start` call (single-turn, parallel-worker,
synthesis) through a helper that pins:

- `approval_mode = ApprovalMode.deny_all` -- reject any tool /
  command escalation rather than auto-approving it.
- `sandbox = SandboxMode.read_only` -- the policy that bans file
  writes and disables network.

The kwargs are looked up dynamically: when the installed SDK is too
old to expose either enum we log a structured warning and proceed
without them rather than refusing to run, so users on pre-release
alpha builds are not bricked. Once the canonical openai-codex SDK
is what every install pulls, the warning will be silent and the
safety pins will always apply.

Tests: three new regressions in TestCodexHardenedRegressions cover
the safe-pin path on a fake SDK that exposes the enums, the
warn-and-proceed path on a fake SDK that does not, and the same
pins on the synthesis turn so a fan-out tab cannot sneak an unsafe
default into the unification step.
2026-05-24 16:40:48 +00:00
Daniel Han
1a107651c4 Studio: round 5 hardening for the Codex provider
Five tightening fixes driven by the reviewer pass on top of round 4:

1. Drop OPENAI_API_KEY from the codex subprocess safe-list.
   The OpenAI provider key belongs to the OpenAI provider; a shimmed
   `codex` binary on PATH must not receive it. Users who want to wire
   the same key into Codex now set CODEX_OPENAI_API_KEY, which is the
   one OpenAI-shaped key we still forward.

2. Fail-closed env scrub on the SDK path.
   When AppServerConfig is missing from the installed openai_codex
   build, the bare AsyncCodex() constructor used to inherit the full
   os.environ via the SDK's internal os.environ.copy(). Replaced the
   fallback with a _ScrubbedEnvAsyncCodex wrapper that swaps
   os.environ for the lifetime of the session so HF_TOKEN, GH_TOKEN,
   WANDB_API_KEY etc never reach the spawned app-server.

3. Prefer the upstream-canonical base_instructions kwarg.
   The real openai_codex SDK takes the system prompt as
   `base_instructions`; our previous helper only knew `system`. Now
   tries base_instructions first, falls back to system, then inlines
   the system text in the user prompt as a last resort.

4. Filter visible text to answer-bearing event types only.
   _coerce_text used to render any payload that exposed a `delta` /
   `text` / `content` field, which let command output, file paths and
   tool call arguments leak into the Chat Completions reply. Gated on
   a _ANSWER_EVENT_TYPES allow-list (message.delta, completed,
   text_delta, etc.); untyped legacy dicts still pass through.

5. Treat the SDK as the install gate.
   openai-codex-cli-bin ships the codex runtime that backs
   AsyncCodex(...), so SDK alone is sufficient to drive the provider.
   `installed` no longer also requires a standalone codex binary on
   PATH; cli_path remains reported separately so the UI can still
   show whether a CLI is also installed.

Also added the matching positive-match regex line ("Authenticated:
Yes") for one more login-status wording the CLI ships in some
locales.

Tests: grown to 39 cases. New regressions cover the SDK-only install
gate, base_instructions kwarg priority + system fallback, the
fail-closed env scrub wrapper, the answer-only delta filter, the
Authenticated: Yes wording, and the CODEX_OPENAI_API_KEY-vs-
OPENAI_API_KEY split.
2026-05-24 16:20:27 +00:00
pre-commit-ci[bot]
593fc9edba [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-05-24 15:44:59 +00:00
Daniel Han
67b837e9f1 Merge branch 'feat/codex-provider' of https://github.com/unslothai/unsloth into feat/codex-provider 2026-05-24 15:44:11 +00:00
Daniel Han
4be807bbd8 Studio: round 4 hardening for the Codex provider
Fourth reviewer.py pass surfaced one more security finding and a
handful of correctness gaps. Each is small but the env-scrub for the
SDK path closes the asymmetric-fix loop opened in the previous round.

* Codex SDK construction now passes an `AppServerConfig(env=...)`
  that overrides every non-safe-listed env key to an empty string.
  Upstream openai/codex/sdk/python/client.py builds the spawn env as
  `os.environ.copy()` then `env.update(self.config.env)`, so this
  scrubs HF_TOKEN / GH_TOKEN / WANDB_API_KEY / ANTHROPIC_API_KEY etc.
  out of the codex app-server subprocess env on the chat / parallel
  / synthesis paths, matching the CLI/login paths from the previous
  round. The helper falls back to bare `AsyncCodex()` when the SDK
  version does not expose AppServerConfig, with logged warning.

* Install hint now names the actual upstream PyPI project,
  `openai-codex` (canonical), with `codex_app_server` documented as
  the legacy alias. The probe still accepts both import names so
  forward compat is preserved.

* Device-auth URL regex broadened to accept upstream's current
  `chatgpt.com/activate` shape and any `/device|/activate|/verify`
  variant, not just `/codex/device`. The frontend can now open the
  verification page on CLI builds that print the documented
  ChatGPT-style URL.

* `_run_codex_synthesis` now takes a `system` arg and forwards it
  to `thread_start(system=...)`, falling back to a prompt-prefix on
  older SDK revs that reject the kwarg. Previously a fan-out with
  "Always answer in Spanish" produced Spanish per-tab attempts but
  an English synthesis.

* `_detect_logged_in` negative regex now also matches "Not signed
  in", "Please sign in" (alternative localisations / future CLI
  releases). Same word-boundary anchoring as before.

* Frontend `CodexLoginEvent` union gains `device_code` and a `code`
  field. `CodexLoginButton` now renders the one-time code under the
  verification URL so users on a headless / remote install can copy
  the code without scraping the log pane. Also fixes a closure-stale
  bug where setError(message) was followed by a stale `error` read,
  losing specific backend errors; the new path keeps `lastStreamError`
  inside the closure.

* Replaced four hardcoded `/mnt/disks/...` paths in the new
  regression tests with `_backend_file()` resolved from `__file__`,
  so the suite runs in any checkout (CI, local dev, the review
  worker tree). Found by the round-4 reviewer.

Four new pytest cases pin the behaviour:
`test_not_signed_in_wording_also_handled`,
`test_device_url_accepts_generic_verification_url`,
`test_synthesis_call_forwards_system_prompt`, and
`test_sdk_env_scrubbed_via_appserverconfig`. 32/32 codex_provider
tests pass; `tsc --noEmit` clean.
2026-05-24 15:44:11 +00:00
pre-commit-ci[bot]
4b4c8553f8 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-05-24 15:16:52 +00:00
Daniel Han
fd8f25f507 Studio: scrub Codex subprocess env, guard partial-stream replay, abort login on unmount
Third reviewer.py pass found three remaining sharp edges. Each fix
is small and paired with a regression test where applicable.

* Codex subprocess env is now scrubbed to a safe-list before spawn.
  Both `_run_cli` in codex_availability and the device-auth spawn
  in stream_codex_device_login switch from `env=os.environ.copy()`
  to `env=_codex_subprocess_env()`, which forwards only PATH /
  HOME / USER / Windows-equivalents / CODEX_HOME / OPENAI_API_KEY /
  OPENAI_BASE_URL. Other-provider secrets like HF_TOKEN, GH_TOKEN,
  WANDB_API_KEY, ANTHROPIC_API_KEY no longer reach the local codex
  binary, so a shimmed `codex` earlier on PATH cannot harvest them.

* `_stream_thread_run` now tracks `emitted_any` and refuses to fall
  through to the buffered `await thread.run(prompt)` after either
  streaming helper has already yielded text. Previously a network
  glitch mid-stream re-executed the same Codex turn, which can
  duplicate file writes, shell commands, and other Codex side
  effects. The buffered path is now reserved for the zero-output
  case (no streaming helper resolved, or streaming returned empty).

* `CodexLoginButton` now aborts the SSE reader on unmount via a
  useEffect cleanup that calls `abortRef.current?.abort()`. The
  underlying `codex login --device-auth` subprocess no longer
  keeps streaming (and holding a device-auth session) after the
  dialog closes.

Two new pytest cases pin the behaviour: `test_codex_subprocess_env_scrubbed`
sets HF/GH/WANDB/ANTHROPIC keys and asserts none reach the codex
env while OPENAI_API_KEY / CODEX_HOME survive; and
`test_partial_stream_failure_does_not_replay_turn` injects a fake
`turn().stream()` that yields "partial output " then raises, and
asserts `thread.run()` is never called. 28/28 codex_provider tests
pass; `tsc --noEmit` clean.
2026-05-24 15:16:38 +00:00
Daniel Han
1f7cad4ccf Merge branch 'feat/codex-provider' of https://github.com/unslothai/unsloth into feat/codex-provider 2026-05-24 15:01:49 +00:00
Daniel Han
028b7b7187 Studio: finish Codex UI flow (test, sign-in, parallel tabs)
Second reviewer.py pass surfaced three follow-ups missed in the
earlier round. All caught by 12 parallel reviewers + cross-block
audit; each fix is small but user-facing.

* `testProvider` no longer pushes a Codex connection back to the
  edit form to "add an API key". Codex has no remote endpoint to
  ping, so the Test button now calls `/api/codex/status` directly:
  toasts success with the CLI version when installed+logged in,
  prompts to sign in when installed+logged out, and errors when
  the CLI or SDK is missing.

* The Sign-in to Codex affordance is now actually mounted. When
  the selected provider is Codex and `/api/codex/status` reports
  `installed:true, logged_in:false`, the dialog renders the new
  `CodexLoginButton` above the (hidden) API key row. The button's
  `onLoggedIn` callback re-probes status so the UI flips to the
  ready state without a page reload.

* The chat adapter now handles `codex_*` `_toolEvent` types
  instead of silently swallowing them. Per-tab chunks render
  inline with a `[Codex tab N/M]` header so users see each
  parallel attempt; `codex_gather` adds a `--- Synthesis ---`
  divider before the final unified content delta the backend
  also emits as plain text. This unblocks the existing fan-out
  path while a dedicated `CodexParallelTabs` UI is wired in a
  future change.

Verified: 26/26 codex_provider tests pass; `tsc --noEmit` on
studio/frontend completes clean.
2026-05-24 15:01:33 +00:00
pre-commit-ci[bot]
e2ac4907bf [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-05-24 14:46:17 +00:00
Daniel Han
d6c47f6664 Studio: wire Codex provider through the UI end-to-end
Post-review pass driven by reviewer.py. The original PR shipped the
backend codex provider, the registry entry (with `hidden:true`), the
status API, and the `CodexParallelTabs` component, but the chat UI
never surfaced the row, required an API key for the connection, and
never sent `parallel_calls` over the wire. Also fixes a CodeQL leak
in the parallel fan-out error path and adds the canonical streaming
hook upstream actually exposes.

Frontend
* chat-providers-dialog.tsx now calls `/api/codex/status` alongside
  `/api/providers/registry`. When the host has Codex installed the
  Add connection dialog gains a synthetic Codex row (curated model
  list comes from `supported_models`) so the picker is reachable.
* The Add / Edit connection guards now skip the API-key requirement
  for Codex the same way they do for the custom OpenAI-compat
  presets; the field itself is also hidden so the user is not asked
  for a key Studio will not use.
* chat-adapter.ts now also exempts Codex from the "Missing API key"
  pre-flight, and emits `parallel_calls` on the outgoing request
  when the selected connection is Codex (clamped to [1, 20] by the
  shared helper, defaults to 1).
* external-providers.ts adds `codexParallelCalls` to
  ExternalProviderConfig so future composer UI can persist the
  user's pick per connection.

Backend
* `_stream_thread_run` now tries `thread.turn(prompt).stream()`
  first, mirroring the canonical openai_codex API
  (`openai/codex/sdk/python/src/openai_codex/api.py`). The legacy
  `thread.run_streaming(prompt)` path is kept as a fallback and the
  buffered `await thread.run(prompt)` stays as the last resort.
* `_stream_codex_parallel` no longer echoes `str(exc)` in the
  `codex_tab_error` SSE event. Per-tab failures now surface a
  generic "Codex tab failed" message plus an `exception_type`
  discriminator; `CodexUnavailableError` is the only exception
  whose text is forwarded verbatim because it is a user-actionable
  install hint with no sensitive content (CodeQL
  `py/information-exposure-through-exception`).

Tests
* New `TestCodexHardenedRegressions::test_parallel_tab_error_sanitised`
  injects a fake SDK that raises with a path-like message and
  asserts the SSE frames do not echo it.
* New `TestCodexHardenedRegressions::test_thread_turn_stream_path_taken`
  verifies the canonical `thread.turn(prompt).stream()` hook is
  preferred over the legacy helper.

All 26 codex_provider tests pass. Frontend `tsc --noEmit` clean.
2026-05-24 14:46:03 +00:00
Daniel Han
0d904d615c Merge branch 'main' into feat/codex-provider 2026-05-24 14:25:41 +00:00
Daniel Han
b6577a6287 Studio: name the actual PyPI package in the Codex install hint
The OpenAI Codex Python SDK ships on PyPI as
`openai-codex-app-server-sdk`, not `openai-codex` (which is the
GitHub repo project name in pyproject.toml). The runtime binary
ships separately as `openai-codex-cli-bin`. Both packages expose
the import name `openai_codex`; the older docs reference
`codex_app_server` so we keep probing both.

Update the `CodexUnavailableError` message and the provider
registry notes so a user hitting the unavailable path gets a
copy-pasteable `pip install` command. No behaviour change.
2026-05-24 14:25:04 +00:00
Daniel Han
eeb49d54b8
Bump install.sh / install.ps1 pin to unsloth>=2026.5.7 (#5753)
PyPI release unsloth 2026.5.7 is now live. Bumps the pinned floor in
install.sh and install.ps1 from unsloth>=2026.5.6 to unsloth>=2026.5.7
so fresh installs resolve to the new wheel.

Tagged on main as v0.1.416-beta.
2026-05-24 07:22:49 -07:00
pre-commit-ci[bot]
861da31fcf [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-05-24 14:13:24 +00:00
Daniel Han
b8cd677397 Studio: harden Codex provider against upstream CLI/SDK shape
Followups on the post-merge review pass for the Codex SDK chat
provider. Verified against codex-cli 0.133.0 + the upstream
`openai/codex` Rust + Python sources, then pinned each fix with
a regression test in `test_codex_provider.py` (24/24 passing).

* Probe both `openai_codex` (canonical upstream Python package at
  `openai/codex/sdk/python`) and the legacy `codex_app_server`
  alias. Without this the availability probe always reported
  `sdk_importable: false` even when the SDK was installed, so the
  provider was permanently hidden.
* Switch the device-auth and login-status invocations from
  `codex auth login --device-auth` / `codex auth status` to the
  real upstream subcommands `codex login --device-auth` and
  `codex login status`. The former path returns
  `unrecognized subcommand 'auth'` on a real CLI.
* Strip ANSI control sequences before extracting the device URL
  (upstream wraps the URL in `\x1b[34m...\x1b[0m`) and tighten the
  pattern to the canonical `.../codex/device` shape. Also surface
  the one-time code as a `device_code` SSE event so the UI can
  show it alongside the URL.
* Fix `_detect_logged_in` substring footgun: `"logged in" in
  combined` matched inside `"not logged in"`, flipping logged-out
  users to logged-in. Anchor on word boundaries with negative
  prefixes winning regardless of return code.
* Cancel in-flight fan-out workers on SSE disconnect. Previously
  every parallel Codex turn ran to completion against a
  disconnected client and burned quota; now `_stream_codex_parallel`
  cancels its worker + drain tasks in a try/finally on
  `CancelledError`/`GeneratorExit`.
* Tear down the device-login subprocess on disconnect via
  `start_new_session=True` + `os.killpg(SIGTERM)` (Unix) or
  `CREATE_NEW_PROCESS_GROUP` + `CTRL_BREAK_EVENT` (Windows), with
  a bounded `proc.wait()` and `proc.kill()` fallback. Previously
  `finally: await proc.wait()` blocked the SSE close path because
  `codex login --device-auth` only exits on user action.
* Render the full conversation transcript in `_last_user_prompt`
  instead of returning only the most recent user message. The PR
  opens a fresh thread per request so prior assistant turns were
  dropped, degrading multi-turn chats to single-shot prompts.
  Single-turn input is unchanged.
* Make `ChatCompletionRequest.parallel_calls` default to 1 (`int`
  with `ge=1, le=20`) instead of `Optional[int] = None`. The
  runtime already coerced `None` -> 1, but the schema now matches
  the documented `[1, 20]` range.
* Replace the registry's hardcoded `default_models` (which
  contained `o3`, not in the upstream catalog) with the current
  `gpt-5.5 / 5.4 / 5.4-mini / 5.3-codex / 5.2` set from
  `codex-rs/models-manager/models.json`.
* Stop echoing `str(exc)` in SSE error frames in both
  `routes/inference.py` and `routes/codex.py`. The Codex SDK can
  raise with local paths, env-var content, or traceback fragments
  (CodeQL `py/information-exposure-through-exception`). Surface a
  generic message + `exception_type` discriminator; log the full
  reason server-side via `logger.error(..., exc_type=..., error=...)`.

Doc / comment updates throughout to refer to `codex login` /
`openai_codex` rather than the older incorrect strings.

Tested: pytest 24 cases in `test_codex_provider.py` (the original
14 + 10 new `TestCodexHardenedRegressions`) plus the rest of the
Studio-backend test suite the PR touches (209 passing). Also
verified live against Studio launched from this branch on a
Blackwell B200 via `UNSLOTH_STUDIO_HOME=$WORKSPACE/temp/...
./install.sh --local` then a Playwright probe.
2026-05-24 14:13:03 +00:00
Daniel Han
9c5d751c66 Fixes v0.1.416-beta 2026-05-24 07:11:21 -07:00
Daniel Han
f7f540a58b
Studio: strip orphan tool_call XML leaking into visible content (#5735)
* Studio: strip orphan tool_call XML from streamed visible content

The speculative-buffer state machine in
`studio/backend/core/inference/llama_cpp.py` can slice a tool_call XML
block between the silent DRAINING path and the user-visible
content_accum, depending on when in the model's emission the BUFFERING
-> STREAMING -> DRAINING transitions fire. Three leak shapes were
observed in a 2026-05-22 sweep of 900 Qwen3.5 / Qwen3.6 GGUF runs:

  Pre-fix XML leak rate: 20/900 (2.22%), concentrated 6.7% on the
  larger Q8 / MTP configs:

    Qwen3.6-35B-A3B Q8_0         4/60  (6.7%)
    Qwen3.6-35B-A3B-MTP Q4       4/60  (6.7%)
    Qwen3.5-35B-A3B Q8_0         3/60  (5.0%)
    Qwen3.6-27B Q8_0             3/60  (5.0%)

The existing `_TOOL_XML_RE` only matched well-formed
`<tool_call>...</tool_call>` and `<function=...></function>` pairs, so
unterminated openings (close was DRAINED) and orphan closes (opening
was DRAINED) survived the strip and reached the user.

Fix relaxes the regex to also strip:
  1. Orphan opening up to end-of-string: `(?:</tool_call>|\Z)`
  2. Orphan closing tag: bare `</tool_call>` / `</function>`

Verified on the full sweep: 20/900 -> 0/900 (100% of detected leaks
eliminated). 16 unit tests in `test_tool_xml_strip.py` pin all three
leak shapes plus the well-formed cases, plus parametrised checks on
the 5 actual real-world leak samples from the sweep data.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: strip tail-only </parameter> orphan + tighten regex

The 2026-05-22 gdpval sweep surfaced a 4th XML-leak shape not caught
by the earlier regex: a bare `</parameter>\n\n` at end-of-buffer (7
of 192 trials, all Qwen3.5-27B + a few Qwen3.6-27B). The model emits
the full `<tool_call><function=...><parameter=...>...content...
</parameter></function></tool_call>` envelope, the speculative buffer
DRAINS the opening tags as intended, but EOS (max_tokens cutoff)
truncates the outer `</function></tool_call>` close, leaving just
`</parameter>` as the visible tail.

We strip this ONLY when end-anchored (`\s*\Z`) so legitimate
mid-text uses (user code samples, documentation discussing the
Qwen tool-call XML shape) survive. Verified on the 192-trial
gdpval corpus: before=7, after=0.

While at it, fold the five top-level alternations into three by
sharing tag-name and prefix subgroups:

  <tool_call>...    + <function=\w+>...    +    -->  <(?:tool_call|function=\w+)>...
  </tool_call>      | </function>                  -->  </(?:tool_call|function)>

Semantically identical (verified by replay over the 192-trial
corpus + adversarial inputs, 0 diffs) and 1.34x faster on real
workloads. Backtracking-safety pinned by two new perf guards
(256KB '<' spam, 1000x orphan opens).

Tests: 16 -> 28 (6 new functional + 4 well-formed-vs-orphan +
2 perf guards).

* Tighten comments in XML-strip regex and tests

Code says what it does; comments were repeating it. Strip the verbose
explanations down to the WHY-only bits (engine quirk, tail-anchor
rationale, real-world source of each test sample). No code changes.

inference.py:  21 -> 12 lines around _TOOL_XML_RE
test_tool_xml_strip.py: 343 -> 259 lines (-84)
Tests: 28/28 still pass.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-05-24 05:00:08 -07:00
Daniel Han
56e9046b2f
Lower default weight_decay in RL config from 0.01 to 0.001 (#5747)
In full FT, AdamW weight decay shrinks the parameter directly so the
implicit prior is W -> 0. In LoRA the trained parameters are A and B
while the effective weight is W = W_init + (alpha/r) * B @ A; decaying
A and B separately drives BA -> 0, hence W -> W_init rather than 0.
The previous default of 0.01 inherited from full-FT recipes adds a
measurable pull on the merged adapter back toward the base model over
a few thousand steps. 0.001 keeps a small Frobenius-norm prior on
||A||^2 + ||B||^2 for numerical stability without meaningfully biasing
the merged weight toward init, and aligns with the value used across
the unsloth notebook templates.
2026-05-23 22:28:59 -07:00
Daniel Han
dfb3eedf77
ci: broaden Linux + narrow Windows llama.cpp runtime patterns + trim #5741 comments (#5746)
* ci: broaden Linux llama.cpp runtime pattern to lib*.so*

#5741 patched the explicit Linux pattern list to add
``libllama-*-impl.so*`` after ggml-org/llama.cpp#23462 (between
b9279 and b9283) split each binary's entry code into a paired
``lib<binary>-impl.so`` shared library. Same class of upstream
repackaging will hit us again whenever a new shared lib is added.

Mirror what macOS already does and replace the per-lib list with a
single ``lib*.so*`` glob. ``copy_globs`` (line 3614) unions
patterns, so the per-variant ``libggml-cuda.so*`` / ``libggml-hip.so*``
entries were never filtering anything; the spec lives in
``runtime_payload_health_groups`` (line 5209) which keeps the
explicit minimum-required list per variant.

Dry-run against b9296-bin-ubuntu-x64.tar.gz: 40 files copied (all
ggml, llama, mtmd, impl variants + the two binaries we ship), 22
skipped (other CLIs, rpc-server, LICENSE). Functionally equal to
the post-#5741 set.

* cleanup: trim #5741 comments on the pydantic split

Comments added in #5741 explained the original bug in full each
time. They are mostly redundant with the commit message and the PR.
Trim them to one short paragraph per site.

No behavior change.

* ci: narrow Windows runtime pattern to llama-server.exe + llama-quantize.exe

Studio only invokes llama-server and llama-quantize. Mac and Linux
already filter to those two binaries; Windows was the odd one out
with ``*.exe`` copying every CLI upstream ships (llama-cli,
llama-bench, llama-mtmd-cli, ...).

Dry-run on b9296 (win cpu-x64, cpu-arm64, cuda-13.1, hip-radeon):
20 unused EXEs skipped per variant, all DLLs (incl. the new
llama-*-impl.dll family) still copied via ``*.dll``.

``existing_install_matches_choice`` already checks llama-server.exe
exists explicitly (line 5297), so the health gate is unchanged.
2026-05-23 21:48:12 -07:00
Daniel Han
0a4309fefd Studio: add codex_router to test_desktop_auth router stub
test_health_response_reports_desktop_capability_fields builds a
SimpleNamespace as a fake routes module so it can exercise
main.health_check without standing the full app up. The stub
listed every router name except codex_router, which lands in the
main.py import block alongside the others as of this PR, so the
import failed with 'cannot import name codex_router from <unknown
module name>' on the Python 3.13 unit run.

Add the codex_router slot to the stub.
2026-05-23 14:00:31 +00:00
Daniel Han
4188f916f4 wip: anthropic citation helper 2026-05-23 14:00:31 +00:00
pre-commit-ci[bot]
ea7ae85562 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-05-23 14:00:31 +00:00
Daniel Han
cbc3c43655 Studio: add Codex SDK as a chat provider with parallel-calls fan-out
Wires the OpenAI Codex CLI / Python SDK (codex_app_server) into Studio
as a new chat provider type. Hosts that don't have the CLI or the SDK
installed never see the entry; on logged-out hosts the provider config
dialog renders a device-auth Sign-in button that surfaces the
verification URL and streams CLI progress back over SSE.

Backend
- new core/inference/codex_availability.py probes the CLI + SDK and
  reports {installed, logged_in, version, supported_models}; it never
  imports codex_app_server at module top level so the rest of the
  backend keeps starting cleanly on hosts that don't have the SDK.
- new core/inference/codex_provider.py wraps AsyncCodex and translates
  Codex events into OpenAI chat-completion chunks. Supports the
  thread.run_streaming path with a non-streaming fallback for older
  SDK revs.
- parallel_calls > 1 fans the turn out across N tasks (capped at 20)
  via asyncio.gather and emits codex_tab_open / codex_tab_chunk /
  codex_tab_close tool-events per attempt plus a final codex_gather
  synthesis event. A separate standalone Codex call produces the
  unified answer.
- new routes/codex.py exposes GET /api/codex/status and POST
  /api/codex/login. The login route shells out to
  codex auth login --device-auth and streams events; the first event
  carries the verification URL so the frontend can window.open it.
- ChatCompletionRequest gains a parallel_calls field bounded [1, 20]
  by pydantic. The codex registry entry stays hidden by default; the
  /api/codex/status probe is the authoritative gate.
- routes/inference.py dispatches provider_type=codex through the
  local CLI/SDK pipeline instead of the standard HTTP client, with
  graceful error surfacing for CodexUnavailableError.

Frontend
- new api/codex-api.ts exposes fetchCodexStatus() and an async
  generator streamCodexDeviceLogin() that drives the SSE stream and
  yields parsed events.
- new components/codex-parallel-tabs.tsx renders the tabbed parallel-
  calls UI with a Synthesis tab highlighted once the codex_gather
  event arrives. Pure reducer keeps the state transitions unit-
  testable.
- new components/codex-login-button.tsx posts to /api/codex/login,
  opens the verification URL in a new tab via window.open, and shows
  the streamed CLI log as it lands.
- external-providers.ts exports CODEX_PROVIDER_TYPE,
  CODEX_MAX_PARALLEL_CALLS, isCodexProviderType, and
  clampCodexParallelCalls. Codex is marked text-only so the composer
  hides image-attach affordances when selected.

Tests
- tests/test_codex_provider.py (14 cases) covers the availability
  probe across the four install / login states, the streaming +
  parallel-calls translation against a fake codex_app_server module
  injected into sys.modules, the [1, 20] pydantic clamp, the
  CodexUnavailableError surfacing path, and the parallel_calls=1
  single-call shape (no tab tool-events).
2026-05-23 14:00:31 +00:00
Daniel Han
83b20976f7
ci: unblock Studio Windows + Linux + Mac smoke (#5741)
Bundles three independent CI regressions hitting the maintainer PR
backlog. Each one is verified end-to-end on a staging fork against
real Ubuntu / macOS / Windows GitHub-hosted runners before this
lands.

1. Windows --no-torch install: pydantic + pydantic-core drift to
   incompatible versions under `uv pip install --no-deps -r
   no-torch-runtime.txt` because pip resolves each independently
   from latest. pydantic.VERSION 2.13.4 pins pydantic-core==2.46.4
   but pydantic-core 2.47.0 was the freshest published wheel, so
   `import pydantic` raised
   `SystemError: pydantic-core 2.47.0 is incompatible with the
   current pydantic version`. Resolve pydantic WITH deps in a
   focused pip call (install.sh, install.ps1,
   install_python_stack.py) before the --no-deps no-torch-runtime
   pass so pip pins pydantic-core to the version pydantic declares.
   pydantic's transitive deps (annotated-types, pydantic-core,
   typing-extensions, typing-inspection) are torch-free. Drop the
   redundant `Patch Studio venv with full typer / pydantic dep
   trees` workaround from the four Windows smoke YAMLs.
   Supersedes #5733 + #5734.

2. Linux Studio Update CI: upstream llama.cpp b9261+ split each
   binary's entry code into a paired `libllama-<binary>-impl.so`
   shared library. `llama-server` and `llama-quantize` NEEDED-link
   against `libllama-server-impl.so` / `libllama-quantize-impl.so`
   with RUNPATH `$ORIGIN`, so the prebuilt overlay must copy those
   alongside the binaries. Without that, ldd reports them missing,
   preflight rejects, the installer falls back to source build, and
   studio-update-smoke annotates `setup.sh idempotency regressed`.
   Add `libllama-*-impl.so*` to the Linux runtime patterns and lock
   the pattern in test_rocm_support.TestRuntimePatterns.

3. Mac Studio UI Chat: change-password submit clicked while
   disabled. The disable gate only checked new + confirm password
   length, but Playwright's first click landed before the
   current-password field's React state had committed, so the form
   was simultaneously logically-invalid (current_password empty) and
   the button was disabled. Tighten the gate to require
   `currentPassword.length >= 8` and mirror the same check in the
   submit handler so Enter / autofill cannot bypass.
   Supersedes #5738.
2026-05-23 06:59:16 -07:00
Daniel Han
382683ebdc
Bump pinned PyPI floor to unsloth>=2026.5.6 (#5716)
PyPI release 2026.5.6 is now live; update install.sh and install.ps1 to
pin against the new minimum so fresh installs pick up the latest wheel.

Co-authored-by: Michael Han <michaelhan2050@gmail.com>
2026-05-22 09:31:28 -07:00
Daniel Han
18c300197c API fixes + pypi v0.1.415-beta 2026-05-22 09:16:53 -07:00
Datta Nimmaturi
ed1e3929ad
fix(gpt-oss): prefer flex attention over sdpa (#5701)
* fix(gpt-oss): prefer flex attention over sdpa

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix(gpt-oss): use eager config for unsupported backends

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-05-22 08:38:38 -07:00
Wasim Yousef Said
df2d31fea8
Truncate long code execution tool output (#5708) 2026-05-22 07:45:58 -07:00
Daniel Han
e9cf735f1b
Studio: render generated images inline for the Images pill (#5705)
The pill wired the request end of the loop but the response was lost
on the client: the backend emits a `tool_end` _toolEvent carrying the
base64 PNG on `image_b64` / `image_mime`, but the chat-adapter only
read the `result` string and the generic ToolFallback printed the
prompt as JSON args with an empty Result block -- the "I see no
image" symptom in the chat.

- chat-adapter: when the closing `tool_end` is for `image_generation`,
  repackage `image_b64` + `image_mime` (+ size/quality/background)
  into a structured result object instead of dropping them.
- New `ImageGenerationToolUI` reads that result and renders the image
  inline via `<img src="data:image/...;base64,...">` with the prompt
  as a caption. Falls back to a spinner while the request is still
  running.
- Register the component under `image_generation` in thread.tsx's
  tools.by_name map so it preempts ToolFallback for this tool only.
2026-05-22 07:22:37 -07:00
Wasim Yousef Said
8b235c752b
Fix connected chat model persistence (#5702) 2026-05-22 07:20:24 -07:00
Daniel Han
b89e28a836
Studio: expose Anthropic 5m vs 1h prompt cache TTL in Configuration (#5703)
#5685 wired the backend to honor `prompt_cache_ttl` on the request,
but there was no UI to actually pick it -- every Studio chat ended up
on Anthropic's default 5 minute pool. This adds a Cache TTL selector
to the chat settings sheet's Provider section, visible only when the
provider supports the choice (Anthropic today) and Prompt caching is
on.

- New `promptCacheTtl?: "5m" | "1h"` on `ExternalProviderConfig`.
  Normalizer drops the field on providers that don't support the
  choice so localStorage stays clean across provider swaps.
- `supportsProviderPromptCacheTtl` + `isPromptCacheTtl` helpers so
  the picker, normalizer, and adapter all agree on which values are
  valid.
- Settings sheet renders a small Select (5 minutes / 1 hour) right
  under the Prompt caching switch when the toggle is on; flipping
  it persists on the provider config like the other per-provider
  knobs.
- chat-adapter passes `prompt_cache_ttl` on outbound requests when
  the value is valid; omitted otherwise so the backend keeps
  inheriting Anthropic's 5m default.
2026-05-22 07:16:30 -07:00
Daniel Han
7e0ee4a719
Studio: surface OpenAI image_generation as composer Images pill (#5699)
The backend already wires OpenAI's Responses-API image_generation
server tool: when `enabled_tools` carries "image_generation" on an
OpenAI cloud request, _stream_openai_responses appends
`{type: "image_generation"}` to the request's tools array and emits
`image_generation_call` output items back to the assistant stream
(see backend/core/inference/external_provider.py and
backend/tests/test_openai_image_generation.py for the round-trip).

This wires the frontend half so a user can actually opt into it from
the composer next to the Search and Code pills, instead of the tool
sitting dormant.

- `providerSupportsBuiltinImageGeneration` gates on OpenAI cloud
  (`api.openai.com`) + a Responses-API model prefix (gpt-5.x, o3).
  Mirror of the backend's `is_openai_cloud` guard so the pill is hidden
  on custom OpenAI-compat backends (ollama / llama.cpp / vLLM) that
  report `provider_type="openai"` but would 400 on the tool.
- New `imageToolsEnabled` flag in chat-runtime-store, persisted under
  `unsloth_chat_image_tools_enabled` and reset on model change in
  chat-page exactly like `codeToolsEnabled`.
- `chat-adapter` appends "image_generation" to `enabled_tools` and
  flips `enable_tools: true` when the pill is on, so the existing
  backend dispatch picks it up.
- Composer renders an Images pill (lucide `ImageIcon`) immediately
  after the Code pill, only when the active model advertises the
  capability. The in-thread composer (assistant-ui/thread.tsx) gets
  the matching `ImagesToggle` for parity.
2026-05-22 07:08:42 -07:00
Daniel Han
9d3ad3ba12
Studio: also persist external checkpoint when picker calls setParams (#5700)
The first pass only wired the localStorage mirror into `setCheckpoint`,
but the main chat-page picker actually selects an external model by
calling `setParams({ ...store.params, checkpoint: value })`. That path
never hit `setCheckpoint`, so the persisted slot stayed empty and a
refresh fell back to whatever `/api/inference/status.active_model`
returned -- the previously loaded local model (Qwen3.5 etc) or null
("Select model") when nothing was loaded locally.

Mirror the persistence in `setParams` whenever the checkpoint changes
so every entry point converges on the same behavior. `setCheckpoint`
still does it directly so the load path (compare, GGUF auto-load,
gemma fallback in chat-adapter) keeps working.
2026-05-22 07:06:49 -07:00
Lee Jackson
51736a7766
Studio: add Anthropic and OpenAI prompt guards for disabled tools (#5674)
* Add Anthropic prompt guards for disabled tools

* fix: merge Anthropic tool guard into structured system prompts

* fix: scope Anthropic disabled-tool guard wording

* chore: adjust claude guard prompt

* chore: add openai to list of prompt guarded providers

* Studio: include web_fetch in the per-turn disabled-tool guard

Add webFetchEnabledForThisTurn alongside webSearchEnabledForThisTurn
and codeExecEnabledForThisTurn. Use it in the enabled_tools payload
so web_fetch follows the Search pill the same way web_search does,
and mention "web fetch" in the disabled-tool guard prose on providers
that ship the tool (Anthropic today; other providers stay inert via
providerSupportsBuiltinWebFetch).

---------

Co-authored-by: Roland Tannous <115670425+rolandtannous@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-05-22 07:03:56 -07:00
Daniel Han
228d1cd40c
Studio: persist external provider selection across page refresh (#5697)
Selecting a connected external provider (Anthropic, OpenAI, Google, etc.)
and refreshing the page reverted the picker back to no selection. Root
cause is that `PersistedInferenceParams` in `chat-settings-api.ts`
excludes `checkpoint` from the server-side settings payload by design.
Local model selections survive refresh because the backend re-derives
them from `/api/inference/status.active_model`, but external selections
have no backend mirror, so they were lost.

Fix: persist `external::*` checkpoints to a small dedicated
`localStorage` key (`unsloth_chat_last_external_checkpoint`) and hydrate
from it on store init. Local checkpoints continue to come from the
backend status as before; only external ids are mirrored client-side.
`setCheckpoint` writes the key when an external id is selected and
clears it when switching back to a local id, and `clearCheckpoint`
clears it so the picker does not snap back after an explicit reset.
2026-05-22 06:45:27 -07:00