Skip to content

[Improvement] Per-model coordinate spaces via coord_scale - #71

Open
aaronsmulktis wants to merge 6 commits into
mainfrom
aaronsmulktis/coord-rescale
Open

[Improvement] Per-model coordinate spaces via coord_scale#71
aaronsmulktis wants to merge 6 commits into
mainfrom
aaronsmulktis/coord-rescale

Conversation

@aaronsmulktis

@aaronsmulktis aaronsmulktis commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

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:

convention coord_scale models
raw viewport pixels null UI-TARS 1.5, GPT-4o-style computer use
normalized 0-1000 1000 Qwen-VL, GLM-VL
normalized [0, N) N PaliGemma/Gemma lineage bins to 1024

Getting 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_xy is now the one conversion point; every parser reaches it through ActionParser.rescale. Each family carries a default (uitars: raw pixels, qwen3vl: 0-1000), overridable per model in the agent yaml with coord_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_scale silently not apply:

  1. browsergym-syntax mouse actions were never rescaled. uitars_parser only remapped UI-TARS-native forms (click(point=…) and friends). A model prompted in browsergym syntax emits mouse_click(x=, y=), which matched none of them and passed through untouched. This is the common case — config/agent/default.yaml has always prompted for mouse_click(x=612, y=455). Now rewritten in place, preserving trailing kwargs so button='right' survives.

  2. The "no conversion" path still converted. UITarsActionParser.parse always passed a rescale hook, even with coord_scale=None, so raw pixels round-tripped through int(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 as x=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:

run model's stated coords coord_scale executed expected
1 (88, 238) 1024 (165, 251) (165, 251) ✓
2 (611, 645) 1000 (1173, 697) (1173, 697) ✓

Exact 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_scale to match — correct by construction if the model complies — rather than reverse-engineering a checkpoint's native convention. Qwen3.6-VL-computer-use.yaml does exactly this.

Notes for review

  • rescale_xy applies 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.
  • The (1920, 1080) viewport fallback in vLLM_prompt.py now logs a warning. It is only reachable with use_screenshot off, but under a non-1080p preset it would silently miscale every converted coordinate.
  • The default UI-TARS path is unchanged and pinned by a regression test.
  • The README documents a calibration recipe using the ground-truth boxes in set_of_marks_coordinates.json, so the next model's convention gets measured rather than guessed.

Testing

46 tests in tests/test_action_parsers.py and tests/test_uitars_parser.py, covering rescale_xy directly (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.

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.
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Aug 17, 2026
@aaronsmulktis aaronsmulktis self-assigned this Aug 17, 2026
@aaronsmulktis
aaronsmulktis requested review from marksibrahim and a lite review from Copilot August 18, 2026 01:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) and ActionParser.rescale, plus export wiring.
  • Introduced configurable per-model coord_scale (family defaults + YAML override) and propagated it through VLLMAgent.
  • 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 thread src/open_apps/agent/vLLM_agent.py Outdated
Comment thread config/agent/default.yaml Outdated
Comment thread src/open_apps/agent/README.md Outdated
Comment on lines +384 to +385
x, y = rescale(coords[0], coords[1])
result["action"] = f"mouse_click(x={x}, y={y})"
aaronsmulktis and others added 3 commits August 24, 2026 11:02
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants