hx-cli 0.2.13__tar.gz → 0.2.14__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.
- {hx_cli-0.2.13 → hx_cli-0.2.14}/CHANGELOG.md +48 -1
- {hx_cli-0.2.13 → hx_cli-0.2.14}/PKG-INFO +14 -8
- {hx_cli-0.2.13 → hx_cli-0.2.14}/README.md +13 -7
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/__init__.py +1 -1
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/agents/subagent.py +1 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/cli.py +5 -5
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/core/loop.py +39 -1
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/providers/base.py +5 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/providers/claude_code.py +72 -3
- hx_cli-0.2.14/src/hx/providers/claude_code_catalogue.py +276 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/providers/codex.py +1 -1
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/providers/models.py +256 -31
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tui/commands.py +56 -36
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tui/runtime.py +13 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tui/views/pickers.py +101 -14
- {hx_cli-0.2.13 → hx_cli-0.2.14}/tests/e2e/conftest.py +42 -4
- {hx_cli-0.2.13 → hx_cli-0.2.14}/tests/e2e/fake_claude.py +71 -4
- {hx_cli-0.2.13 → hx_cli-0.2.14}/tests/e2e/stub.py +32 -1
- {hx_cli-0.2.13 → hx_cli-0.2.14}/tests/e2e/test_claude_code.py +457 -7
- {hx_cli-0.2.13 → hx_cli-0.2.14}/tests/e2e/test_commands.py +133 -2
- hx_cli-0.2.13/src/hx/providers/claude_code_catalogue.py +0 -140
- {hx_cli-0.2.13 → hx_cli-0.2.14}/.github/workflows/ci.yml +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/.gitignore +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/examples/hooks/check.sh +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/install.sh +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/pyproject.toml +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/agents/__init__.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/agents/definitions.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/auth/__init__.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/auth/claude_code.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/auth/oauth/__init__.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/auth/oauth/browser.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/auth/oauth/callback.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/auth/oauth/codex.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/auth/oauth/devin.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/auth/oauth/pkce.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/auth/resolve.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/auth/store.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/config.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/core/__init__.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/core/checkpoints.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/core/compaction.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/core/context.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/core/events.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/core/images.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/core/lateinject.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/core/messages.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/core/session.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/core/title.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/core/usage.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/docs.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/frontmatter.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/git.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/hooks/__init__.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/hooks/engine.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/hooks/spec.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/keys.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/mcp/__init__.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/mcp/client.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/mcp/manager.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/mcp/oauth.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/net.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/paths.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/permissions/__init__.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/permissions/engine.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/permissions/parser.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/permissions/sandbox.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/providers/__init__.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/providers/claude_code_cli.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/providers/claude_code_transcript.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/providers/codex_catalogue.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/providers/devin.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/providers/devin_catalogue.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/providers/devin_wire.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/providers/openrouter.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/providers/protowire.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/providers/registry.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/providers/responses_codec.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/skills/__init__.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/skills/loader.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/skills/runtime.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/term/__init__.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/term/ansi.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/term/buffer.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/term/component.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/term/demo.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/term/editor.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/term/keydecode.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/term/loop.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/term/markdown.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/term/modifiers.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/term/primitives.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/term/sanitize.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/term/screen.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/term/syntax.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/term/terminal.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/term/undo.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/term/width.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/term/word_nav.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tools/__init__.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tools/anchors.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tools/base.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tools/bash.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tools/edit.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tools/glob.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tools/grep.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tools/output.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tools/read.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tools/registry.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tools/symbols.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tools/task.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tools/todo.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tools/websearch.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tools/write.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/trace.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tui/__init__.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tui/app.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tui/clipboard.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tui/format.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tui/fuzzy.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tui/glyphs.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tui/killring.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tui/limits.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tui/paint.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tui/renderers.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tui/roles.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tui/theme.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tui/theme_json.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tui/themes/ansi.json +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tui/themes/dark.json +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tui/themes/light.json +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tui/views/__init__.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tui/views/blocks.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tui/views/dialog.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tui/views/login.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tui/views/permission.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tui/views/prompt.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tui/views/status.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/src/hx/tui/views/transcript.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/tests/__init__.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/tests/conftest.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/tests/e2e/__init__.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/tests/e2e/fixtures/echo_server.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/tests/e2e/report.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/tests/e2e/terminal.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/tests/e2e/test_chat.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/tests/e2e/test_cli.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/tests/e2e/test_extend.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/tests/e2e/test_input.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/tests/e2e/test_print.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/tests/e2e/test_queue.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/tests/e2e/test_tools.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/tests/test_live.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/tests/test_live_claude_code.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/tests/test_live_codex.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/tests/test_live_devin.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/tests/test_live_tavily.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/tests/tls_proxy.py +0 -0
- {hx_cli-0.2.13 → hx_cli-0.2.14}/uv.lock +0 -0
|
@@ -28,6 +28,52 @@ the project follows [semantic versioning](https://semver.org/).
|
|
|
28
28
|
|
|
29
29
|
---
|
|
30
30
|
|
|
31
|
+
## [0.2.14] - 2026-10-04
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- When the installed Claude Code is behind its current release - and so lists older Claude
|
|
36
|
+
models than your subscription can run - `/model` says so under the list, with the command
|
|
37
|
+
that updates it (`brew upgrade --cask claude-code` for a Homebrew install, otherwise
|
|
38
|
+
`claude update`). After updating, the next `/model` lists the new models; no
|
|
39
|
+
refresh needed.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- The model catalogue is refreshed from inside the `/model` picker: press `ctrl+r`. The
|
|
44
|
+
list updates in place and keeps your filter and selection; the outcome, or why the refresh
|
|
45
|
+
failed, shows under the list. With no models at all, `/model` now opens anyway and says
|
|
46
|
+
so, so `ctrl+r` is there to fill it.
|
|
47
|
+
|
|
48
|
+
### Removed
|
|
49
|
+
|
|
50
|
+
- `/models refresh`. It sat one letter away from `/model` and did nothing without its one
|
|
51
|
+
argument; use `ctrl+r` in the `/model` picker.
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- On the `claude-code` route, the context window is the one Claude Code reports, not a
|
|
56
|
+
200k guess: Opus 5.5 runs with 1M. Claude Code lists its models without their sizes, and
|
|
57
|
+
what it serves depends on the plan, so a model starts at the window it was measured
|
|
58
|
+
serving on every plan - 1M for Opus 5.5, Sonnet 5.5, Sonnet 5 and Opus 4.7, 200k
|
|
59
|
+
otherwise - until its first turn reports the real window and maximum output for your
|
|
60
|
+
account. From then on the context gauge, the compaction threshold, the `/model` picker
|
|
61
|
+
and later sessions all use it, and a catalogue refresh keeps it.
|
|
62
|
+
- A model Claude Code lists at its long context - Fable 5.1 as `claude-fable-5-1[1m]` -
|
|
63
|
+
runs at it: `/model` offers `claude-code/claude-fable-5-1[1m]` at 1M, where HX used to
|
|
64
|
+
drop the `[1m]` and run the model at its standard context.
|
|
65
|
+
- A turn refused because the model, or its 1M context, needs usage credits your Claude plan
|
|
66
|
+
has off no longer reads "usage limit is reached. It resets at ...": HX says what needs the
|
|
67
|
+
credits, where to turn them on, and which model runs without them. Once they are on, the
|
|
68
|
+
next turn runs.
|
|
69
|
+
- After a catalogue refresh (`ctrl+r` in `/model`, or signing in), the session in progress
|
|
70
|
+
measures its context against the refreshed window, not the one it started with.
|
|
71
|
+
- `/model anthropic` (or typing `anthropic` in the picker) lists the Claude models your Claude
|
|
72
|
+
Code subscription runs, first, ahead of the per-token OpenRouter ones; `openai` does the same
|
|
73
|
+
for Codex. Several words match in any order: `/model anthropic sonnet`.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
31
77
|
## [0.2.13] - 2026-10-04
|
|
32
78
|
|
|
33
79
|
### Added
|
|
@@ -833,7 +879,8 @@ First release, published to PyPI as [`hx-cli`](https://pypi.org/project/hx-cli/)
|
|
|
833
879
|
- `/configure` and `hx auth` for the OpenRouter key, `hx upgrade` for
|
|
834
880
|
self-update, and `install.sh` bootstrapping uv with a pinned Python.
|
|
835
881
|
|
|
836
|
-
[Unreleased]: https://github.com/aletisunil/hx/compare/v0.2.
|
|
882
|
+
[Unreleased]: https://github.com/aletisunil/hx/compare/v0.2.14...HEAD
|
|
883
|
+
[0.2.14]: https://github.com/aletisunil/hx/compare/v0.2.13...v0.2.14
|
|
837
884
|
[0.2.13]: https://github.com/aletisunil/hx/compare/v0.2.12...v0.2.13
|
|
838
885
|
[0.2.12]: https://github.com/aletisunil/hx/compare/v0.2.11...v0.2.12
|
|
839
886
|
[0.2.11]: https://github.com/aletisunil/hx/compare/v0.2.10...v0.2.11
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hx-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.14
|
|
4
4
|
Summary: HX - an agent harness for the terminal
|
|
5
5
|
Project-URL: Homepage, https://hx.sunilaleti.dev
|
|
6
6
|
Project-URL: Documentation, https://hx.sunilaleti.dev
|
|
@@ -156,7 +156,7 @@ reach it at all.
|
|
|
156
156
|
|
|
157
157
|
**Which Codex models you get** is decided by your ChatGPT account, not by HX.
|
|
158
158
|
Signing in fetches the account's own list from the Codex backend, and `/model`
|
|
159
|
-
offers exactly that; `/
|
|
159
|
+
offers exactly that; ctrl+r in `/model` asks again. Entitlement is per model -
|
|
160
160
|
the same `plus` account can run `gpt-5.6-sol` and be refused `gpt-5.3-codex` —
|
|
161
161
|
so a model the picker does not list is one the subscription does not cover.
|
|
162
162
|
|
|
@@ -218,7 +218,7 @@ you to sign in again. On a machine with no browser at all, a session token in
|
|
|
218
218
|
`HX_DEVIN_API_KEY` or `DEVIN_API_KEY` is used when nothing is saved.
|
|
219
219
|
|
|
220
220
|
Signing in fetches the account's own model list, and `/model` offers exactly
|
|
221
|
-
that; `/
|
|
221
|
+
that; ctrl+r in `/model` asks again. Devin publishes each reasoning depth as a
|
|
222
222
|
model of its own - Claude Opus 5 High, Claude Opus 5 Max - and HX folds each
|
|
223
223
|
family into one model, so `/effort` picks the depth on Devin exactly as it does
|
|
224
224
|
on Codex. A fast lane or a 1M-context lane stays a separate model, since it
|
|
@@ -264,8 +264,15 @@ rest - are removed from its environment; its TLS settings are kept (see
|
|
|
264
264
|
`claude` other than the one on `PATH`.
|
|
265
265
|
|
|
266
266
|
`/model` offers the models Claude Code lists for the account, each with the
|
|
267
|
-
efforts it accepts for `/effort`; `/
|
|
268
|
-
|
|
267
|
+
efforts it accepts for `/effort`; ctrl+r in `/model` asks again. A model Claude
|
|
268
|
+
Code runs at its long context keeps `[1m]` in its id
|
|
269
|
+
(`claude-code/claude-fable-5-1[1m]`), exactly as Claude Code's own `/model` runs
|
|
270
|
+
it. Claude Code lists no context sizes, so a model starts at the window it was
|
|
271
|
+
measured serving on every plan - 1M for Opus 5.5, Sonnet 5.5, Sonnet 5 and Opus
|
|
272
|
+
4.7, otherwise 200k - and from its first turn uses the window Claude Code
|
|
273
|
+
reports for your account. When a usage limit is reached, HX says which one and
|
|
274
|
+
when it resets; when a model or its 1M context needs usage credits your plan
|
|
275
|
+
has off, HX says that instead, and what to pick without them.
|
|
269
276
|
|
|
270
277
|
**Web search.** Optional, and the one credential that is not a model route.
|
|
271
278
|
It powers two tools:
|
|
@@ -421,7 +428,7 @@ be rebound (see [Keybindings](#keybindings)).
|
|
|
421
428
|
| `ctrl+d` | exit, when the prompt is empty |
|
|
422
429
|
| `shift+tab` | cycle permission mode |
|
|
423
430
|
| `ctrl+p` | command palette |
|
|
424
|
-
| `ctrl+l` | model picker |
|
|
431
|
+
| `ctrl+l` | model picker; inside it, `ctrl+r` fetches the catalogue again |
|
|
425
432
|
| `ctrl+o` | expand tool output (also `ctrl+r`) |
|
|
426
433
|
| `ctrl+t` | show the current plan |
|
|
427
434
|
| `ctrl+x` | copy the selected message to the clipboard |
|
|
@@ -527,8 +534,7 @@ silently resolved.
|
|
|
527
534
|
|
|
528
535
|
| Command | Does |
|
|
529
536
|
|---|---|
|
|
530
|
-
| `/model [query]` | pick a model; shows context window, price per Mtok, cache support |
|
|
531
|
-
| `/models refresh` | re-fetch the catalogue |
|
|
537
|
+
| `/model [query]` | pick a model; shows context window, price per Mtok, cache support. `ctrl+r` in the picker re-fetches the catalogue |
|
|
532
538
|
| `/effort [level]` | how hard a reasoning model thinks, from the levels it offers |
|
|
533
539
|
| `/configure` | session settings and the API key |
|
|
534
540
|
| `/login [provider]` | sign in to a model route |
|
|
@@ -115,7 +115,7 @@ reach it at all.
|
|
|
115
115
|
|
|
116
116
|
**Which Codex models you get** is decided by your ChatGPT account, not by HX.
|
|
117
117
|
Signing in fetches the account's own list from the Codex backend, and `/model`
|
|
118
|
-
offers exactly that; `/
|
|
118
|
+
offers exactly that; ctrl+r in `/model` asks again. Entitlement is per model -
|
|
119
119
|
the same `plus` account can run `gpt-5.6-sol` and be refused `gpt-5.3-codex` —
|
|
120
120
|
so a model the picker does not list is one the subscription does not cover.
|
|
121
121
|
|
|
@@ -177,7 +177,7 @@ you to sign in again. On a machine with no browser at all, a session token in
|
|
|
177
177
|
`HX_DEVIN_API_KEY` or `DEVIN_API_KEY` is used when nothing is saved.
|
|
178
178
|
|
|
179
179
|
Signing in fetches the account's own model list, and `/model` offers exactly
|
|
180
|
-
that; `/
|
|
180
|
+
that; ctrl+r in `/model` asks again. Devin publishes each reasoning depth as a
|
|
181
181
|
model of its own - Claude Opus 5 High, Claude Opus 5 Max - and HX folds each
|
|
182
182
|
family into one model, so `/effort` picks the depth on Devin exactly as it does
|
|
183
183
|
on Codex. A fast lane or a 1M-context lane stays a separate model, since it
|
|
@@ -223,8 +223,15 @@ rest - are removed from its environment; its TLS settings are kept (see
|
|
|
223
223
|
`claude` other than the one on `PATH`.
|
|
224
224
|
|
|
225
225
|
`/model` offers the models Claude Code lists for the account, each with the
|
|
226
|
-
efforts it accepts for `/effort`; `/
|
|
227
|
-
|
|
226
|
+
efforts it accepts for `/effort`; ctrl+r in `/model` asks again. A model Claude
|
|
227
|
+
Code runs at its long context keeps `[1m]` in its id
|
|
228
|
+
(`claude-code/claude-fable-5-1[1m]`), exactly as Claude Code's own `/model` runs
|
|
229
|
+
it. Claude Code lists no context sizes, so a model starts at the window it was
|
|
230
|
+
measured serving on every plan - 1M for Opus 5.5, Sonnet 5.5, Sonnet 5 and Opus
|
|
231
|
+
4.7, otherwise 200k - and from its first turn uses the window Claude Code
|
|
232
|
+
reports for your account. When a usage limit is reached, HX says which one and
|
|
233
|
+
when it resets; when a model or its 1M context needs usage credits your plan
|
|
234
|
+
has off, HX says that instead, and what to pick without them.
|
|
228
235
|
|
|
229
236
|
**Web search.** Optional, and the one credential that is not a model route.
|
|
230
237
|
It powers two tools:
|
|
@@ -380,7 +387,7 @@ be rebound (see [Keybindings](#keybindings)).
|
|
|
380
387
|
| `ctrl+d` | exit, when the prompt is empty |
|
|
381
388
|
| `shift+tab` | cycle permission mode |
|
|
382
389
|
| `ctrl+p` | command palette |
|
|
383
|
-
| `ctrl+l` | model picker |
|
|
390
|
+
| `ctrl+l` | model picker; inside it, `ctrl+r` fetches the catalogue again |
|
|
384
391
|
| `ctrl+o` | expand tool output (also `ctrl+r`) |
|
|
385
392
|
| `ctrl+t` | show the current plan |
|
|
386
393
|
| `ctrl+x` | copy the selected message to the clipboard |
|
|
@@ -486,8 +493,7 @@ silently resolved.
|
|
|
486
493
|
|
|
487
494
|
| Command | Does |
|
|
488
495
|
|---|---|
|
|
489
|
-
| `/model [query]` | pick a model; shows context window, price per Mtok, cache support |
|
|
490
|
-
| `/models refresh` | re-fetch the catalogue |
|
|
496
|
+
| `/model [query]` | pick a model; shows context window, price per Mtok, cache support. `ctrl+r` in the picker re-fetches the catalogue |
|
|
491
497
|
| `/effort [level]` | how hard a reasoning model thinks, from the levels it offers |
|
|
492
498
|
| `/configure` | session settings and the API key |
|
|
493
499
|
| `/login [provider]` | sign in to a model route |
|
|
@@ -173,6 +173,7 @@ class SubagentRunner:
|
|
|
173
173
|
model_info=self.models.get_or_default(model) if self.models else None,
|
|
174
174
|
project_context=self.project_context,
|
|
175
175
|
hooks=self.hooks,
|
|
176
|
+
models=self.models,
|
|
176
177
|
)
|
|
177
178
|
loop.origin = f"{definition.name} subagent"
|
|
178
179
|
return loop
|
|
@@ -291,13 +291,10 @@ class Runtime:
|
|
|
291
291
|
self.notices.append(f"Model catalogue refresh failed: {describe(exc)}")
|
|
292
292
|
return
|
|
293
293
|
finally:
|
|
294
|
-
|
|
295
|
-
# being replaced. Left alone it would gauge against yesterday's
|
|
296
|
-
# context window and decide image support from a cache that did
|
|
297
|
-
# not record it - until the user happened to switch models.
|
|
298
|
-
self.loop.model_info = self.models.get_or_default(self.loop.model)
|
|
294
|
+
self.loop.catalogue_refreshed()
|
|
299
295
|
for label, error in self.models.subscription_errors():
|
|
300
296
|
self.notices.append(f"Could not list this account's {label} models: {error}")
|
|
297
|
+
self.notices.extend(self.models.subscription_advice())
|
|
301
298
|
|
|
302
299
|
async def aclose(self) -> None:
|
|
303
300
|
if self.mcp is not None:
|
|
@@ -489,6 +486,7 @@ def build_runtime(parsed: ParsedArgs, *, resume: str | None = None) -> Runtime:
|
|
|
489
486
|
project_context=project_context,
|
|
490
487
|
instructions=instructions,
|
|
491
488
|
hooks=hooks,
|
|
489
|
+
models=models,
|
|
492
490
|
)
|
|
493
491
|
|
|
494
492
|
return Runtime(
|
|
@@ -1303,6 +1301,8 @@ def _report_models_after_login(provider_id: str) -> None:
|
|
|
1303
1301
|
print(f"Could not refresh the model list: {describe(exc)}", file=sys.stderr)
|
|
1304
1302
|
for label, error in models.subscription_errors():
|
|
1305
1303
|
print(f"Could not list this account's {label} models: {error}", file=sys.stderr)
|
|
1304
|
+
for advice in models.subscription_advice():
|
|
1305
|
+
print(advice, file=sys.stderr)
|
|
1306
1306
|
|
|
1307
1307
|
available = [m.id for m in models.all() if m.provider_id == provider_id]
|
|
1308
1308
|
if not available:
|
|
@@ -66,7 +66,7 @@ if TYPE_CHECKING:
|
|
|
66
66
|
from hx.hooks.engine import HookEngine
|
|
67
67
|
from hx.permissions.engine import PermissionEngine
|
|
68
68
|
from hx.providers.base import Provider
|
|
69
|
-
from hx.providers.models import ModelInfo
|
|
69
|
+
from hx.providers.models import ModelInfo, ModelRegistry
|
|
70
70
|
from hx.skills.runtime import ActiveSkills
|
|
71
71
|
from hx.tools.registry import ToolRegistry
|
|
72
72
|
|
|
@@ -107,6 +107,7 @@ class AgentLoop:
|
|
|
107
107
|
project_context: str | None = None,
|
|
108
108
|
instructions: list[Instructions] | None = None,
|
|
109
109
|
hooks: HookEngine | None = None,
|
|
110
|
+
models: ModelRegistry | None = None,
|
|
110
111
|
) -> None:
|
|
111
112
|
self.provider = provider
|
|
112
113
|
self.session = session
|
|
@@ -124,6 +125,9 @@ class AgentLoop:
|
|
|
124
125
|
self.instructions = instructions or []
|
|
125
126
|
"""The AGENTS.md files folded into ``project_context``, least specific first."""
|
|
126
127
|
self.hooks = hooks
|
|
128
|
+
self.models = models
|
|
129
|
+
"""Where a window the backend reports is kept, so the picker and the
|
|
130
|
+
next session start from it rather than from the catalogue's guess."""
|
|
127
131
|
self._cancelled = False
|
|
128
132
|
self._turn_index = 0
|
|
129
133
|
self._steer: list[UserTurn] = []
|
|
@@ -155,6 +159,19 @@ class AgentLoop:
|
|
|
155
159
|
self.compactor.provider = provider
|
|
156
160
|
return previous
|
|
157
161
|
|
|
162
|
+
def catalogue_refreshed(self) -> None:
|
|
163
|
+
"""Re-read the model in force from a catalogue that has just changed.
|
|
164
|
+
|
|
165
|
+
It was resolved from the one being replaced. Left alone, the window the
|
|
166
|
+
gauge and the compaction threshold measure against would stay the old
|
|
167
|
+
one, and image support would be decided from it, until the user
|
|
168
|
+
happened to switch models.
|
|
169
|
+
"""
|
|
170
|
+
if self.models is None:
|
|
171
|
+
return
|
|
172
|
+
self.model_info = self.models.get_or_default(self.model)
|
|
173
|
+
self.session.usage.context_window = self.model_info.context_window
|
|
174
|
+
|
|
158
175
|
def set_model(self, model_id: str, model_info: ModelInfo | None = None) -> None:
|
|
159
176
|
self.session.meta.model = model_id
|
|
160
177
|
self.model_info = model_info
|
|
@@ -450,6 +467,7 @@ class AgentLoop:
|
|
|
450
467
|
if isinstance(item, StreamEnd):
|
|
451
468
|
stop_reason = item.stop_reason
|
|
452
469
|
usage = item.usage
|
|
470
|
+
self._adopt_limits(item)
|
|
453
471
|
continue
|
|
454
472
|
if not isinstance(item, StreamDelta): # pragma: no cover - defensive
|
|
455
473
|
continue
|
|
@@ -581,6 +599,26 @@ class AgentLoop:
|
|
|
581
599
|
blocks.extend(tool_calls)
|
|
582
600
|
return assistant_message(blocks, model=self.model)
|
|
583
601
|
|
|
602
|
+
def _adopt_limits(self, end: StreamEnd) -> None:
|
|
603
|
+
"""Take the window the backend reported over the catalogue's figure.
|
|
604
|
+
|
|
605
|
+
Before the usage update that follows, so the gauge it drives - and the
|
|
606
|
+
compaction threshold measured against the same window - move together.
|
|
607
|
+
Kept in the catalogue too, by every mode and every subagent alike: a
|
|
608
|
+
figure only the live session knew would be the guess again next time.
|
|
609
|
+
"""
|
|
610
|
+
info = self.model_info
|
|
611
|
+
if info is None or info.id != self.model:
|
|
612
|
+
return
|
|
613
|
+
window = end.context_window or info.context_window
|
|
614
|
+
output = end.max_output_tokens or info.max_output_tokens
|
|
615
|
+
if (window, output) == (info.context_window, info.max_output_tokens):
|
|
616
|
+
return
|
|
617
|
+
self.model_info = replace(info, context_window=window, max_output_tokens=output)
|
|
618
|
+
self.session.usage.context_window = window
|
|
619
|
+
if self.models is not None:
|
|
620
|
+
self.models.adopt_limits(info.id, window, output)
|
|
621
|
+
|
|
584
622
|
def _record_usage(
|
|
585
623
|
self,
|
|
586
624
|
usage: TurnUsage,
|
|
@@ -38,6 +38,11 @@ class StreamDelta:
|
|
|
38
38
|
class StreamEnd:
|
|
39
39
|
stop_reason: StopReason
|
|
40
40
|
usage: TurnUsage = field(default_factory=TurnUsage)
|
|
41
|
+
context_window: int | None = None
|
|
42
|
+
"""The model's window as the backend reported it running this turn, when
|
|
43
|
+
it does. Truer than the catalogue's figure, which for some routes is a
|
|
44
|
+
guess: Claude Code lists its models without their sizes."""
|
|
45
|
+
max_output_tokens: int | None = None
|
|
41
46
|
|
|
42
47
|
|
|
43
48
|
StreamItem = StreamDelta | StreamEnd
|
|
@@ -51,6 +51,7 @@ from hx.core.usage import TurnUsage
|
|
|
51
51
|
from hx.net import with_tls_hint
|
|
52
52
|
from hx.providers import claude_code_transcript as transcript
|
|
53
53
|
from hx.providers.base import ProviderError, ProviderRequest, StreamDelta, StreamEnd, StreamItem
|
|
54
|
+
from hx.providers.claude_code_catalogue import DATED
|
|
54
55
|
from hx.providers.claude_code_cli import CliProcess, Exit, ServedTool, ToolServer
|
|
55
56
|
|
|
56
57
|
PROVIDER_ID = cc_auth.PROVIDER_ID
|
|
@@ -435,7 +436,7 @@ class ClaudeCodeProvider:
|
|
|
435
436
|
frame = await frames.get()
|
|
436
437
|
if isinstance(frame, Exit):
|
|
437
438
|
conversation.state = State.DEAD
|
|
438
|
-
raise ProviderError(_exit_message(frame, turn.rate_limit))
|
|
439
|
+
raise ProviderError(_exit_message(frame, turn.rate_limit, request.model))
|
|
439
440
|
kind = frame.get("type")
|
|
440
441
|
if kind == "stream_event" and frame.get("parent_tool_use_id") is None:
|
|
441
442
|
for item in turn.consume(frame.get("event") or {}):
|
|
@@ -465,7 +466,13 @@ class ClaudeCodeProvider:
|
|
|
465
466
|
payload = from_payload(turn)
|
|
466
467
|
if payload is not None:
|
|
467
468
|
yield payload
|
|
468
|
-
|
|
469
|
+
window, output = _reported_limits(frame, request.model)
|
|
470
|
+
yield StreamEnd(
|
|
471
|
+
turn.stop_reason(),
|
|
472
|
+
turn.finish_usage(started),
|
|
473
|
+
context_window=window,
|
|
474
|
+
max_output_tokens=output,
|
|
475
|
+
)
|
|
469
476
|
return
|
|
470
477
|
|
|
471
478
|
def _remember(self, conversation: Conversation, turn: _Turn) -> None:
|
|
@@ -800,6 +807,30 @@ def _wire_model(model_id: str) -> str:
|
|
|
800
807
|
return model_id[len(MODEL_PREFIX) :] if model_id.startswith(MODEL_PREFIX) else model_id
|
|
801
808
|
|
|
802
809
|
|
|
810
|
+
def _reported_limits(frame: dict[str, Any], model_id: str) -> tuple[int | None, int | None]:
|
|
811
|
+
"""``(contextWindow, maxOutputTokens)`` Claude Code reports for the model run.
|
|
812
|
+
|
|
813
|
+
``modelUsage`` is keyed by the resolved name, and lists every model the
|
|
814
|
+
turn touched - one Claude Code chose for itself among them - so only the
|
|
815
|
+
entry for the model asked for is taken. Started by alias (``opus``), that
|
|
816
|
+
entry cannot be told apart and nothing is taken.
|
|
817
|
+
"""
|
|
818
|
+
wanted = _wire_model(model_id)
|
|
819
|
+
usage = frame.get("modelUsage")
|
|
820
|
+
for name, entry in usage.items() if isinstance(usage, dict) else ():
|
|
821
|
+
if not isinstance(entry, dict):
|
|
822
|
+
continue
|
|
823
|
+
names = {DATED.sub("", str(n)) for n in (name, entry.get("canonicalModel")) if n}
|
|
824
|
+
if wanted not in names:
|
|
825
|
+
continue
|
|
826
|
+
window, output = entry.get("contextWindow"), entry.get("maxOutputTokens")
|
|
827
|
+
return (
|
|
828
|
+
window if isinstance(window, int) and window > 0 else None,
|
|
829
|
+
output if isinstance(output, int) and output > 0 else None,
|
|
830
|
+
)
|
|
831
|
+
return None, None
|
|
832
|
+
|
|
833
|
+
|
|
803
834
|
def _private_file(text: str) -> Path:
|
|
804
835
|
fd, name = tempfile.mkstemp(prefix="hx-claude-", suffix=".md")
|
|
805
836
|
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
@@ -833,7 +864,9 @@ def _startup_failure(message: str, cli: CliProcess) -> str:
|
|
|
833
864
|
return with_tls_hint(f"Claude Code failed to start: {message}")
|
|
834
865
|
|
|
835
866
|
|
|
836
|
-
def _exit_message(frame: Exit, rate_limit: dict[str, Any] | None) -> str:
|
|
867
|
+
def _exit_message(frame: Exit, rate_limit: dict[str, Any] | None, model_id: str) -> str:
|
|
868
|
+
if _needs_credits(rate_limit, frame.stderr):
|
|
869
|
+
return _credits_message(model_id, frame.stderr.strip())
|
|
837
870
|
if rate_limit:
|
|
838
871
|
return _limit_message(rate_limit)
|
|
839
872
|
detail = frame.stderr.strip()
|
|
@@ -849,6 +882,8 @@ def _result_error(
|
|
|
849
882
|
errors = frame.get("errors")
|
|
850
883
|
text = "; ".join(str(e) for e in errors) if isinstance(errors, list) and errors else ""
|
|
851
884
|
text = text or str(frame.get("result") or frame.get("subtype") or "unknown error")
|
|
885
|
+
if _needs_credits(rate_limit, text):
|
|
886
|
+
return _credits_message(request.model, text)
|
|
852
887
|
if _billed_as_extra_usage(frame, text, rate_limit):
|
|
853
888
|
return _extra_usage_message(request, text)
|
|
854
889
|
if rate_limit or "usage limit" in text.lower() or "out of extra usage" in text.lower():
|
|
@@ -858,6 +893,40 @@ def _result_error(
|
|
|
858
893
|
return with_tls_hint(f"Claude Code: {text.strip()}")
|
|
859
894
|
|
|
860
895
|
|
|
896
|
+
def _needs_credits(rate_limit: dict[str, Any] | None, text: str) -> bool:
|
|
897
|
+
"""A turn refused because the model, or its long context, needs usage credits.
|
|
898
|
+
|
|
899
|
+
Claude Code reports it as a rejected rate limit, with the billing period's
|
|
900
|
+
reset beside it, but no usage window is reached: the plan does not include
|
|
901
|
+
what was asked for, and it fails the same way until credits are turned on.
|
|
902
|
+
"""
|
|
903
|
+
if (rate_limit or {}).get("errorCode") == "credits_required":
|
|
904
|
+
return True
|
|
905
|
+
lowered = text.lower()
|
|
906
|
+
return "requires usage credits" in lowered or "usage credits required" in lowered
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
def _credits_message(model_id: str, detail: str) -> str:
|
|
910
|
+
"""Said in HX's words alone: Claude Code's own adds a link meant for its
|
|
911
|
+
CLI and advice for flags HX sets itself."""
|
|
912
|
+
model = _wire_model(model_id)
|
|
913
|
+
base = model.removesuffix("[1m]")
|
|
914
|
+
lowered = detail.lower()
|
|
915
|
+
# Asked for at 1M, it is the long context that needs them - unless what was
|
|
916
|
+
# said names the model itself: "Fable 5.1 requires usage credits".
|
|
917
|
+
names_model = "requires usage credits" in lowered
|
|
918
|
+
if "1m context" in lowered or "long context" in lowered or (model != base and not names_model):
|
|
919
|
+
what = f"The 1M context of {base} needs"
|
|
920
|
+
instead = f"pick {MODEL_PREFIX}{base} for its standard context"
|
|
921
|
+
else:
|
|
922
|
+
what = f"{base} needs"
|
|
923
|
+
instead = "pick another model"
|
|
924
|
+
return (
|
|
925
|
+
f"{what} usage credits, which your Claude plan has off - its usage limits are not "
|
|
926
|
+
f"reached. Turn them on at claude.ai/settings/usage and send again, or {instead}."
|
|
927
|
+
)
|
|
928
|
+
|
|
929
|
+
|
|
861
930
|
def _billed_as_extra_usage(
|
|
862
931
|
frame: dict[str, Any], text: str, rate_limit: dict[str, Any] | None
|
|
863
932
|
) -> bool:
|