agentshim 0.20.0__tar.gz → 0.21.0__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 (135) hide show
  1. {agentshim-0.20.0 → agentshim-0.21.0}/CHANGELOG.md +17 -0
  2. {agentshim-0.20.0 → agentshim-0.21.0}/PKG-INFO +1 -1
  3. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/__init__.py +3 -1
  4. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/core/__init__.py +2 -0
  5. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/core/auth.py +38 -0
  6. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/core/models.py +56 -0
  7. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/core/profile.py +5 -1
  8. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/models.py +1 -1
  9. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/claude/model_data.py +10 -0
  10. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/claude/provider.py +4 -0
  11. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/codex/model_data.py +18 -8
  12. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/codex/provider.py +4 -0
  13. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/copilot/provider.py +3 -1
  14. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/opencode/model_data.py +3 -0
  15. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/opencode/provider.py +3 -0
  16. {agentshim-0.20.0 → agentshim-0.21.0}/pyproject.toml +1 -1
  17. {agentshim-0.20.0 → agentshim-0.21.0}/.gitignore +0 -0
  18. {agentshim-0.20.0 → agentshim-0.21.0}/LICENSE +0 -0
  19. {agentshim-0.20.0 → agentshim-0.21.0}/README.md +0 -0
  20. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/agent.py +0 -0
  21. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/core/_files.py +0 -0
  22. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/core/checkpoints.py +0 -0
  23. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/core/clock.py +0 -0
  24. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/core/conversation.py +0 -0
  25. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/core/env.py +0 -0
  26. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/core/errors.py +0 -0
  27. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/core/events.py +0 -0
  28. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/core/home.py +0 -0
  29. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/core/ids.py +0 -0
  30. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/core/mcp.py +0 -0
  31. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/core/permissions.py +0 -0
  32. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/core/plugin.py +0 -0
  33. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/core/pricing.py +0 -0
  34. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/core/provider.py +0 -0
  35. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/core/schema.py +0 -0
  36. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/core/session_policy.py +0 -0
  37. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/core/skills.py +0 -0
  38. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/core/status.py +0 -0
  39. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/core/stream.py +0 -0
  40. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/core/turn.py +0 -0
  41. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/core/usage.py +0 -0
  42. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/execution/__init__.py +0 -0
  43. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/execution/confinement.py +0 -0
  44. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/execution/docker.py +0 -0
  45. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/execution/executor.py +0 -0
  46. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/execution/host.py +0 -0
  47. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/execution/process.py +0 -0
  48. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/execution/readiness.py +0 -0
  49. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/execution/transform.py +0 -0
  50. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/oneshot.py +0 -0
  51. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/pricing.py +0 -0
  52. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/probe.py +0 -0
  53. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/__init__.py +0 -0
  54. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/_testing.py +0 -0
  55. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/catalog.py +0 -0
  56. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/claude/__init__.py +0 -0
  57. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/claude/events.py +0 -0
  58. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/claude/failures.py +0 -0
  59. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/claude/hooks/__init__.py +0 -0
  60. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/claude/hooks/confine_reads.py +0 -0
  61. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/claude/parser.py +0 -0
  62. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/claude/plugin.py +0 -0
  63. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/claude/probe.py +0 -0
  64. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/claude/rate_limits.py +0 -0
  65. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/claude/sandbox.py +0 -0
  66. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/claude/scripted.py +0 -0
  67. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/claude/stream_argv.py +0 -0
  68. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/claude/stream_transport.py +0 -0
  69. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/claude/testing.py +0 -0
  70. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/claude/tools.py +0 -0
  71. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/claude/user_hooks.py +0 -0
  72. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/codex/__init__.py +0 -0
  73. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/codex/_toml.py +0 -0
  74. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/codex/app_server/__init__.py +0 -0
  75. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/codex/app_server/_wire.py +0 -0
  76. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/codex/app_server/approvals.py +0 -0
  77. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/codex/app_server/channel.py +0 -0
  78. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/codex/app_server/errors.py +0 -0
  79. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/codex/app_server/items.py +0 -0
  80. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/codex/app_server/mcp.py +0 -0
  81. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/codex/app_server/permissions.py +0 -0
  82. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/codex/app_server/profile.py +0 -0
  83. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/codex/app_server/protocol.py +0 -0
  84. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/codex/app_server/rate_limits.py +0 -0
  85. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/codex/app_server/transport.py +0 -0
  86. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/codex/app_server/usage.py +0 -0
  87. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/codex/events.py +0 -0
  88. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/codex/failures.py +0 -0
  89. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/codex/mcp_scope.py +0 -0
  90. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/codex/parser.py +0 -0
  91. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/codex/plugin.py +0 -0
  92. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/codex/probe.py +0 -0
  93. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/codex/rules.py +0 -0
  94. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/codex/sandbox.py +0 -0
  95. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/codex/scripted.py +0 -0
  96. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/codex/skills.py +0 -0
  97. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/codex/testing.py +0 -0
  98. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/copilot/__init__.py +0 -0
  99. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/copilot/events.py +0 -0
  100. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/copilot/parser.py +0 -0
  101. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/copilot/plugin.py +0 -0
  102. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/copilot/probe.py +0 -0
  103. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/copilot/scripted.py +0 -0
  104. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/copilot/testing.py +0 -0
  105. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/gemini/__init__.py +0 -0
  106. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/gemini/events.py +0 -0
  107. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/gemini/parser.py +0 -0
  108. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/gemini/plugin.py +0 -0
  109. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/gemini/probe.py +0 -0
  110. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/gemini/provider.py +0 -0
  111. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/gemini/scripted.py +0 -0
  112. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/gemini/testing.py +0 -0
  113. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/opencode/__init__.py +0 -0
  114. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/opencode/events.py +0 -0
  115. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/opencode/parser.py +0 -0
  116. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/opencode/plugin.py +0 -0
  117. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/opencode/probe.py +0 -0
  118. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/opencode/scripted.py +0 -0
  119. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/opencode/testing.py +0 -0
  120. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/providers/static_plugin.py +0 -0
  121. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/py.typed +0 -0
  122. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/runtime.py +0 -0
  123. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/session.py +0 -0
  124. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/testing/__init__.py +0 -0
  125. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/testing/claude_stream.py +0 -0
  126. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/testing/clock.py +0 -0
  127. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/testing/codex_app_server.py +0 -0
  128. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/testing/confinement.py +0 -0
  129. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/testing/contracts.py +0 -0
  130. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/testing/conversation.py +0 -0
  131. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/testing/probe.py +0 -0
  132. {agentshim-0.20.0 → agentshim-0.21.0}/agentshim/testing/process.py +0 -0
  133. {agentshim-0.20.0 → agentshim-0.21.0}/tests/fixtures/claude_stream/README.md +0 -0
  134. {agentshim-0.20.0 → agentshim-0.21.0}/tests/fixtures/codex/README.md +0 -0
  135. {agentshim-0.20.0 → agentshim-0.21.0}/tests/fixtures/codex_app_server/README.md +0 -0
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.21.0 (2026-10-18)
4
+
5
+ ### Added
6
+
7
+ - `ModelSpec` ranks a model's generation among its vendor's: `generation_id` (`"5.5"`, shared
8
+ by every model of that generation), `generation` (`"Claude 5.5"`, `"GPT-6"`) and
9
+ `generation_rank` (`1` the newest). The three are set together, default to `None`, and are
10
+ filled for every shipped model. `ModelTable` rejects a vendor whose generations disagree on a
11
+ name or rank, or share a rank. Newest first: Claude 5.5, 5.1, 5, 4.8, 4.7, 4.6, 4.5; GPT-6.1,
12
+ 6, 5.6, 5.5; Gemini 3.
13
+ - `ProviderProfile.login` is a `LoginSpec`: the CLI's own command for a person to sign in
14
+ (`claude auth login`, `codex login` with `--device-auth` as its device variant,
15
+ `copilot login`, `opencode auth login`), and whether it needs a terminal.
16
+ `LoginSpec.command(binary, device=False)` is the full command line. Gemini CLI has no
17
+ sign-in command (`login` is `None`). Signed-in state stays in `ProbeSpec.auth` and
18
+ `probe_provider`.
19
+
3
20
  ## 0.20.0 (2026-10-18)
4
21
 
5
22
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentshim
3
- Version: 0.20.0
3
+ Version: 0.21.0
4
4
  Summary: Provider-agnostic coding-agent CLI shims
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -51,6 +51,7 @@ from .core import (
51
51
  IdAllocator,
52
52
  InMemoryCheckpointStore,
53
53
  Lifecycle,
54
+ LoginSpec,
54
55
  McpConfigError,
55
56
  McpInstallation,
56
57
  McpMechanism,
@@ -184,7 +185,7 @@ from .providers.opencode import OpencodeProvider
184
185
  from .runtime import Agent
185
186
  from .session import Session, Turn, TurnTicket, map_mcp_servers
186
187
 
187
- __version__ = "0.20.0"
188
+ __version__ = "0.21.0"
188
189
 
189
190
  __all__ = [ # noqa: RUF022 - public API test uses Python's case-sensitive order
190
191
  "Agent",
@@ -253,6 +254,7 @@ __all__ = [ # noqa: RUF022 - public API test uses Python's case-sensitive order
253
254
  "IdAllocator",
254
255
  "InMemoryCheckpointStore",
255
256
  "Lifecycle",
257
+ "LoginSpec",
256
258
  "McpConfigError",
257
259
  "McpInstallation",
258
260
  "McpMechanism",
@@ -19,6 +19,7 @@ from .auth import (
19
19
  FileCredential,
20
20
  HostAuthGap,
21
21
  HostAuthUnplannableError,
22
+ LoginSpec,
22
23
  SettingsSpec,
23
24
  StagedCredentialFile,
24
25
  host_auth_plan,
@@ -171,6 +172,7 @@ __all__ = [
171
172
  "IdAllocator",
172
173
  "InMemoryCheckpointStore",
173
174
  "Lifecycle",
175
+ "LoginSpec",
174
176
  "McpConfigError",
175
177
  "McpInstallation",
176
178
  "McpMechanism",
@@ -151,6 +151,44 @@ class EndpointSpec:
151
151
  credentials: frozenset[CredentialKind] = frozenset({CredentialKind.API_KEY})
152
152
 
153
153
 
154
+ @dataclass(frozen=True)
155
+ class LoginSpec:
156
+ """How a person signs a CLI in, as the CLI itself does it.
157
+
158
+ ``argv`` follows the binary and starts the CLI's own subscription login
159
+ (a browser page, or a URL to open). ``device_argv`` is the variant for a
160
+ machine without a usable browser, where the CLI prints a code to enter on
161
+ another device, or ``None`` when the CLI has none. ``needs_terminal`` is
162
+ true when the command prompts the person (a menu, a pasted code), so it
163
+ must run attached to a terminal; false when it runs unattended and only
164
+ prints a URL. Whether a CLI is signed in is not here: the provider's
165
+ ``ProbeSpec.auth`` reads that, where the CLI has a status command.
166
+ """
167
+
168
+ argv: tuple[str, ...]
169
+ device_argv: tuple[str, ...] | None = None
170
+ needs_terminal: bool = False
171
+
172
+ def __post_init__(self) -> None:
173
+ """Reject an empty command."""
174
+ if not self.argv or (self.device_argv is not None and not self.device_argv):
175
+ msg = "LoginSpec commands need at least one argument"
176
+ raise ValueError(msg)
177
+
178
+ def command(self, binary: str, *, device: bool = False) -> tuple[str, ...]:
179
+ """The full command line, with *binary* first.
180
+
181
+ Raises:
182
+ ValueError: *device* is asked for and the CLI has no device login.
183
+ """
184
+ if not device:
185
+ return (binary, *self.argv)
186
+ if self.device_argv is None:
187
+ msg = f"{binary} has no device login"
188
+ raise ValueError(msg)
189
+ return (binary, *self.device_argv)
190
+
191
+
154
192
  @dataclass(frozen=True)
155
193
  class AuthPolicy:
156
194
  """Which credentials a caller allows, and the endpoint to use.
@@ -30,6 +30,14 @@ class ModelSpec:
30
30
  that sits on no tier ladder.
31
31
  tier_name: The tier's readable name (``"Opus"``); set together with
32
32
  ``tier``.
33
+ generation_id: A short stable token naming the model's generation
34
+ within its vendor (``"5.5"``); every model of one generation
35
+ shares it, and no two generations of a vendor do.
36
+ generation: The generation as people call it (``"Claude 5.5"``,
37
+ ``"GPT-6"``); set together with ``generation_id``.
38
+ generation_rank: The generation's rank among its vendor's, ``1`` the
39
+ newest; set together with ``generation_id``. ``ModelTable``
40
+ checks that one vendor's generations have distinct ranks.
33
41
  """
34
42
 
35
43
  vendor: str
@@ -41,6 +49,9 @@ class ModelSpec:
41
49
  family: str | None = None
42
50
  tier: int | None = None
43
51
  tier_name: str | None = None
52
+ generation_id: str | None = None
53
+ generation: str | None = None
54
+ generation_rank: int | None = None
44
55
 
45
56
  def __post_init__(self) -> None:
46
57
  """Reject a missing name, source or date, and a window that is not a positive int."""
@@ -62,6 +73,26 @@ class ModelSpec:
62
73
  msg = f"{self.vendor}/{self.model}: context_window must be a positive int"
63
74
  raise ValueError(msg)
64
75
  self._check_presentation()
76
+ self._check_generation()
77
+
78
+ def _check_generation(self) -> None:
79
+ """Reject a half-set generation triple, a blank name, and a rank that is not a positive int."""
80
+ parts = (self.generation_id, self.generation, self.generation_rank)
81
+ if any(part is None for part in parts) and any(part is not None for part in parts):
82
+ msg = (
83
+ f"{self.vendor}/{self.model}: "
84
+ "generation_id, generation and generation_rank are set together"
85
+ )
86
+ raise ValueError(msg)
87
+ for name in ("generation_id", "generation"):
88
+ value: object = getattr(self, name)
89
+ if value is not None and (not isinstance(value, str) or not value.strip()):
90
+ msg = f"{self.vendor}/{self.model}: {name} must be a non-empty string"
91
+ raise ValueError(msg)
92
+ rank = cast("object", self.generation_rank)
93
+ if rank is not None and (isinstance(rank, bool) or not isinstance(rank, int) or rank < 1):
94
+ msg = f"{self.vendor}/{self.model}: generation_rank must be a positive int"
95
+ raise ValueError(msg)
65
96
 
66
97
  def _check_presentation(self) -> None:
67
98
  """Reject a half-set name pair or tier pair, and a tier that is not a positive int."""
@@ -125,6 +156,31 @@ class ModelTable:
125
156
  f"after the table's last_updated {self.last_updated}"
126
157
  )
127
158
  raise ValueError(msg)
159
+ self._check_generations()
160
+
161
+ def _check_generations(self) -> None:
162
+ """Reject a vendor whose generations disagree on a name or rank, or share a rank."""
163
+ by_id: dict[tuple[str, str], tuple[str | None, int | None]] = {}
164
+ by_rank: dict[tuple[str, int], str] = {}
165
+ for entry in self.entries:
166
+ if entry.generation_id is None or entry.generation_rank is None:
167
+ continue
168
+ known = by_id.setdefault(
169
+ (entry.vendor, entry.generation_id), (entry.generation, entry.generation_rank)
170
+ )
171
+ if known != (entry.generation, entry.generation_rank):
172
+ msg = (
173
+ f"{entry.vendor}/{entry.model}: generation {entry.generation_id!r} "
174
+ "has another name or rank on another model"
175
+ )
176
+ raise ValueError(msg)
177
+ holder = by_rank.setdefault((entry.vendor, entry.generation_rank), entry.generation_id)
178
+ if holder != entry.generation_id:
179
+ msg = (
180
+ f"{entry.vendor}: generations {holder!r} and {entry.generation_id!r} "
181
+ f"share rank {entry.generation_rank}"
182
+ )
183
+ raise ValueError(msg)
128
184
 
129
185
  def get(self, vendor: str, model: str) -> ModelSpec | None:
130
186
  """The entry for exactly ``(vendor, model)``, or ``None``."""
@@ -16,7 +16,7 @@ from .permissions import NativeMode
16
16
  if TYPE_CHECKING:
17
17
  from collections.abc import Mapping
18
18
 
19
- from .auth import CredentialSource, EndpointSpec, SettingsSpec
19
+ from .auth import CredentialSource, EndpointSpec, LoginSpec, SettingsSpec
20
20
 
21
21
 
22
22
  class McpMechanism(Enum):
@@ -242,6 +242,10 @@ class ProviderProfile:
242
242
  #: agentshim knows of none: a caller may still pass any model, at its own
243
243
  #: risk. A model's facts (its context window) are in ``model_spec``.
244
244
  models: tuple[str, ...] = ()
245
+ #: The CLI's own command for a person to sign in (0.21.0+), or ``None``
246
+ #: when it has none (Gemini CLI signs in on its first interactive run).
247
+ #: Signed-in state is read by the provider's ``ProbeSpec``.
248
+ login: LoginSpec | None = None
245
249
 
246
250
  @property
247
251
  def default_model(self) -> str | None:
@@ -88,4 +88,4 @@ def default_models(*, catalog: ProviderCatalog | None = None) -> ModelTable:
88
88
  by_key[entry.key] = entry
89
89
  entries = tuple(by_key.values())
90
90
  updated = max((entry.checked for entry in entries), default=dt.date(2026, 10, 17))
91
- return ModelTable(version="2026-10-17.1", last_updated=updated, entries=entries)
91
+ return ModelTable(version="2026-10-18.1", last_updated=updated, entries=entries)
@@ -16,6 +16,7 @@ _PRICE_CHECKED = dt.date(2026, 9, 27)
16
16
  def _anthropic_model(
17
17
  model: str, context_window: int, tier: tuple[int, str], version: str
18
18
  ) -> ModelSpec:
19
+ generation_id, generation_rank = _GENERATIONS[version]
19
20
  return ModelSpec(
20
21
  vendor="anthropic",
21
22
  model=model,
@@ -26,9 +27,18 @@ def _anthropic_model(
26
27
  family="Claude",
27
28
  tier=tier[0],
28
29
  tier_name=tier[1],
30
+ generation_id=generation_id,
31
+ generation=f"Claude {version}",
32
+ generation_rank=generation_rank,
29
33
  )
30
34
 
31
35
 
36
+ #: Claude's generations by version, newest first: ``(generation_id, rank)``.
37
+ _GENERATIONS = {
38
+ version: (version, rank)
39
+ for rank, version in enumerate(("5.5", "5.1", "5", "4.8", "4.7", "4.6", "4.5"), start=1)
40
+ }
41
+
32
42
  _HAIKU = (1, "Haiku")
33
43
  _SONNET = (2, "Sonnet")
34
44
  _OPUS = (3, "Opus")
@@ -11,6 +11,7 @@ from agentshim.core.auth import (
11
11
  EnvCredential,
12
12
  ExternalCredential,
13
13
  FileCredential,
14
+ LoginSpec,
14
15
  SettingsSpec,
15
16
  )
16
17
  from agentshim.core.errors import FailureKind, ProviderCapabilityError, SessionResumeError
@@ -83,6 +84,9 @@ PROFILE = ProviderProfile(
83
84
  "claude-haiku-4-5",
84
85
  ),
85
86
  display_name="Claude Code",
87
+ # ``claude auth login`` opens a browser page and, when none opens, asks for a
88
+ # pasted code, so it needs a terminal. It has no device-code variant.
89
+ login=LoginSpec(argv=("auth", "login"), needs_terminal=True),
86
90
  binary="claude",
87
91
  supports_resume=True,
88
92
  supports_reasoning_effort=True,
@@ -15,8 +15,10 @@ def _openai_model(
15
15
  model: str,
16
16
  context_window: int,
17
17
  display_name: str,
18
+ version: str,
18
19
  tier: tuple[int, str] | None = None,
19
20
  ) -> ModelSpec:
21
+ generation_id, generation_rank = _GENERATIONS[version]
20
22
  return ModelSpec(
21
23
  vendor="openai",
22
24
  model=model,
@@ -27,9 +29,17 @@ def _openai_model(
27
29
  family="GPT",
28
30
  tier=None if tier is None else tier[0],
29
31
  tier_name=None if tier is None else tier[1],
32
+ generation_id=generation_id,
33
+ generation=f"GPT-{version}",
34
+ generation_rank=generation_rank,
30
35
  )
31
36
 
32
37
 
38
+ #: GPT's generations by version, newest first: ``(generation_id, rank)``.
39
+ _GENERATIONS = {
40
+ version: (version, rank) for rank, version in enumerate(("6.1", "6", "5.6", "5.5"), start=1)
41
+ }
42
+
33
43
  _LUNA = (1, "Luna")
34
44
  _TERRA = (2, "Terra")
35
45
  _SOL = (3, "Sol")
@@ -37,14 +47,14 @@ _ASTRA = (4, "Astra")
37
47
  _OPENAI_1_05M = 1_050_000
38
48
 
39
49
  MODEL_ENTRIES = (
40
- _openai_model("gpt-6-astra", _OPENAI_1_05M, "GPT-6 Astra", _ASTRA),
41
- _openai_model("gpt-6.1-sol", _OPENAI_1_05M, "GPT-6.1 Sol", _SOL),
42
- _openai_model("gpt-6-sol", _OPENAI_1_05M, "GPT-6 Sol", _SOL),
43
- _openai_model("gpt-6-luna", _OPENAI_1_05M, "GPT-6 Luna", _LUNA),
44
- _openai_model("gpt-5.6-sol", _OPENAI_1_05M, "GPT-5.6 Sol", _SOL),
45
- _openai_model("gpt-5.6-terra", _OPENAI_1_05M, "GPT-5.6 Terra", _TERRA),
46
- _openai_model("gpt-5.6-luna", _OPENAI_1_05M, "GPT-5.6 Luna", _LUNA),
47
- _openai_model("gpt-5.5", _OPENAI_1_05M, "GPT-5.5"),
50
+ _openai_model("gpt-6-astra", _OPENAI_1_05M, "GPT-6 Astra", "6", _ASTRA),
51
+ _openai_model("gpt-6.1-sol", _OPENAI_1_05M, "GPT-6.1 Sol", "6.1", _SOL),
52
+ _openai_model("gpt-6-sol", _OPENAI_1_05M, "GPT-6 Sol", "6", _SOL),
53
+ _openai_model("gpt-6-luna", _OPENAI_1_05M, "GPT-6 Luna", "6", _LUNA),
54
+ _openai_model("gpt-5.6-sol", _OPENAI_1_05M, "GPT-5.6 Sol", "5.6", _SOL),
55
+ _openai_model("gpt-5.6-terra", _OPENAI_1_05M, "GPT-5.6 Terra", "5.6", _TERRA),
56
+ _openai_model("gpt-5.6-luna", _OPENAI_1_05M, "GPT-5.6 Luna", "5.6", _LUNA),
57
+ _openai_model("gpt-5.5", _OPENAI_1_05M, "GPT-5.5", "5.5"),
48
58
  )
49
59
 
50
60
  _OPENAI_NOTE = (
@@ -11,6 +11,7 @@ from agentshim.core.auth import (
11
11
  EndpointSpec,
12
12
  EnvCredential,
13
13
  FileCredential,
14
+ LoginSpec,
14
15
  classify_json_field,
15
16
  )
16
17
  from agentshim.core.errors import ProviderCapabilityError, SessionResumeError
@@ -87,6 +88,9 @@ PROFILE = ProviderProfile(
87
88
  ),
88
89
  display_name="Codex",
89
90
  binary="codex",
91
+ # ``codex login`` prints a URL and waits for the browser redirect on a local
92
+ # port; ``--device-auth`` prints a code to enter on another device instead.
93
+ login=LoginSpec(argv=("login",), device_argv=("login", "--device-auth")),
90
94
  supports_resume=True,
91
95
  supports_reasoning_effort=True,
92
96
  mcp=McpMechanism.CLI_FLAGS,
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import json
6
6
  from typing import TYPE_CHECKING, Any
7
7
 
8
- from agentshim.core.auth import CredentialKind, EnvCredential, FileCredential
8
+ from agentshim.core.auth import CredentialKind, EnvCredential, FileCredential, LoginSpec
9
9
  from agentshim.core.errors import ProviderCapabilityError
10
10
  from agentshim.core.mcp import FlagsInstallation, HttpMcpServer, NoopInstallation
11
11
  from agentshim.core.profile import McpMechanism, OutputSchemaStyle, ProviderProfile
@@ -28,6 +28,8 @@ PROFILE = ProviderProfile(
28
28
  name="copilot",
29
29
  display_name="Copilot CLI",
30
30
  binary="copilot",
31
+ # ``copilot login`` runs GitHub's device flow: a code to enter at a URL.
32
+ login=LoginSpec(argv=("login",)),
31
33
  supports_resume=True,
32
34
  # ``--effort`` exists on the CLI but the provider does not expose it: the
33
35
  # levels are model-specific and unvalidated, so a request naming one is
@@ -19,6 +19,9 @@ def _google(model: str, context_window: int, display_name: str, tier: tuple[int,
19
19
  family="Gemini",
20
20
  tier=tier[0],
21
21
  tier_name=tier[1],
22
+ generation_id="3",
23
+ generation="Gemini 3",
24
+ generation_rank=1,
22
25
  )
23
26
 
24
27
 
@@ -17,6 +17,7 @@ from __future__ import annotations
17
17
 
18
18
  from typing import TYPE_CHECKING, Any
19
19
 
20
+ from agentshim.core.auth import LoginSpec
20
21
  from agentshim.core.errors import ProviderCapabilityError, SessionResumeError
21
22
  from agentshim.core.mcp import (
22
23
  HttpMcpServer,
@@ -52,6 +53,8 @@ PROFILE = ProviderProfile(
52
53
  models=("google-vertex/gemini-3-pro-preview",),
53
54
  display_name="opencode",
54
55
  binary="opencode",
56
+ # ``opencode auth login`` prompts for a provider and its credential.
57
+ login=LoginSpec(argv=("auth", "login"), needs_terminal=True),
55
58
  supports_resume=True,
56
59
  # ``run --session <id>`` continues a stored session.
57
60
  supports_reasoning_effort=False,
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentshim"
3
- version = "0.20.0"
3
+ version = "0.21.0"
4
4
  description = "Provider-agnostic coding-agent CLI shims"
5
5
  readme = "README.md"
6
6
  license = "MIT"
File without changes
File without changes
File without changes