dostuff 0.2.2__tar.gz → 0.2.3__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 (129) hide show
  1. {dostuff-0.2.2 → dostuff-0.2.3}/PKG-INFO +4 -2
  2. {dostuff-0.2.2 → dostuff-0.2.3}/README.md +3 -1
  3. dostuff-0.2.3/dostuff/helpers/agent/get_model_token_limit.py +54 -0
  4. dostuff-0.2.3/dostuff/helpers/models/opencode.py +200 -0
  5. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/templates/config.example.yaml +1 -1
  6. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff.egg-info/PKG-INFO +4 -2
  7. {dostuff-0.2.2 → dostuff-0.2.3}/pyproject.toml +1 -1
  8. {dostuff-0.2.2 → dostuff-0.2.3}/tests/test_opencode.py +43 -0
  9. dostuff-0.2.2/dostuff/helpers/agent/get_model_token_limit.py +0 -29
  10. dostuff-0.2.2/dostuff/helpers/models/opencode.py +0 -76
  11. {dostuff-0.2.2 → dostuff-0.2.3}/LICENSE +0 -0
  12. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/__init__.py +0 -0
  13. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/__main__.py +0 -0
  14. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/agent/call_agent.py +0 -0
  15. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/agent/instructions.py +0 -0
  16. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/agent/loop.py +0 -0
  17. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/agent/permissions.py +0 -0
  18. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/agent/run_tool.py +0 -0
  19. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/cli.py +0 -0
  20. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/cli_tui.py +0 -0
  21. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/config.py +0 -0
  22. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/headless.py +0 -0
  23. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/agent/append_step.py +0 -0
  24. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/agent/constants.py +0 -0
  25. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/agent/consume_stream.py +0 -0
  26. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/agent/context_usage.py +0 -0
  27. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/agent/environment_context.py +0 -0
  28. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/agent/file_mentions.py +0 -0
  29. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/agent/load_identity.py +0 -0
  30. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/agent/load_project_instructions.py +0 -0
  31. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/agent/loop_check_cancelled.py +0 -0
  32. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/agent/loop_emit.py +0 -0
  33. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/agent/manage_context.py +0 -0
  34. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/agent/model_cost.py +0 -0
  35. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/agent/pastes.py +0 -0
  36. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/agent/quick_memory.py +0 -0
  37. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/agent/slash_suggestion.py +0 -0
  38. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/agent/tool_output.py +0 -0
  39. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/mcp/load_mcp_config.py +0 -0
  40. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/mcp/mcp_oauth.py +0 -0
  41. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/mcp/server_config.py +0 -0
  42. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/mcp/token_store.py +0 -0
  43. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/memory/export_session.py +0 -0
  44. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/memory/resolve_memory_operation.py +0 -0
  45. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/memory/shell_step.py +0 -0
  46. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/models/add_model_entry.py +0 -0
  47. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/models/list_models.py +0 -0
  48. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/skills/custom_commands.py +0 -0
  49. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/skills/discover_skills.py +0 -0
  50. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/skills/skill_command.py +0 -0
  51. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/tools/diff.py +0 -0
  52. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/tools/generate_tool_schema.py +0 -0
  53. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/tools/project_root.py +0 -0
  54. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/tools/resolve_safe_path.py +0 -0
  55. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/ui/__init__.py +0 -0
  56. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/ui/doctor.py +0 -0
  57. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/ui/emit.py +0 -0
  58. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/ui/prompt_history.py +0 -0
  59. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/helpers/ui/terminal_signals.py +0 -0
  60. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/lib/checkpoints.py +0 -0
  61. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/lib/exceptions.py +0 -0
  62. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/lib/mcp/mcp_client.py +0 -0
  63. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/lib/mcp/mcp_client_registration_store.py +0 -0
  64. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/lib/mcp/mcp_tool_registry_store.py +0 -0
  65. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/lib/memory/chroma.py +0 -0
  66. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/lib/memory/episodic_memory_store.py +0 -0
  67. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/lib/memory/semantic_memory_store.py +0 -0
  68. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/lib/memory/session_store.py +0 -0
  69. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/lib/memory/stores.py +0 -0
  70. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/lib/memory/types.py +0 -0
  71. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/lib/model.py +0 -0
  72. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/lib/tracing.py +0 -0
  73. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/lib/worktree.py +0 -0
  74. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/memory/__init__.py +0 -0
  75. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/setup_wizard.py +0 -0
  76. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/skills.py +0 -0
  77. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/templates/env.example +0 -0
  78. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/tools/__init__.py +0 -0
  79. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/tools/bash/bash_command.py +0 -0
  80. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/tools/definitions.py +0 -0
  81. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/tools/delegate_to_subagent.py +0 -0
  82. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/tools/files/delete_file.py +0 -0
  83. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/tools/files/edit_file.py +0 -0
  84. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/tools/files/list_files.py +0 -0
  85. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/tools/files/read_file.py +0 -0
  86. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/tools/files/write_file.py +0 -0
  87. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/tools/get_current_datetime.py +0 -0
  88. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/tools/mcp/call_mcp_tool.py +0 -0
  89. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/tools/mcp/get_mcp_tool_details.py +0 -0
  90. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/tools/mcp/search_mcp_tools.py +0 -0
  91. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/tools/memory/save_episode.py +0 -0
  92. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/tools/memory/save_memory.py +0 -0
  93. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/tools/search/glob.py +0 -0
  94. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/tools/search/grep.py +0 -0
  95. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/tools/search/walk.py +0 -0
  96. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/tools/settings.py +0 -0
  97. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/tools/todo_write.py +0 -0
  98. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/tools/web_fetch.py +0 -0
  99. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff/tools/web_search.py +0 -0
  100. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff.egg-info/SOURCES.txt +0 -0
  101. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff.egg-info/dependency_links.txt +0 -0
  102. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff.egg-info/entry_points.txt +0 -0
  103. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff.egg-info/requires.txt +0 -0
  104. {dostuff-0.2.2 → dostuff-0.2.3}/dostuff.egg-info/top_level.txt +0 -0
  105. {dostuff-0.2.2 → dostuff-0.2.3}/setup.cfg +0 -0
  106. {dostuff-0.2.2 → dostuff-0.2.3}/tests/test_checkpoints_todos_caching.py +0 -0
  107. {dostuff-0.2.2 → dostuff-0.2.3}/tests/test_cli_commands.py +0 -0
  108. {dostuff-0.2.2 → dostuff-0.2.3}/tests/test_config_memory_mcp.py +0 -0
  109. {dostuff-0.2.2 → dostuff-0.2.3}/tests/test_file_mentions.py +0 -0
  110. {dostuff-0.2.2 → dostuff-0.2.3}/tests/test_file_tools.py +0 -0
  111. {dostuff-0.2.2 → dostuff-0.2.3}/tests/test_loop.py +0 -0
  112. {dostuff-0.2.2 → dostuff-0.2.3}/tests/test_mcp_e2e.py +0 -0
  113. {dostuff-0.2.2 → dostuff-0.2.3}/tests/test_mcp_oauth.py +0 -0
  114. {dostuff-0.2.2 → dostuff-0.2.3}/tests/test_optional_memory_and_instructions.py +0 -0
  115. {dostuff-0.2.2 → dostuff-0.2.3}/tests/test_pastes.py +0 -0
  116. {dostuff-0.2.2 → dostuff-0.2.3}/tests/test_permissions.py +0 -0
  117. {dostuff-0.2.2 → dostuff-0.2.3}/tests/test_roadmap_021.py +0 -0
  118. {dostuff-0.2.2 → dostuff-0.2.3}/tests/test_session_store.py +0 -0
  119. {dostuff-0.2.2 → dostuff-0.2.3}/tests/test_settings_tools.py +0 -0
  120. {dostuff-0.2.2 → dostuff-0.2.3}/tests/test_setup_wizard.py +0 -0
  121. {dostuff-0.2.2 → dostuff-0.2.3}/tests/test_skill_command.py +0 -0
  122. {dostuff-0.2.2 → dostuff-0.2.3}/tests/test_slash_suggestion.py +0 -0
  123. {dostuff-0.2.2 → dostuff-0.2.3}/tests/test_subagent.py +0 -0
  124. {dostuff-0.2.2 → dostuff-0.2.3}/tests/test_terminal_signals.py +0 -0
  125. {dostuff-0.2.2 → dostuff-0.2.3}/tests/test_tool_schemas.py +0 -0
  126. {dostuff-0.2.2 → dostuff-0.2.3}/tests/test_tui.py +0 -0
  127. {dostuff-0.2.2 → dostuff-0.2.3}/tests/test_web_fetch.py +0 -0
  128. {dostuff-0.2.2 → dostuff-0.2.3}/tests/test_web_search.py +0 -0
  129. {dostuff-0.2.2 → dostuff-0.2.3}/tests/test_worktree.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dostuff
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: DoStuff - a terminal coding agent for any LLM: TUI and headless mode, permissions, MCP, skills, web tools, sessions and git worktrees
5
5
  Author-email: Varun Kumawat <varunkumawatleap2@gmail.com>
