use-computer 0.0.39__tar.gz → 0.0.41__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. {use_computer-0.0.39 → use_computer-0.0.41}/PKG-INFO +3 -1
  2. {use_computer-0.0.39 → use_computer-0.0.41}/pyproject.toml +3 -1
  3. {use_computer-0.0.39 → use_computer-0.0.41}/scripts/smoke_wheel.py +1 -1
  4. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/base/actions.py +1 -0
  5. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/base/desktop.py +70 -0
  6. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/base/recordings.py +25 -11
  7. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/providers/generic.py +20 -30
  8. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/providers/openai.py +60 -23
  9. use_computer-0.0.41/use_computer/agents/providers/tinker_backend.py +163 -0
  10. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/client.py +18 -8
  11. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/harbor/environment/platforms/__init__.py +2 -1
  12. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/sandbox.py +14 -4
  13. {use_computer-0.0.39 → use_computer-0.0.41}/uv.lock +879 -29
  14. {use_computer-0.0.39 → use_computer-0.0.41}/.env.example +0 -0
  15. {use_computer-0.0.39 → use_computer-0.0.41}/.gitignore +0 -0
  16. {use_computer-0.0.39 → use_computer-0.0.41}/.pre-commit-config.yaml +0 -0
  17. {use_computer-0.0.39 → use_computer-0.0.41}/README.md +0 -0
  18. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/__init__.py +0 -0
  19. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/accessibility.py +0 -0
  20. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/__init__.py +0 -0
  21. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/base/__init__.py +0 -0
  22. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/base/agent.py +0 -0
  23. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/base/artifacts.py +0 -0
  24. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/base/final_state.py +0 -0
  25. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/base/prompts.py +0 -0
  26. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/base/sandbox.py +0 -0
  27. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/base/screenshots.py +0 -0
  28. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/base/trajectory.py +0 -0
  29. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/core.py +0 -0
  30. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/debug/README.md +0 -0
  31. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/debug/__init__.py +0 -0
  32. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/debug/actions.py +0 -0
  33. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/debug/agent.py +0 -0
  34. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/debug/models.py +0 -0
  35. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/ios/__init__.py +0 -0
  36. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/ios/agent.py +0 -0
  37. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/ios/tools.py +0 -0
  38. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/prompts/anthropic.txt +0 -0
  39. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/prompts/gemini.txt +0 -0
  40. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/prompts/ios.txt +0 -0
  41. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/prompts/openai.txt +0 -0
  42. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/prompts/pyautogui.txt +0 -0
  43. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/providers/__init__.py +0 -0
  44. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/providers/anthropic.py +0 -0
  45. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/providers/gemini.py +0 -0
  46. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/types.py +0 -0
  47. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/ax_transpile.py +0 -0
  48. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/display.py +0 -0
  49. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/errors.py +0 -0
  50. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/harbor/__init__.py +0 -0
  51. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/harbor/agents.py +0 -0
  52. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/harbor/environment/__init__.py +0 -0
  53. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/harbor/environment/gateway.py +0 -0
  54. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/harbor/environment/platforms/ios.py +0 -0
  55. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/harbor/environment/platforms/macos.py +0 -0
  56. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/harbor/environment/platforms/ubuntu.py +0 -0
  57. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/harbor/environment/platforms/windows.py +0 -0
  58. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/harbor/environment/setup_files.py +0 -0
  59. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/harbor/environment/use_computer.py +0 -0
  60. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/harbor/environment/utils/__init__.py +0 -0
  61. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/harbor/environment/utils/verifier.py +0 -0
  62. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/ios/__init__.py +0 -0
  63. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/ios/apps.py +0 -0
  64. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/ios/environment.py +0 -0
  65. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/ios/input.py +0 -0
  66. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/macos/__init__.py +0 -0
  67. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/macos/keyboard.py +0 -0
  68. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/macos/mouse.py +0 -0
  69. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/models.py +0 -0
  70. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/parsers.py +0 -0
  71. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/py.typed +0 -0
  72. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/recording.py +0 -0
  73. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/resources/__init__.py +0 -0
  74. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/resources/accessibility.py +0 -0
  75. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/resources/display.py +0 -0
  76. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/resources/recording.py +0 -0
  77. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/resources/screenshot.py +0 -0
  78. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/retry.py +0 -0
  79. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/screenshot.py +0 -0
  80. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/simulators.py +0 -0
  81. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/tasks/__init__.py +0 -0
  82. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/tasks/templates/pre_command.ps1 +0 -0
  83. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/tasks/templates/pre_command.sh +0 -0
  84. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/tasks/templates/task.toml +0 -0
  85. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/tasks/templates/test_ios.sh +0 -0
  86. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/tasks/templates/test_ios_nograder.sh +0 -0
  87. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/tasks/templates/test_macos.sh +0 -0
  88. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/tasks/templates/test_macos_check.sh +0 -0
  89. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/tasks/templates/test_macos_nograder.sh +0 -0
  90. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/tasks/templates/test_ubuntu.sh +0 -0
  91. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/tasks/templates/test_ubuntu_check.sh +0 -0
  92. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/tasks/templates/test_ubuntu_nograder.sh +0 -0
  93. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/tasks/templates/test_windows.sh +0 -0
  94. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/tasks/templates/test_windows_check.sh +0 -0
  95. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/tasks/templates/test_windows_nograder.sh +0 -0
  96. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/windows/__init__.py +0 -0
  97. {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/windows/shell.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: use-computer
3
- Version: 0.0.39
3
+ Version: 0.0.41
4
4
  Summary: Python SDK for use.computer sandboxes across macOS, iOS simulators, Windows, and Ubuntu
5
5
  Project-URL: Homepage, https://use.computer
6
6
  Project-URL: Documentation, https://api.use.computer/docs
@@ -24,6 +24,8 @@ Requires-Dist: google-genai>=1; extra == 'agents'
24
24
  Requires-Dist: litellm>=1; extra == 'agents'
25
25
  Requires-Dist: openai>=1; extra == 'agents'
26
26
  Requires-Dist: pillow>=10; extra == 'agents'
27
+ Requires-Dist: tinker-cookbook>=0.1.0; (python_full_version >= '3.11') and extra == 'agents'
28
+ Requires-Dist: tinker>=0.14.0; (python_full_version >= '3.11') and extra == 'agents'
27
29
  Provides-Extra: harbor
28
30
  Requires-Dist: harbor>=0.4.0; (python_version >= '3.12') and extra == 'harbor'
29
31
  Requires-Dist: pyyaml>=6; extra == 'harbor'
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "use-computer"
7
- version = "0.0.39"
7
+ version = "0.0.41"
8
8
  description = "Python SDK for use.computer sandboxes across macOS, iOS simulators, Windows, and Ubuntu"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -30,6 +30,8 @@ agents = [
30
30
  "anthropic>=0.86",
31
31
  "litellm>=1",
32
32
  "google-genai>=1",
33
+ "tinker>=0.14.0 ; python_full_version >= '3.11'",
34
+ "tinker-cookbook>=0.1.0 ; python_full_version >= '3.11'",
33
35
  ]
34
36
  harbor = [
35
37
  "harbor>=0.4.0; python_version >= '3.12'",
@@ -102,7 +102,7 @@ with tempfile.TemporaryDirectory() as tmp:
102
102
 
103
103
  for agent_cls, model_name in [
104
104
  (AnthropicComputerAgent, "anthropic/claude-sonnet-4-6"),
105
- (OpenAIComputerAgent, "openai/computer-use-preview"),
105
+ (OpenAIComputerAgent, "openai/gpt-5.5"),
106
106
  (GeminiComputerAgent, "gemini/gemini-2.5-computer-use-preview-10-2025"),
107
107
  (GenericComputerAgent, "openai/gpt-4o"),
108
108
  (IOSComputerAgent, "anthropic/claude-sonnet-4-6"),
@@ -140,6 +140,7 @@ async def execute_action(
140
140
  await sandbox.keyboard.type(command.text)
141
141
  elif command.action is DesktopAction.KEY:
142
142
  key = normalize_key(command.key or command.text, key_aliases)
143
+ action["key"] = key
143
144
  if "+" in key:
144
145
  await sandbox.keyboard.hotkey(key)
145
146
  else:
@@ -12,32 +12,102 @@ from use_computer.agents.types import Platform
12
12
  _KEY_ALIASES = {
13
13
  "Escape": "esc",
14
14
  "escape": "esc",
15
+ "ESCAPE": "esc",
16
+ "ESC": "esc",
15
17
  "Return": "enter",
16
18
  "return": "enter",
19
+ "RETURN": "enter",
20
+ "Enter": "enter",
21
+ "enter": "enter",
22
+ "ENTER": "enter",
23
+ "Command": "cmd",
24
+ "command": "cmd",
25
+ "COMMAND": "cmd",
26
+ "Cmd": "cmd",
27
+ "cmd": "cmd",
28
+ "CMD": "cmd",
17
29
  "super": "cmd",
30
+ "SUPER": "cmd",
18
31
  "meta": "cmd",
32
+ "META": "cmd",
19
33
  "Super_L": "cmd",
20
34
  "Meta_L": "cmd",
21
35
  "Control": "ctrl",
36
+ "control": "ctrl",
37
+ "CONTROL": "ctrl",
38
+ "Ctrl": "ctrl",
39
+ "ctrl": "ctrl",
40
+ "CTRL": "ctrl",
22
41
  "Shift": "shift",
42
+ "shift": "shift",
43
+ "SHIFT": "shift",
23
44
  "Alt": "alt",
45
+ "alt": "alt",
46
+ "ALT": "alt",
47
+ "Option": "alt",
48
+ "option": "alt",
49
+ "OPTION": "alt",
24
50
  "Backspace": "backspace",
51
+ "backspace": "backspace",
52
+ "BACKSPACE": "backspace",
25
53
  "Delete": "delete",
54
+ "delete": "delete",
55
+ "DELETE": "delete",
56
+ "Tab": "tab",
57
+ "tab": "tab",
58
+ "TAB": "tab",
59
+ "Space": "space",
60
+ "space": "space",
61
+ "SPACE": "space",
26
62
  "ArrowUp": "up",
63
+ "ARROWUP": "up",
64
+ "Up": "up",
65
+ "up": "up",
66
+ "UP": "up",
27
67
  "ArrowDown": "down",
68
+ "ARROWDOWN": "down",
69
+ "Down": "down",
70
+ "down": "down",
71
+ "DOWN": "down",
28
72
  "ArrowLeft": "left",
73
+ "ARROWLEFT": "left",
74
+ "Left": "left",
75
+ "left": "left",
76
+ "LEFT": "left",
29
77
  "ArrowRight": "right",
78
+ "ARROWRIGHT": "right",
79
+ "Right": "right",
80
+ "right": "right",
81
+ "RIGHT": "right",
30
82
  "Page_Up": "pageup",
83
+ "PAGE_UP": "pageup",
84
+ "PAGEUP": "pageup",
31
85
  "Page_Down": "pagedown",
86
+ "PAGE_DOWN": "pagedown",
87
+ "PAGEDOWN": "pagedown",
32
88
  }
33
89
 
34
90
  # Windows: the modifier is the Windows key, not Cmd.
35
91
  _WINDOWS_KEY_ALIASES = {
36
92
  **_KEY_ALIASES,
93
+ "Command": "win",
94
+ "command": "win",
95
+ "COMMAND": "win",
96
+ "Cmd": "win",
97
+ "cmd": "win",
98
+ "CMD": "win",
37
99
  "super": "win",
100
+ "SUPER": "win",
38
101
  "meta": "win",
102
+ "META": "win",
39
103
  "Super_L": "win",
40
104
  "Meta_L": "win",
105
+ "Win": "win",
106
+ "win": "win",
107
+ "WIN": "win",
108
+ "Windows": "win",
109
+ "windows": "win",
110
+ "WINDOWS": "win",
41
111
  }
42
112
 
43
113
 
@@ -11,6 +11,11 @@ from use_computer.sandbox import AsyncSandbox
11
11
 
12
12
  class RecordingMixin:
13
13
  async def start_recording(self: Any, sandbox: AsyncSandbox) -> None:
14
+ if getattr(getattr(sandbox, "type", None), "value", "") == "ios":
15
+ try:
16
+ await sandbox.screenshot.take_full_screen()
17
+ except Exception as e:
18
+ self.logger.warning(f"iOS recording warm-up screenshot failed: {e}")
14
19
  try:
15
20
  result = await sandbox.recording.start(name="trial")
16
21
  self._recording_id = result.id
@@ -22,6 +27,7 @@ class RecordingMixin:
22
27
  if not self._recording_id:
23
28
  return
24
29
  await asyncio.sleep(3.0)
30
+ rec_path = self.logs_dir / "recording.mp4"
25
31
  try:
26
32
  info = await sandbox.recording.stop(self._recording_id)
27
33
  self.logger.info(
@@ -33,16 +39,24 @@ class RecordingMixin:
33
39
 
34
40
  await asyncio.sleep(2)
35
41
 
36
- rec_path = self.logs_dir / "recording.mp4"
37
- self.logger.info(f"Downloading recording → {rec_path}")
38
- t_dl = time.monotonic()
39
- await sandbox.recording.download(self._recording_id, str(rec_path), timeout=180.0)
40
- dl_elapsed = time.monotonic() - t_dl
41
- local_size = rec_path.stat().st_size
42
- self.logger.info(
43
- f"Recording saved: {rec_path} "
44
- f"local_size={local_size}B ({local_size / (1024 * 1024):.2f} MB) "
45
- f"download_elapsed={dl_elapsed:.1f}s"
46
- )
42
+ await self._download_recording(sandbox, rec_path)
47
43
  except Exception as e:
48
44
  self.logger.warning(f"Recording save failed: {type(e).__name__}: {e}")
45
+ try:
46
+ await self._download_recording(sandbox, rec_path)
47
+ except Exception as fallback:
48
+ self.logger.warning(
49
+ f"Recording fallback download failed: {type(fallback).__name__}: {fallback}"
50
+ )
51
+
52
+ async def _download_recording(self: Any, sandbox: AsyncSandbox, rec_path) -> None:
53
+ self.logger.info(f"Downloading recording → {rec_path}")
54
+ t_dl = time.monotonic()
55
+ await sandbox.recording.download(self._recording_id, str(rec_path), timeout=180.0)
56
+ dl_elapsed = time.monotonic() - t_dl
57
+ local_size = rec_path.stat().st_size
58
+ self.logger.info(
59
+ f"Recording saved: {rec_path} "
60
+ f"local_size={local_size}B ({local_size / (1024 * 1024):.2f} MB) "
61
+ f"download_elapsed={dl_elapsed:.1f}s"
62
+ )
@@ -83,6 +83,11 @@ class GenericCUAAgent(BaseCUAAgent):
83
83
  coord_mode: str | None = None,
84
84
  reasoning_effort: str | None = None,
85
85
  reasoning_history: str | None = None,
86
+ renderer_name: str | None = None,
87
+ model_path: str | None = None,
88
+ temperature: float = 1.0,
89
+ top_p: float = 0.95,
90
+ top_k: int = -1,
86
91
  **kwargs: Any,
87
92
  ):
88
93
  super().__init__(logs_dir=logs_dir, model_name=model_name, **kwargs)
@@ -106,7 +111,12 @@ class GenericCUAAgent(BaseCUAAgent):
106
111
  ReasoningHistory(reasoning_history) if reasoning_history is not None else None
107
112
  ),
108
113
  ).as_dict()
109
- self._tinker_llm = None
114
+ self._tinker_backend = None
115
+ self._renderer_name = renderer_name
116
+ self._model_path = model_path
117
+ self._temperature = temperature
118
+ self._top_p = top_p
119
+ self._top_k = top_k
110
120
  # Coordinate convention emitted by the model:
111
121
  # "api_pixel" — Kimi default: 0..1 floats OR pixel ints in API space (1280x800)
112
122
  # "normalized_1000" — Qwen-VL default: 0..1000 ints as per-mille of screen
@@ -168,39 +178,19 @@ class GenericCUAAgent(BaseCUAAgent):
168
178
  async def _tinker_completion(
169
179
  self, model: str, messages: list[dict], max_tokens: int
170
180
  ) -> tuple[str, int, int]:
171
- if self._tinker_llm is None:
172
- from harbor.llms.tinker import TinkerLLM
181
+ if self._tinker_backend is None:
182
+ from use_computer.agents.providers.tinker_backend import TinkerBackend
173
183
 
174
- self._tinker_llm = TinkerLLM(
184
+ self._tinker_backend = TinkerBackend(
175
185
  model_name=model,
186
+ model_path=self._model_path,
187
+ renderer_name=self._renderer_name,
188
+ temperature=self._temperature,
189
+ top_p=self._top_p,
190
+ top_k=self._top_k,
176
191
  max_tokens=max_tokens,
177
192
  )
178
-
179
- # TinkerLLM.call() expects (prompt, message_history)
180
- # Split system + history from the final user message
181
- history = messages[:-1] if len(messages) > 1 else []
182
- prompt = ""
183
- last_msg = messages[-1] if messages else {"content": ""}
184
- content = last_msg.get("content")
185
- if isinstance(content, str):
186
- prompt = content
187
- elif isinstance(content, list):
188
- prompt = " ".join(
189
- p.get("text", "")
190
- for p in content
191
- if isinstance(p, dict) and p.get("type") == "text"
192
- )
193
-
194
- response = await self._tinker_llm.call(prompt=prompt, message_history=history)
195
- content = response.content or ""
196
- text = (
197
- content
198
- if isinstance(content, str)
199
- else "".join(p.get("text", "") if isinstance(p, dict) else str(p) for p in content)
200
- )
201
- prompt_tokens = response.usage.prompt_tokens if response.usage else 0
202
- completion_tokens = response.usage.completion_tokens if response.usage else 0
203
- return text, prompt_tokens, completion_tokens
193
+ return await self._tinker_backend.complete(messages, max_tokens)
204
194
 
205
195
  async def _run_agent(
206
196
  self,
@@ -19,6 +19,9 @@ from use_computer.agents.base import (
19
19
  )
20
20
  from use_computer.agents.types import DesktopAction, ScrollDirection
21
21
 
22
+ DEFAULT_OPENAI_COMPUTER_MODEL = "gpt-5.5"
23
+ LEGACY_OPENAI_COMPUTER_MODEL = "computer-use-preview"
24
+
22
25
 
23
26
  class OpenAIToolType(str, Enum):
24
27
  COMPUTER = "computer"
@@ -31,6 +34,7 @@ class OpenAIItemType(str, Enum):
31
34
  COMPUTER_SCREENSHOT = "computer_screenshot"
32
35
  INPUT_IMAGE = "input_image"
33
36
  MESSAGE = "message"
37
+ OUTPUT_TEXT = "output_text"
34
38
 
35
39
 
36
40
  @dataclass(frozen=True)
@@ -46,7 +50,6 @@ class OpenAIComputerObservation:
46
50
  "output": {
47
51
  "type": OpenAIItemType.COMPUTER_SCREENSHOT.value,
48
52
  "image_url": f"data:image/png;base64,{self.screenshot_b64}",
49
- "detail": "original",
50
53
  },
51
54
  }
52
55
  if self.pending_safety_checks:
@@ -69,7 +72,7 @@ class OpenAICUAAgent(BaseCUAAgent):
69
72
  self.steps[0]["message"] = instruction
70
73
 
71
74
  client = openai.OpenAI()
72
- model = self._parsed_model_name or "computer-use-preview"
75
+ model = self._parsed_model_name or DEFAULT_OPENAI_COMPUTER_MODEL
73
76
 
74
77
  system = build_system_prompt(
75
78
  load_prompt("openai.txt"),
@@ -109,21 +112,14 @@ class OpenAICUAAgent(BaseCUAAgent):
109
112
 
110
113
  for step_idx in range(self.max_steps):
111
114
  await self._fire_in_process(environment, step_idx)
112
- if previous_response_id is None:
113
- response = client.responses.create(
114
- model=model,
115
- tools=tools, # type: ignore
116
- input=input_items,
117
- truncation="auto",
118
- )
119
- else:
120
- response = client.responses.create(
121
- model=model,
122
- tools=tools, # type: ignore
123
- input=input_items,
115
+ response = client.responses.create(
116
+ **_response_create_kwargs(
117
+ model,
118
+ tools=tools,
119
+ input_items=input_items,
124
120
  previous_response_id=previous_response_id,
125
- truncation="auto",
126
121
  )
122
+ )
127
123
  previous_response_id = response.id
128
124
 
129
125
  self.total_in += response.usage.input_tokens if response.usage else 0
@@ -136,16 +132,11 @@ class OpenAICUAAgent(BaseCUAAgent):
136
132
  ]
137
133
 
138
134
  if not computer_calls:
139
- text_parts = [
140
- getattr(item, "text", "")
141
- for item in response.output
142
- if getattr(item, "type", None) == OpenAIItemType.MESSAGE.value
143
- ]
144
135
  self.steps.append(
145
136
  {
146
137
  "step_id": len(self.steps) + 1,
147
138
  "source": "agent",
148
- "message": "\n".join(text_parts) or "Task complete.",
139
+ "message": _response_text(response) or "Task complete.",
149
140
  }
150
141
  )
151
142
  break
@@ -222,7 +213,7 @@ def _openai_tools_for_model(
222
213
  height: int,
223
214
  environment: str,
224
215
  ) -> list[dict[str, Any]]:
225
- if model == "computer-use-preview":
216
+ if _uses_legacy_preview_tool(model):
226
217
  return [
227
218
  {
228
219
  "type": OpenAIToolType.COMPUTER_USE_PREVIEW.value,
@@ -234,6 +225,29 @@ def _openai_tools_for_model(
234
225
  return [{"type": OpenAIToolType.COMPUTER.value}]
235
226
 
236
227
 
228
+ def _response_create_kwargs(
229
+ model: str,
230
+ *,
231
+ tools: list[dict[str, Any]],
232
+ input_items: list[Any],
233
+ previous_response_id: str | None,
234
+ ) -> dict[str, Any]:
235
+ kwargs: dict[str, Any] = {
236
+ "model": model,
237
+ "tools": tools,
238
+ "input": input_items,
239
+ }
240
+ if previous_response_id is not None:
241
+ kwargs["previous_response_id"] = previous_response_id
242
+ if _uses_legacy_preview_tool(model):
243
+ kwargs["truncation"] = "auto"
244
+ return kwargs
245
+
246
+
247
+ def _uses_legacy_preview_tool(model: str) -> bool:
248
+ return model in {LEGACY_OPENAI_COMPUTER_MODEL, "computer-use-preview-2025-03-11"}
249
+
250
+
237
251
  def _openai_actions(call: Any) -> list[Any]:
238
252
  actions = getattr(call, "actions", None)
239
253
  if isinstance(actions, Iterable) and not isinstance(actions, (str, bytes)):
@@ -255,8 +269,31 @@ def _pending_safety_checks(call: Any) -> tuple[dict[str, Any], ...]:
255
269
  return tuple(normalized)
256
270
 
257
271
 
272
+ def _response_text(response: Any) -> str:
273
+ output_text = getattr(response, "output_text", "")
274
+ if isinstance(output_text, str) and output_text:
275
+ return output_text
276
+
277
+ text_parts = []
278
+ for item in getattr(response, "output", []):
279
+ if getattr(item, "type", None) != OpenAIItemType.MESSAGE.value:
280
+ continue
281
+ content = getattr(item, "content", None)
282
+ if isinstance(content, Iterable) and not isinstance(content, (str, bytes)):
283
+ for block in content:
284
+ if getattr(block, "type", None) == OpenAIItemType.OUTPUT_TEXT.value:
285
+ text = getattr(block, "text", "")
286
+ if text:
287
+ text_parts.append(text)
288
+ else:
289
+ text = getattr(item, "text", "")
290
+ if text:
291
+ text_parts.append(text)
292
+ return "\n".join(text_parts)
293
+
294
+
258
295
  def _map_openai_action(action: Any) -> dict[str, Any]:
259
- """Map OpenAI computer_use_preview action to our standard format."""
296
+ """Map OpenAI computer actions to our standard format."""
260
297
  action_type = action.type
261
298
 
262
299
  type_map = {
@@ -0,0 +1,163 @@
1
+ """Image-aware Tinker sampling backend for the generic CUA agent.
2
+
3
+ harbor's TinkerLLM forwards text only, so a vision agent loses its screenshots
4
+ on the tinker path. This backend builds the generation prompt through a
5
+ tinker-cookbook renderer with the model's image processor attached, so the
6
+ OpenAI-style `image_url` parts the generic agent emits reach the model.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import base64
13
+ import importlib
14
+ import io
15
+ from typing import Any, cast
16
+
17
+ from PIL import Image
18
+
19
+
20
+ class TinkerBackend:
21
+ """Vision-capable tinker sampler that consumes OpenAI chat messages."""
22
+
23
+ def __init__(
24
+ self,
25
+ *,
26
+ model_name: str,
27
+ model_path: str | None = None,
28
+ renderer_name: str | None = None,
29
+ temperature: float = 1.0,
30
+ top_p: float = 0.95,
31
+ top_k: int = -1,
32
+ max_tokens: int = 2048,
33
+ retry_times: int = 5,
34
+ retry_interval: float = 5.0,
35
+ ) -> None:
36
+ self.model_name = model_name
37
+ self.model_path = model_path
38
+ self.renderer_name = renderer_name
39
+ self.temperature = temperature
40
+ self.top_p = top_p
41
+ self.top_k = top_k
42
+ self.max_tokens = max_tokens
43
+ self.retry_times = retry_times
44
+ self.retry_interval = retry_interval
45
+ self._sampling_client = None
46
+ self._renderer = None
47
+
48
+ async def _ensure_ready(self) -> None:
49
+ if self._sampling_client is not None and self._renderer is not None:
50
+ return
51
+ try:
52
+ tinker = importlib.import_module("tinker")
53
+ get_image_processor = importlib.import_module(
54
+ "tinker_cookbook.image_processing_utils"
55
+ ).get_image_processor
56
+ get_model_attributes = importlib.import_module(
57
+ "tinker_cookbook.model_info"
58
+ ).get_model_attributes
59
+ get_renderer = importlib.import_module("tinker_cookbook.renderers").get_renderer
60
+ get_tokenizer = importlib.import_module("tinker_cookbook.tokenizer_utils").get_tokenizer
61
+ except ImportError as exc:
62
+ raise ImportError(
63
+ "TinkerBackend requires `tinker` and `tinker-cookbook`. "
64
+ "Install them with `uv pip install harbor[tinker]`."
65
+ ) from exc
66
+
67
+ service_client = tinker.ServiceClient()
68
+ if self.model_path:
69
+ self._sampling_client = await service_client.create_sampling_client_async(
70
+ model_path=self.model_path
71
+ )
72
+ else:
73
+ self._sampling_client = await service_client.create_sampling_client_async(
74
+ base_model=self.model_name
75
+ )
76
+
77
+ tokenizer = get_tokenizer(self.model_name)
78
+ attrs = get_model_attributes(self.model_name)
79
+ image_processor = get_image_processor(self.model_name) if attrs.is_vl else None
80
+ renderer_name = self.renderer_name or _default_renderer(self.model_name)
81
+ self._renderer = get_renderer(
82
+ renderer_name,
83
+ tokenizer,
84
+ image_processor=image_processor,
85
+ model_name=self.model_name,
86
+ )
87
+
88
+ async def complete(
89
+ self, messages: list[dict[str, Any]], max_tokens: int
90
+ ) -> tuple[str, int, int]:
91
+ """Sample one response; returns (text, prompt_tokens, completion_tokens)."""
92
+ await self._ensure_ready()
93
+ assert self._sampling_client is not None
94
+ assert self._renderer is not None
95
+
96
+ tinker = importlib.import_module("tinker")
97
+ get_text_content = importlib.import_module("tinker_cookbook.renderers").get_text_content
98
+
99
+ model_input = self._renderer.build_generation_prompt(
100
+ cast(Any, _to_tinker_messages(messages))
101
+ )
102
+ stop = self._renderer.get_stop_sequences()
103
+
104
+ last_error: Exception | None = None
105
+ for attempt in range(self.retry_times):
106
+ try:
107
+ response = await self._sampling_client.sample_async(
108
+ prompt=model_input,
109
+ num_samples=1,
110
+ sampling_params=tinker.SamplingParams(
111
+ temperature=self.temperature,
112
+ top_p=self.top_p,
113
+ top_k=self.top_k,
114
+ max_tokens=max_tokens,
115
+ stop=stop,
116
+ ),
117
+ )
118
+ sequence = response.sequences[0]
119
+ parsed_message, _termination = self._renderer.parse_response(sequence.tokens)
120
+ return get_text_content(parsed_message), model_input.length, len(sequence.tokens)
121
+ except Exception as exc:
122
+ last_error = exc
123
+ if attempt == self.retry_times - 1:
124
+ break
125
+ await asyncio.sleep(self.retry_interval)
126
+ assert last_error is not None
127
+ raise last_error
128
+
129
+
130
+ def _to_tinker_messages(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
131
+ """Convert OpenAI chat messages to tinker-cookbook messages (text + PIL images)."""
132
+ converted: list[dict[str, Any]] = []
133
+ for message in messages:
134
+ content = message.get("content")
135
+ if isinstance(content, str):
136
+ converted.append({"role": message["role"], "content": content})
137
+ continue
138
+
139
+ parts: list[dict[str, Any]] = []
140
+ for part in content or []:
141
+ if part.get("type") == "text":
142
+ parts.append({"type": "text", "text": part.get("text", "")})
143
+ elif part.get("type") == "image_url":
144
+ url = part.get("image_url", {}).get("url", "")
145
+ b64 = url.split(",", 1)[1] if "," in url else url
146
+ image = Image.open(io.BytesIO(base64.b64decode(b64))).convert("RGB")
147
+ parts.append({"type": "image", "image": image})
148
+ if len(parts) == 1 and parts[0]["type"] == "text":
149
+ converted.append({"role": message["role"], "content": parts[0]["text"]})
150
+ else:
151
+ converted.append({"role": message["role"], "content": parts})
152
+ return converted
153
+
154
+
155
+ def _default_renderer(model_name: str) -> str:
156
+ get_recommended_renderer_names = importlib.import_module(
157
+ "tinker_cookbook.model_info"
158
+ ).get_recommended_renderer_names
159
+
160
+ recommended = get_recommended_renderer_names(model_name)
161
+ if "qwen3_5_disable_thinking" in recommended:
162
+ return "qwen3_5_disable_thinking"
163
+ return recommended[0]