[Improvement] Per-model coordinate spaces via coord_scale - #71
Open
aaronsmulktis wants to merge 6 commits into
Open
[Improvement] Per-model coordinate spaces via coord_scale#71aaronsmulktis wants to merge 6 commits into
aaronsmulktis wants to merge 6 commits into
Conversation
Vision models disagree about what an (x, y) in their output means, and getting it wrong is silent: the click lands somewhere plausible and the episode scores 0 without an error. Gemma clicking off-target under screenshot-only is this failure. Add action_parsers/coords.py::rescale_xy as the single conversion point and route every parser through ActionParser.rescale: - ActionParser gains a coord_scale field. None = raw viewport pixels, N = a normalized [0, N) grid. - qwen3vl's hardcoded _COORD_SPACE = 1000 becomes coord_scale = 1000, so it is now overridable rather than baked in. Behavior unchanged. - uitars was discarding its viewport argument and passing raw pixels straight through, so no normalized-grid model could use that grammar. It now threads a rescale hook into flexible_parser/uitars_parser, applied to click, right_single, and the scroll magnitude (which UI-TARS reads off a point, so it shares the coordinate space). coord_scale defaults to None, keeping the UI-TARS path byte-identical -- pinned by a regression test. - Plumb AgentArgs.coord_scale -> VLLMAgent -> get_action_parser, so a family's default can be overridden per model in yaml. Passing None means "keep the family default", not "raw pixels". rescale_xy applies one scalar against each viewport axis independently, which is what a square normalized grid means; it cannot express a model predicting in its own non-square resized image space. Noted in the docstring. Also log a warning on the (1920, 1080) viewport fallback in vLLM_prompt. It is only reachable with use_screenshot off, but under a non-1080p preset it would silently miscale every rescaled coordinate. Gemma's actual convention is unmeasured -- the Gemma lineage bins locations to 0-1024, but that is inference from the family, not data. A wrong scale fails exactly like no scale, so README documents a calibration recipe using set_of_marks_coordinates.json as the oracle rather than shipping a guessed default.
Two gaps in the coord_scale plumbing, both of which made a normalized coordinate space silently not apply. 1. A model prompted directly in browsergym syntax emits mouse_click(x=, y=) rather than UI-TARS's click(point=), so it matched none of the remaps in uitars_parser and skipped rescaling entirely. This is the common case -- config/agent/default.yaml has always prompted for mouse_click(x=612, y=455). Rewrite those in place via _BG_MOUSE_XY_RE, preserving trailing kwargs so button='right' survives, and covering dblclick/move/down/up alongside click. 2. UITarsActionParser.parse always passed a rescale hook, even with coord_scale=None, so "no conversion" still round-tripped through int(round(float(x))). That rounded float coordinates and rewrote already-valid calls. Pass no hook at all when there is nothing to convert, so the default path leaves coordinates exactly as written. The regression guard for (2) is what caught it: mouse_click(x=500.5) was coming back as x=500 on the default UI-TARS path.
The browsergym-syntax rescaling added in the previous commit was not described anywhere. Also note the declare-the-grid-in-the-prompt approach, which is what makes the conversion correct by construction rather than a guess about a checkpoint's native convention.
aaronsmulktis
requested review from
marksibrahim
and
a lite review from Copilot
August 18, 2026 01:20
Contributor
There was a problem hiding this comment.
Pull request overview
This PR centralizes coordinate-space conversion for vision-model actions by introducing a single rescale_xy implementation and plumbing a per-parser (and per-model override) coord_scale through the agent and action parser registry. It also updates UI-TARS parsing to correctly rescale browsergym-form mouse actions and adds warnings when viewport fallback could silently mis-scale coordinates.
Changes:
- Added shared coordinate conversion (
action_parsers/coords.py::rescale_xy) andActionParser.rescale, plus export wiring. - Introduced configurable per-model
coord_scale(family defaults + YAML override) and propagated it throughVLLMAgent. - Expanded tests and documentation around coordinate conventions and UI-TARS/browsergym rescaling behavior.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_action_parsers.py | Adds coverage for coord_scale defaults/overrides and rescale_xy, plus UI-TARS/browsergym rescaling cases. |
| src/open_apps/agent/vLLM_prompt.py | Logs a warning when falling back to a default viewport (important for coordinate rescaling correctness). |
| src/open_apps/agent/vLLM_agent.py | Adds coord_scale to agent args and passes it into get_action_parser. |
| src/open_apps/agent/utils.py | Extends flexible_parser/uitars_parser to accept an optional rescale hook and rescales browsergym mouse actions. |
| src/open_apps/agent/README.md | Documents coordinate-space conventions and a calibration workflow. |
| src/open_apps/agent/action_parsers/uitars.py | Makes UITars parser carry coord_scale and conditionally provide rescaling behavior. |
| src/open_apps/agent/action_parsers/qwen3vl.py | Replaces hardcoded coordinate space with configurable coord_scale via ActionParser.rescale. |
| src/open_apps/agent/action_parsers/coords.py | New shared coordinate conversion implementation. |
| src/open_apps/agent/action_parsers/base.py | Adds coord_scale to the base parser and provides rescale. |
| src/open_apps/agent/action_parsers/init.py | Adds optional coord_scale override and exports rescale_xy. |
| config/agent/default.yaml | Documents new coord_scale configuration knob. |
Suppressed comments (1)
src/open_apps/agent/utils.py:420
- The right-click coordinate extraction uses
re.findall(r"\d+"), which mis-parses float coordinates and drops negative signs, producing incorrect coordinates before any rescaling.
x, y = rescale(coords[0], coords[1])
result["action"] = f"mouse_click(x={x}, y={y}, button='right')"
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+384
to
+385
| x, y = rescale(coords[0], coords[1]) | ||
| result["action"] = f"mouse_click(x={x}, y={y})" |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Gives OpenApps a single place where a model's predicted coordinates are converted into viewport pixels, and fixes two bugs found while building it.
Why
Vision models disagree about what an
(x, y)in their output means:coord_scalenull1000[0, N)NGetting this wrong is silent. The click lands somewhere plausible, the page does not change, and the episode scores 0 with no error anywhere in the logs. Before this change the only handling lived inside the Qwen parser as a hardcoded
_COORD_SPACE = 1000, so no other family could express a convention at all.What changed
action_parsers/coords.py::rescale_xyis now the one conversion point; every parser reaches it throughActionParser.rescale. Each family carries a default (uitars: raw pixels,qwen3vl: 0-1000), overridable per model in the agent yaml withcoord_scale: N. Qwen's hardcoded constant becomes that field — same behaviour, now expressible rather than baked in.Two real bugs, both of which made a configured
coord_scalesilently not apply:browsergym-syntax mouse actions were never rescaled.
uitars_parseronly remapped UI-TARS-native forms (click(point=…)and friends). A model prompted in browsergym syntax emitsmouse_click(x=, y=), which matched none of them and passed through untouched. This is the common case —config/agent/default.yamlhas always prompted formouse_click(x=612, y=455). Now rewritten in place, preserving trailing kwargs sobutton='right'survives.The "no conversion" path still converted.
UITarsActionParser.parsealways passed a rescale hook, even withcoord_scale=None, so raw pixels round-tripped throughint(round(float(x)))— rounding floats and rewriting already-valid calls. It now passes no hook at all. Caught by its own regression test:mouse_click(x=500.5)was coming back asx=500.On the motivation, and what the evidence actually showed
This work was prompted by Gemma 4 31B clicking off-target in screenshot-only mode, on the theory that coordinates needed rescaling. Cluster runs falsified that theory, and reviewers should not read this PR as a fix for it.
Two runs, inverting the executed action back through the conversion:
coord_scaleExact to the pixel, both times, at two different scales on a 1920×1080 viewport. The pipeline faithfully put the cursor where the model asked. Gemma's failure is upstream of the conversion — its own spatial estimate is wrong — which is the case the README describes as grounding badly, not scaling wrong. Set-of-marks bids are the route for that model.
So the value here is not the original motivation. It is: Qwen's convention becomes configurable instead of hardcoded, two latent bugs are fixed, and any future family (GLM-VL, a Gemma checkpoint that does ground well) can declare its space in one line.
The most reliable setup, documented in the README, is to declare the grid in the prompt and set
coord_scaleto match — correct by construction if the model complies — rather than reverse-engineering a checkpoint's native convention.Qwen3.6-VL-computer-use.yamldoes exactly this.Notes for review
rescale_xyapplies one scalar against each viewport axis, which is what a square normalized grid means. It cannot express a model predicting in its own non-square resized image space; that would need a per-axis pair. Called out in the docstring.(1920, 1080)viewport fallback invLLM_prompt.pynow logs a warning. It is only reachable withuse_screenshotoff, but under a non-1080p preset it would silently miscale every converted coordinate.set_of_marks_coordinates.json, so the next model's convention gets measured rather than guessed.Testing
46 tests in
tests/test_action_parsers.pyandtests/test_uitars_parser.py, coveringrescale_xydirectly (raw passthrough, 0-1000, 0-1024, per-axis scaling, negative deltas, zero-scale), both parser families, the browsergym-form rewrite, and the unchanged default path.