6
6
  License-Expression: MIT
@@ -352,7 +352,7 @@ models:
352
352
  loop:
353
353
  max_iterations: 100
354
354
  subagent_max_iterations: 25
355
- # context_window: 128000 # override when litellm doesn't know the model
355
+ # context_window: 128000 # override when dostuff/litellm don't know the model
356
356
  max_tool_output_chars: 30000 # longer tool results keep head + tail
357
357
  max_retries: 6 # retries on rate limits, timeouts, 5xx (0 = none)
358
358
  retry_max_delay: 60 # cap on each wait, seconds; Retry-After is honored
@@ -416,6 +416,8 @@ or `/model` inside the TUI. Model lists come from litellm's catalog (chat models
416
416
 
417
417
  OpenCode models (`opencode/<id>` for Zen, `opencode-go/<id>` for a Go subscription) use the ids from the [OpenCode docs](https://opencode.ai/docs/zen/). litellm has no OpenCode provider, so DoStuff sends each model to the gateway endpoint for its family: Claude models (and a few Qwen / MiniMax models) to the Anthropic Messages endpoint, GPT / Grok / Muse models to the Responses endpoint, Gemini models to the Gemini endpoint, and all other models to chat completions. One console key in `OPENCODE_API_KEY` works for Zen and Go. OpenCode only allows its free models from its own app, so use paid models or Go.
418
418
 
419
+ The gateways don't publish a model's context window in `/v1/models`, so `helpers/models/opencode.py` carries one per model, taken from [models.dev](https://models.dev)'s `opencode` and `opencode-go` entries (the same id can differ per gateway: `opencode/minimax-m3` is 512k, `opencode-go/minimax-m3` is 1M). `/context`, the status bar and auto-compaction size themselves from it; set `loop.context_window` to override.
420
+
419
421
  ---
420
422
 
421
423
  ## MCP servers
@@ -304,7 +304,7 @@ models:
304
304
  loop:
305
305
  max_iterations: 100
306
306
  subagent_max_iterations: 25
307
- # context_window: 128000 # override when litellm doesn't know the model
307
+ # context_window: 128000 # override when dostuff/litellm don't know the model
308
308
  max_tool_output_chars: 30000 # longer tool results keep head + tail
309
309
  max_retries: 6 # retries on rate limits, timeouts, 5xx (0 = none)
310
310
  retry_max_delay: 60 # cap on each wait, seconds; Retry-After is honored
@@ -368,6 +368,8 @@ or `/model` inside the TUI. Model lists come from litellm's catalog (chat models
368
368
 
369
369
  OpenCode models (`opencode/<id>` for Zen, `opencode-go/<id>` for a Go subscription) use the ids from the [OpenCode docs](https://opencode.ai/docs/zen/). litellm has no OpenCode provider, so DoStuff sends each model to the gateway endpoint for its family: Claude models (and a few Qwen / MiniMax models) to the Anthropic Messages endpoint, GPT / Grok / Muse models to the Responses endpoint, Gemini models to the Gemini endpoint, and all other models to chat completions. One console key in `OPENCODE_API_KEY` works for Zen and Go. OpenCode only allows its free models from its own app, so use paid models or Go.
370
370
 
371
+ The gateways don't publish a model's context window in `/v1/models`, so `helpers/models/opencode.py` carries one per model, taken from [models.dev](https://models.dev)'s `opencode` and `opencode-go` entries (the same id can differ per gateway: `opencode/minimax-m3` is 512k, `opencode-go/minimax-m3` is 1M). `/context`, the status bar and auto-compaction size themselves from it; set `loop.context_window` to override.
372
+
371
373
  ---
372
374
 
373
375
  ## MCP servers
@@ -0,0 +1,54 @@
1
+ import litellm
2
+ from dostuff.config import get_config
3
+ from dostuff.helpers.models.opencode import context_window, is_opencode
4
+ from dostuff.lib.model import get_litellm_model, get_model
5
+
6
+ FALLBACK_CONTEXT_WINDOW = 128_000
7
+ _warned_models: set[str] = set()
8
+
9
+
10
+ def _litellm_limit(model: str) -> int | None:
11
+ """litellm's `max_input_tokens` for `model`, or None when it doesn't map it."""
12
+ try:
13
+ limit = litellm.get_model_info(model=model).get("max_input_tokens")
14
+ except Exception:
15
+ return None
16
+ return int(limit) if limit else None
17
+
18
+
19
+ def get_model_token_limit() -> int:
20
+ """Context window for the active model: `loop.context_window`, else the OpenCode table, else
21
+ litellm's model info, else 128k.
22
+
23
+ OpenCode ids are checked against `helpers/models/opencode._CONTEXT_WINDOWS` first: litellm only
24
+ knows their upstream `openai/` / `anthropic/` route for a few of them, and the same id can have a
25
+ different window on each gateway.
26
+ """
27
+ configured = get_config().context_window
28
+ if configured > 0:
29
+ return configured
30
+ opencode = is_opencode(get_config().provider)
31
+ if opencode:
32
+ model_id = get_model().split("/", 1)[-1]
33
+ known = context_window(get_config().provider, model_id)
34
+ if known:
35
+ return known
36
+ model = get_litellm_model()
37
+ limit = _litellm_limit(model)
38
+ if limit:
39
+ return limit
40
+ if opencode:
41
+ # litellm keys some ids only without the route prefix, e.g. `deepseek-v4-pro` (1M) exists
42
+ # while `openai/deepseek-v4-pro` doesn't.
43
+ limit = _litellm_limit(get_model().split("/", 1)[-1])
44
+ if limit:
45
+ return limit
46
+ if model not in _warned_models:
47
+ _warned_models.add(model)
48
+ from dostuff.helpers.ui.emit import emit
49
+ emit(
50
+ f"Unknown context window for '{model}'; assuming {FALLBACK_CONTEXT_WINDOW:,} tokens. "
51
+ "Set loop.context_window in config.yaml to override.",
52
+ msg_type="system",
53
+ )
54
+ return FALLBACK_CONTEXT_WINDOW
@@ -0,0 +1,200 @@
1
+ """OpenCode Zen (`opencode/<id>`) and OpenCode Go (`opencode-go/<id>`) models.
2
+
3
+ litellm has no OpenCode provider. Each gateway serves a model family on the endpoint of its
4
+ upstream API (https://opencode.ai/docs/zen/), so a model is sent through litellm's openai,
5
+ anthropic or gemini route with the gateway as api_base.
6
+ """
7
+ import os
8
+ from typing import Any, Literal
9
+
10
+ OPENCODE_PROVIDERS = {
11
+ "opencode": "https://opencode.ai/inference",
12
+ "opencode-go": "https://opencode.ai/inference/go",
13
+ }
14
+ OPENCODE_API_KEY_ENV = "OPENCODE_API_KEY" # one console key works for Zen and Go
15
+ # Served on the Anthropic Messages endpoint besides Claude; they differ between the gateways
16
+ _ANTHROPIC_IDS = {
17
+ "opencode": {"qwen3.8-flash", "qwen3.6-plus", "qwen3.5-plus"},
18
+ "opencode-go": {"qwen3.8-max", "qwen3.8-flash", "qwen3.7-plus", "minimax-m3"},
19
+ }
20
+ # Input-token limit per model, from models.dev's `opencode` / `opencode-go` entries
21
+ # (https://models.dev — the catalog OpenCode's own clients size their context with). The gateways
22
+ # don't publish limits in /v1/models, litellm doesn't map their ids, and the same model can have a
23
+ # different window on each gateway, so this table is the only reliable source. Refresh it when
24
+ # `provider_models()` lists an id that is missing here (those fall back to litellm, then 128k).
25
+ _CONTEXT_WINDOWS = {
26
+ "opencode": {
27
+ "beam-free": 1_048_576,
28
+ "big-pickle": 160_000,
29
+ "claude-fable-5": 1_000_000,
30
+ "claude-fable-5-1": 1_000_000,
31
+ "claude-haiku-4-5": 200_000,
32
+ "claude-haiku-5-5": 1_000_000,
33
+ "claude-opus-4-5": 200_000,
34
+ "claude-opus-4-6": 1_000_000,
35
+ "claude-opus-4-7": 1_000_000,
36
+ "claude-opus-4-8": 1_000_000,
37
+ "claude-opus-5": 1_000_000,
38
+ "claude-opus-5-5": 1_000_000,
39
+ "claude-sonnet-4-5": 1_000_000,
40
+ "claude-sonnet-4-6": 1_000_000,
41
+ "claude-sonnet-5": 1_000_000,
42
+ "claude-sonnet-5-5": 1_000_000,
43
+ "deepseek-v4-pro": 1_000_000,
44
+ "deepseek-v4.1-flash": 1_000_000,
45
+ "deepseek-v4.1-flash-free": 200_000,
46
+ "flint-40b-free": 262_144,
47
+ "gemini-3-flash": 1_048_576,
48
+ "gemini-3.1-pro": 1_048_576,
49
+ "gemini-3.5-flash": 1_048_576,
50
+ "gemini-3.5-flash-lite": 1_048_576,
51
+ "gemini-3.6-flash": 1_048_576,
52
+ "gemini-3.7-flash": 1_048_576,
53
+ "gemini-3.8-flash": 1_048_576,
54
+ "gemini-4-argon": 1_048_576,
55
+ "glm-5": 204_800,
56
+ "glm-5.1": 204_800,
57
+ "glm-5.2": 1_000_000,
58
+ "glm-5.3": 1_000_000,
59
+ "glm-5.3-flash": 1_000_000,
60
+ "glm-5.3-flash-free": 200_000,
61
+ "gpt-5": 272_000,
62
+ "gpt-5-codex": 272_000,
63
+ "gpt-5-nano": 272_000,
64
+ "gpt-5.1": 272_000,
65
+ "gpt-5.1-codex": 272_000,
66
+ "gpt-5.1-codex-max": 272_000,
67
+ "gpt-5.1-codex-mini": 272_000,
68
+ "gpt-5.2": 272_000,
69
+ "gpt-5.2-codex": 272_000,
70
+ "gpt-5.3-codex": 272_000,
71
+ "gpt-5.3-codex-spark": 128_000,
72
+ "gpt-5.4": 922_000,
73
+ "gpt-5.4-mini": 272_000,
74
+ "gpt-5.4-nano": 272_000,
75
+ "gpt-5.4-pro": 922_000,
76
+ "gpt-5.5": 922_000,
77
+ "gpt-5.5-pro": 922_000,
78
+ "gpt-5.6-luna": 922_000,
79
+ "gpt-5.6-sol": 922_000,
80
+ "gpt-5.6-terra": 922_000,
81
+ "gpt-6-astra": 922_000,
82
+ "gpt-6-luna": 922_000,
83
+ "gpt-6-sol": 922_000,
84
+ "gpt-6.1-sol": 922_000,
85
+ "grok-4.5": 500_000,
86
+ "grok-4.6": 500_000,
87
+ "grok-4.7": 500_000,
88
+ "grok-build-0.1": 256_000,
89
+ "kimi-k2.5": 262_144,
90
+ "kimi-k2.6": 262_144,
91
+ "kimi-k2.7-code": 262_144,
92
+ "kimi-k3": 1_048_576,
93
+ "mimo-v2.6-flash-free": 200_000,
94
+ "minimax-m2.5": 204_800,
95
+ "minimax-m2.7": 204_800,
96
+ "minimax-m3": 512_000,
97
+ "mistral-large-4": 524_288,
98
+ "muse-spark-1.2": 1_048_576,
99
+ "muse-spark-1.3": 1_048_576,
100
+ "muse-wonder-1.0": 1_048_576,
101
+ "nemotron-3.5-lightning-free": 262_144,
102
+ "nemotron-3.5-super-vl-free": 1_048_576,
103
+ "qwen3.5-plus": 262_144,
104
+ "qwen3.6-plus": 262_144,
105
+ "qwen3.8-flash": 1_000_000,
106
+ "qwen3.8-max": 262_144,
107
+ },
108
+ "opencode-go": {
109
+ "beam-free": 1_048_576,
110
+ "claude-haiku-5-5": 1_000_000,
111
+ "deepseek-v4-pro": 1_000_000,
112
+ "deepseek-v4.1-flash": 1_000_000,
113
+ "gemini-4-argon": 1_048_576,
114
+ "glm-5.2": 1_000_000,
115
+ "glm-5.3": 1_000_000,
116
+ "glm-5.3-flash": 1_000_000,
117
+ "gpt-5.6-luna": 922_000,
118
+ "gpt-6-luna": 922_000,
119
+ "grok-4.7": 500_000,
120
+ "hy3": 192_000,
121
+ "hy4-preview": 1_024_000,
122
+ "iceman-contributor-free": 262_144,
123
+ "kimi-k2.7-code": 262_144,
124
+ "kimi-k3": 1_048_576,
125
+ "kimi-k3.1-pro": 1_048_576,
126
+ "longcat-2.0": 1_000_000,
127
+ "mimo-v2.6-flash": 1_048_576,
128
+ "mimo-v2.6-pro": 1_048_576,
129
+ "minimax-m3": 1_000_000,
130
+ "minimax-m3.1-flash": 1_000_000,
131
+ "muse-spark-1.2-contributor": 1_048_576,
132
+ "muse-spark-1.3-contributor": 1_048_576,
133
+ "qwen3.7-plus": 1_000_000,
134
+ "qwen3.8-flash": 1_000_000,
135
+ "qwen3.8-max": 1_000_000,
136
+ "step-5-preview": 1_000_000,
137
+ },
138
+ }
139
+ # In /v1/models but not chat models
140
+ _NOT_CHAT = ("jev-", "test")
141
+
142
+ Endpoint = Literal["chat", "responses", "anthropic", "google"]
143
+
144
+
145
+ def is_opencode(provider: str) -> bool:
146
+ return provider in OPENCODE_PROVIDERS
147
+
148
+
149
+ def context_window(provider: str, model_id: str) -> int | None:
150
+ """Input-token limit for an OpenCode model id, or None if the table doesn't list it."""
151
+ return _CONTEXT_WINDOWS.get(provider, {}).get(model_id)
152
+
153
+
154
+ def endpoint_for(provider: str, model_id: str) -> Endpoint:
155
+ """Which upstream API the gateway serves `model_id` on."""
156
+ if model_id.startswith("claude-") or model_id in _ANTHROPIC_IDS[provider]:
157
+ return "anthropic"
158
+ if model_id.startswith(("gpt-", "grok-", "muse-")):
159
+ return "responses"
160
+ if model_id.startswith("gemini-"):
161
+ return "google"
162
+ return "chat"
163
+
164
+
165
+ def litellm_route(provider: str, model_id: str) -> tuple[str, str]:
166
+ """(litellm model, api_base) for an OpenCode model id."""
167
+ base = OPENCODE_PROVIDERS[provider]
168
+ endpoint = endpoint_for(provider, model_id)
169
+ if endpoint == "anthropic":
170
+ return f"anthropic/{model_id}", f"{base}/anthropic" # litellm appends /v1/messages
171
+ if endpoint == "google":
172
+ return f"gemini/{model_id}", f"{base}/google/v1beta"
173
+ if endpoint == "responses":
174
+ return f"openai/responses/{model_id}", f"{base}/openai/v1"
175
+ return f"openai/{model_id}", f"{base}/openai/v1"
176
+
177
+
178
+ def opencode_kwargs(provider: str, model_id: str, api_key: str | None, session_id: str, version: str) -> dict[str, Any]:
179
+ """litellm kwargs for an OpenCode model. Go asks clients to name themselves and send a
180
+ stable per-conversation id; Zen gets the same headers."""
181
+ model, api_base = litellm_route(provider, model_id)
182
+ return {
183
+ "model": model,
184
+ "api_base": api_base,
185
+ # Never fall back to OPENAI_API_KEY / ANTHROPIC_API_KEY: those would go to opencode.ai
186
+ "api_key": api_key or os.environ.get(OPENCODE_API_KEY_ENV) or "missing-opencode-key",
187
+ "extra_headers": {"User-Agent": f"dostuff/{version}", "x-opencode-session": session_id},
188
+ }
189
+
190
+
191
+ def models_url(provider: str) -> str:
192
+ return f"{OPENCODE_PROVIDERS[provider]}/v1/models"
193
+
194
+
195
+ def chat_model_ids(data: dict) -> list[str]:
196
+ """Model ids from a /v1/models response, without the non-chat ones."""
197
+ return [
198
+ m["id"] for m in data.get("data") or []
199
+ if isinstance(m, dict) and m.get("id") and not str(m["id"]).startswith(_NOT_CHAT)
200
+ ]
@@ -17,7 +17,7 @@ loop:
17
17
  max_iterations: 100 # max agent turns before forced exit; ESC cancels anytime
18
18
  subagent_max_iterations: 25 # iteration cap for delegate_to_subagent
19
19
  prompt_caching: true # Claude models: cache the system prompt and history between calls (cheaper, faster)
20
- # context_window: 128000 # override the model's context window (tokens) if litellm doesn't know it
20
+ # context_window: 128000 # override the model's context window (tokens) if dostuff/litellm can't tell
21
21
  max_tool_output_chars: 30000 # longer tool results keep head + tail with a truncation marker
22
22
  max_retries: 6 # retries on rate limits, timeouts and 5xx (0 = fail at once)
23
23
  retry_max_delay: 60 # longest wait between retries, seconds (backoff doubles from 1s; Retry-After is honored)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dostuff
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: DoStuff - a terminal coding agent for any LLM: TUI and headless mode, permissions, MCP, skills, web tools, sessions and git worktrees
5
5
  Author-email: Varun Kumawat <varunkumawatleap2@gmail.com>
6
6
  License-Expression: MIT
@@ -352,7 +352,7 @@ models:
352
352
  loop:
353
353
  max_iterations: 100
354
354
  subagent_max_iterations: 25
355
- # context_window: 128000 # override when litellm doesn't know the model
355
+ # context_window: 128000 # override when dostuff/litellm don't know the model
356
356
  max_tool_output_chars: 30000 # longer tool results keep head + tail
357
357
  max_retries: 6 # retries on rate limits, timeouts, 5xx (0 = none)
358
358
  retry_max_delay: 60 # cap on each wait, seconds; Retry-After is honored
@@ -416,6 +416,8 @@ or `/model` inside the TUI. Model lists come from litellm's catalog (chat models
416
416
 
417
417
  OpenCode models (`opencode/<id>` for Zen, `opencode-go/<id>` for a Go subscription) use the ids from the [OpenCode docs](https://opencode.ai/docs/zen/). litellm has no OpenCode provider, so DoStuff sends each model to the gateway endpoint for its family: Claude models (and a few Qwen / MiniMax models) to the Anthropic Messages endpoint, GPT / Grok / Muse models to the Responses endpoint, Gemini models to the Gemini endpoint, and all other models to chat completions. One console key in `OPENCODE_API_KEY` works for Zen and Go. OpenCode only allows its free models from its own app, so use paid models or Go.
418
418
 
419
+ The gateways don't publish a model's context window in `/v1/models`, so `helpers/models/opencode.py` carries one per model, taken from [models.dev](https://models.dev)'s `opencode` and `opencode-go` entries (the same id can differ per gateway: `opencode/minimax-m3` is 512k, `opencode-go/minimax-m3` is 1M). `/context`, the status bar and auto-compaction size themselves from it; set `loop.context_window` to override.
420
+
419
421
  ---
420
422
 
421
423
  ## MCP servers
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dostuff"
7
- version = "0.2.2"
7
+ version = "0.2.3"
8
8
  description = "DoStuff - a terminal coding agent for any LLM: TUI and headless mode, permissions, MCP, skills, web tools, sessions and git worktrees"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,6 +1,7 @@
1
1
  """OpenCode Zen / Go models: routing through litellm's openai / anthropic / gemini providers."""
2
2
  import pytest
3
3
 
4
+ import dostuff.helpers.agent.get_model_token_limit as token_limit_module
4
5
  from dostuff import config as config_module
5
6
  from dostuff.config import reset_config
6
7
  from dostuff.helpers.models import list_models
@@ -79,3 +80,45 @@ def test_model_list(monkeypatch):
79
80
  assert chat_model_ids(data) == ["kimi-k3", "claude-haiku-5-5"]
80
81
  monkeypatch.setattr(list_models, "_get_json", lambda url, what: data if url.endswith("/go/v1/models") else {})
81
82
  assert list_models.provider_models("opencode-go") == ["opencode-go/claude-haiku-5-5", "opencode-go/kimi-k3"]
83
+
84
+
85
+ def test_context_window_table(model):
86
+ """OpenCode gateways publish no context limit, so the table in helpers/models/opencode.py is used."""
87
+ t = token_limit_module
88
+ for name, window in [
89
+ ("opencode-go/deepseek-v4.1-flash", 1_000_000), # the model that used to be assumed 128k
90
+ ("opencode/deepseek-v4.1-flash", 1_000_000),
91
+ ("opencode/minimax-m3", 512_000), # same id, different window per gateway
92
+ ("opencode-go/minimax-m3", 1_000_000),
93
+ ("opencode/qwen3.8-max", 262_144),
94
+ ("opencode-go/qwen3.8-max", 1_000_000),
95
+ ]:
96
+ model(name)
97
+ assert t.get_model_token_limit() == window, name
98
+
99
+
100
+ def test_context_window_bare_litellm_fallback(model, monkeypatch):
101
+ """An id missing from the table still resolves through litellm without the route prefix."""
102
+ t = token_limit_module
103
+ monkeypatch.setattr(t, "context_window", lambda provider, model_id: None)
104
+ model("opencode-go/deepseek-v4-pro")
105
+ assert t._litellm_limit("openai/deepseek-v4-pro") is None # the routed name is unknown to litellm
106
+ assert t._litellm_limit("deepseek-v4-pro") == 1_000_000 # the bare id is not
107
+ assert t.get_model_token_limit() == 1_000_000
108
+
109
+
110
+ def test_context_window_configured_wins(model, monkeypatch):
111
+ model("opencode-go/deepseek-v4.1-flash")
112
+ monkeypatch.setattr(token_limit_module.get_config(), "context_window", 32_000)
113
+ assert token_limit_module.get_model_token_limit() == 32_000
114
+
115
+
116
+ def test_context_window_unknown_warns_once(model, monkeypatch):
117
+ t = token_limit_module
118
+ emitted = []
119
+ monkeypatch.setattr("dostuff.helpers.ui.emit.emit", lambda message, msg_type="system": emitted.append(message))
120
+ monkeypatch.setattr(t, "_warned_models", set())
121
+ model("opencode-go/not-a-real-model")
122
+ assert t.get_model_token_limit() == t.FALLBACK_CONTEXT_WINDOW
123
+ assert t.get_model_token_limit() == t.FALLBACK_CONTEXT_WINDOW
124
+ assert len(emitted) == 1 and "128,000" in emitted[0]
@@ -1,29 +0,0 @@
1
- import litellm
2
- from dostuff.config import get_config
3
- from dostuff.lib.model import get_litellm_model
4
-
5
- FALLBACK_CONTEXT_WINDOW = 128_000
6
- _warned_models: set[str] = set()
7
-
8
-
9
- def get_model_token_limit() -> int:
10
- """Context window for the active model: `loop.context_window`, else litellm's model info, else 128k."""
11
- configured = get_config().context_window
12
- if configured > 0:
13
- return configured
14
- model = get_litellm_model()
15
- try:
16
- limit = litellm.get_model_info(model=model).get("max_input_tokens")
17
- if limit:
18
- return int(limit)
19
- except Exception:
20
- pass
21
- if model not in _warned_models:
22
- _warned_models.add(model)
23
- from dostuff.helpers.ui.emit import emit
24
- emit(
25
- f"Unknown context window for '{model}'; assuming {FALLBACK_CONTEXT_WINDOW:,} tokens. "
26
- "Set loop.context_window in config.yaml to override.",
27
- msg_type="system",
28
- )
29
- return FALLBACK_CONTEXT_WINDOW
@@ -1,76 +0,0 @@
1
- """OpenCode Zen (`opencode/<id>`) and OpenCode Go (`opencode-go/<id>`) models.
2
-
3
- litellm has no OpenCode provider. Each gateway serves a model family on the endpoint of its
4
- upstream API (https://opencode.ai/docs/zen/), so a model is sent through litellm's openai,
5
- anthropic or gemini route with the gateway as api_base.
6
- """
7
- import os
8
- from typing import Any, Literal
9
-
10
- OPENCODE_PROVIDERS = {
11
- "opencode": "https://opencode.ai/inference",
12
- "opencode-go": "https://opencode.ai/inference/go",
13
- }
14
- OPENCODE_API_KEY_ENV = "OPENCODE_API_KEY" # one console key works for Zen and Go
15
- # Served on the Anthropic Messages endpoint besides Claude; they differ between the gateways
16
- _ANTHROPIC_IDS = {
17
- "opencode": {"qwen3.8-flash", "qwen3.6-plus", "qwen3.5-plus"},
18
- "opencode-go": {"qwen3.8-max", "qwen3.8-flash", "qwen3.7-plus", "minimax-m3"},
19
- }
20
- # In /v1/models but not chat models
21
- _NOT_CHAT = ("jev-", "test")
22
-
23
- Endpoint = Literal["chat", "responses", "anthropic", "google"]
24
-
25
-
26
- def is_opencode(provider: str) -> bool:
27
- return provider in OPENCODE_PROVIDERS
28
-
29
-
30
- def endpoint_for(provider: str, model_id: str) -> Endpoint:
31
- """Which upstream API the gateway serves `model_id` on."""
32
- if model_id.startswith("claude-") or model_id in _ANTHROPIC_IDS[provider]:
33
- return "anthropic"
34
- if model_id.startswith(("gpt-", "grok-", "muse-")):
35
- return "responses"
36
- if model_id.startswith("gemini-"):
37
- return "google"
38
- return "chat"
39
-
40
-
41
- def litellm_route(provider: str, model_id: str) -> tuple[str, str]:
42
- """(litellm model, api_base) for an OpenCode model id."""
43
- base = OPENCODE_PROVIDERS[provider]
44
- endpoint = endpoint_for(provider, model_id)
45
- if endpoint == "anthropic":
46
- return f"anthropic/{model_id}", f"{base}/anthropic" # litellm appends /v1/messages
47
- if endpoint == "google":
48
- return f"gemini/{model_id}", f"{base}/google/v1beta"
49
- if endpoint == "responses":
50
- return f"openai/responses/{model_id}", f"{base}/openai/v1"
51
- return f"openai/{model_id}", f"{base}/openai/v1"
52
-
53
-
54
- def opencode_kwargs(provider: str, model_id: str, api_key: str | None, session_id: str, version: str) -> dict[str, Any]:
55
- """litellm kwargs for an OpenCode model. Go asks clients to name themselves and send a
56
- stable per-conversation id; Zen gets the same headers."""
57
- model, api_base = litellm_route(provider, model_id)
58
- return {
59
- "model": model,
60
- "api_base": api_base,
61
- # Never fall back to OPENAI_API_KEY / ANTHROPIC_API_KEY: those would go to opencode.ai
62
- "api_key": api_key or os.environ.get(OPENCODE_API_KEY_ENV) or "missing-opencode-key",
63
- "extra_headers": {"User-Agent": f"dostuff/{version}", "x-opencode-session": session_id},
64
- }
65
-
66
-
67
- def models_url(provider: str) -> str:
68
- return f"{OPENCODE_PROVIDERS[provider]}/v1/models"
69
-
70
-
71
- def chat_model_ids(data: dict) -> list[str]:
72
- """Model ids from a /v1/models response, without the non-chat ones."""
73
- return [
74
- m["id"] for m in data.get("data") or []
75
- if isinstance(m, dict) and m.get("id") and not str(m["id"]).startswith(_NOT_CHAT)
76
- ]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes