agentshim 0.18.3__tar.gz → 0.20.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 (139) hide show
  1. {agentshim-0.18.3 → agentshim-0.20.0}/CHANGELOG.md +29 -0
  2. {agentshim-0.18.3 → agentshim-0.20.0}/PKG-INFO +1 -1
  3. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/__init__.py +29 -14
  4. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/agent.py +6 -4
  5. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/core/__init__.py +7 -7
  6. agentshim-0.20.0/agentshim/core/models.py +153 -0
  7. agentshim-0.20.0/agentshim/core/plugin.py +172 -0
  8. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/core/pricing.py +2 -159
  9. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/core/usage.py +1 -1
  10. agentshim-0.20.0/agentshim/execution/readiness.py +76 -0
  11. agentshim-0.20.0/agentshim/models.py +91 -0
  12. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/oneshot.py +9 -2
  13. agentshim-0.20.0/agentshim/pricing.py +109 -0
  14. agentshim-0.20.0/agentshim/probe.py +73 -0
  15. agentshim-0.20.0/agentshim/providers/__init__.py +139 -0
  16. agentshim-0.20.0/agentshim/providers/_testing.py +25 -0
  17. agentshim-0.20.0/agentshim/providers/catalog.py +150 -0
  18. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/claude/__init__.py +2 -0
  19. agentshim-0.20.0/agentshim/providers/claude/model_data.py +85 -0
  20. agentshim-0.20.0/agentshim/providers/claude/plugin.py +32 -0
  21. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/claude/provider.py +16 -10
  22. agentshim-0.20.0/agentshim/providers/claude/testing.py +48 -0
  23. agentshim-0.20.0/agentshim/providers/claude/tools.py +113 -0
  24. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/codex/app_server/transport.py +2 -2
  25. agentshim-0.20.0/agentshim/providers/codex/model_data.py +96 -0
  26. agentshim-0.20.0/agentshim/providers/codex/plugin.py +32 -0
  27. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/codex/provider.py +1 -1
  28. agentshim-0.20.0/agentshim/providers/codex/testing.py +37 -0
  29. agentshim-0.20.0/agentshim/providers/copilot/plugin.py +24 -0
  30. agentshim-0.20.0/agentshim/providers/copilot/testing.py +28 -0
  31. agentshim-0.20.0/agentshim/providers/gemini/plugin.py +24 -0
  32. agentshim-0.20.0/agentshim/providers/gemini/testing.py +42 -0
  33. agentshim-0.20.0/agentshim/providers/opencode/model_data.py +28 -0
  34. agentshim-0.20.0/agentshim/providers/opencode/plugin.py +27 -0
  35. agentshim-0.20.0/agentshim/providers/opencode/testing.py +41 -0
  36. agentshim-0.20.0/agentshim/providers/static_plugin.py +162 -0
  37. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/runtime.py +17 -3
  38. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/testing/__init__.py +39 -89
  39. agentshim-0.20.0/agentshim/testing/probe.py +34 -0
  40. {agentshim-0.18.3 → agentshim-0.20.0}/pyproject.toml +1 -1
  41. agentshim-0.18.3/agentshim/core/models.py +0 -273
  42. agentshim-0.18.3/agentshim/probe.py +0 -117
  43. agentshim-0.18.3/agentshim/providers/__init__.py +0 -273
  44. agentshim-0.18.3/agentshim/testing/probe.py +0 -63
  45. {agentshim-0.18.3 → agentshim-0.20.0}/.gitignore +0 -0
  46. {agentshim-0.18.3 → agentshim-0.20.0}/LICENSE +0 -0
  47. {agentshim-0.18.3 → agentshim-0.20.0}/README.md +0 -0
  48. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/core/_files.py +0 -0
  49. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/core/auth.py +0 -0
  50. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/core/checkpoints.py +0 -0
  51. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/core/clock.py +0 -0
  52. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/core/conversation.py +0 -0
  53. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/core/env.py +0 -0
  54. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/core/errors.py +0 -0
  55. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/core/events.py +0 -0
  56. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/core/home.py +0 -0
  57. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/core/ids.py +0 -0
  58. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/core/mcp.py +0 -0
  59. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/core/permissions.py +0 -0
  60. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/core/profile.py +0 -0
  61. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/core/provider.py +0 -0
  62. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/core/schema.py +0 -0
  63. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/core/session_policy.py +0 -0
  64. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/core/skills.py +0 -0
  65. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/core/status.py +0 -0
  66. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/core/stream.py +0 -0
  67. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/core/turn.py +0 -0
  68. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/execution/__init__.py +0 -0
  69. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/execution/confinement.py +0 -0
  70. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/execution/docker.py +0 -0
  71. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/execution/executor.py +0 -0
  72. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/execution/host.py +0 -0
  73. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/execution/process.py +0 -0
  74. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/execution/transform.py +0 -0
  75. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/claude/events.py +0 -0
  76. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/claude/failures.py +0 -0
  77. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/claude/hooks/__init__.py +0 -0
  78. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/claude/hooks/confine_reads.py +0 -0
  79. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/claude/parser.py +0 -0
  80. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/claude/probe.py +0 -0
  81. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/claude/rate_limits.py +0 -0
  82. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/claude/sandbox.py +0 -0
  83. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/claude/scripted.py +0 -0
  84. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/claude/stream_argv.py +0 -0
  85. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/claude/stream_transport.py +0 -0
  86. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/claude/user_hooks.py +0 -0
  87. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/codex/__init__.py +0 -0
  88. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/codex/_toml.py +0 -0
  89. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/codex/app_server/__init__.py +0 -0
  90. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/codex/app_server/_wire.py +0 -0
  91. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/codex/app_server/approvals.py +0 -0
  92. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/codex/app_server/channel.py +0 -0
  93. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/codex/app_server/errors.py +0 -0
  94. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/codex/app_server/items.py +0 -0
  95. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/codex/app_server/mcp.py +0 -0
  96. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/codex/app_server/permissions.py +0 -0
  97. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/codex/app_server/profile.py +0 -0
  98. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/codex/app_server/protocol.py +0 -0
  99. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/codex/app_server/rate_limits.py +0 -0
  100. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/codex/app_server/usage.py +0 -0
  101. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/codex/events.py +0 -0
  102. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/codex/failures.py +0 -0
  103. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/codex/mcp_scope.py +0 -0
  104. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/codex/parser.py +0 -0
  105. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/codex/probe.py +0 -0
  106. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/codex/rules.py +0 -0
  107. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/codex/sandbox.py +0 -0
  108. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/codex/scripted.py +0 -0
  109. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/codex/skills.py +0 -0
  110. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/copilot/__init__.py +0 -0
  111. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/copilot/events.py +0 -0
  112. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/copilot/parser.py +0 -0
  113. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/copilot/probe.py +0 -0
  114. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/copilot/provider.py +0 -0
  115. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/copilot/scripted.py +0 -0
  116. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/gemini/__init__.py +0 -0
  117. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/gemini/events.py +0 -0
  118. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/gemini/parser.py +0 -0
  119. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/gemini/probe.py +0 -0
  120. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/gemini/provider.py +0 -0
  121. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/gemini/scripted.py +0 -0
  122. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/opencode/__init__.py +0 -0
  123. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/opencode/events.py +0 -0
  124. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/opencode/parser.py +0 -0
  125. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/opencode/probe.py +0 -0
  126. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/opencode/provider.py +0 -0
  127. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/providers/opencode/scripted.py +0 -0
  128. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/py.typed +0 -0
  129. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/session.py +0 -0
  130. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/testing/claude_stream.py +0 -0
  131. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/testing/clock.py +0 -0
  132. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/testing/codex_app_server.py +0 -0
  133. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/testing/confinement.py +0 -0
  134. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/testing/contracts.py +0 -0
  135. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/testing/conversation.py +0 -0
  136. {agentshim-0.18.3 → agentshim-0.20.0}/agentshim/testing/process.py +0 -0
  137. {agentshim-0.18.3 → agentshim-0.20.0}/tests/fixtures/claude_stream/README.md +0 -0
  138. {agentshim-0.18.3 → agentshim-0.20.0}/tests/fixtures/codex/README.md +0 -0
  139. {agentshim-0.18.3 → agentshim-0.20.0}/tests/fixtures/codex_app_server/README.md +0 -0
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.20.0 (2026-10-18)
4
+
5
+ ### Added
6
+
7
+ - `ClaudeProvider(tools=ClaudeToolPolicy(...))`: run Claude Code with only the built-in tools
8
+ and allow rules the policy names, under `--permission-mode dontAsk`, instead of the
9
+ always-on `--dangerously-skip-permissions`. `ClaudeToolPolicy.only(["mcp__rs"])` gives a
10
+ turn no built-in tools and lets it call only that MCP server; `builtin=()` renders
11
+ `--tools ""`, and each allow rule is its own `--allowedTools` argument so a scoped rule
12
+ such as `Bash(git log:*)` arrives whole. A turn with an output schema also allows
13
+ `StructuredOutput`. Without a policy the argv is unchanged.
14
+
15
+ ### Changed
16
+
17
+ - Provider integrations are resolved through an extensible `ProviderCatalog`; `CliAgent`,
18
+ `Agent`, `probe_provider` and the consumer test helpers take `catalog=` (#70).
19
+
20
+ ## 0.19.0 (2026-10-17)
21
+
22
+ ### Added
23
+
24
+ - `ModelSpec` carries what a model picker needs: `display_name` (`"Opus 5.5"`), `family`
25
+ (`"Claude"`, `"GPT"`, `"Gemini"`), and a within-family ladder, `tier` (a rank, `1` the
26
+ smallest) with `tier_name` (`"Opus"`). Haiku < Sonnet < Opus < Fable; Luna < Terra < Sol <
27
+ Astra. All four fields default to `None`, so a caller-built `ModelSpec` keeps working;
28
+ every shipped entry has a name and family, and every one but `gpt-5.5` a tier.
29
+ Read them with `model_spec(provider, model)`. A provider's readable name was already
30
+ `ProviderProfile.display_name`.
31
+
3
32
  ## 0.18.3 (2026-10-16)
4
33
 
5
34
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentshim
3
- Version: 0.18.3
3
+ Version: 0.20.0
4
4
  Summary: Provider-agnostic coding-agent CLI shims
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -29,6 +29,7 @@ from .core import (
29
29
  CompositeEventHandler,
30
30
  ConfigFileInstallation,
31
31
  ConfigScope,
32
+ ConnectionOptions,
32
33
  ConsoleEventHandler,
33
34
  ContextualStreamParser,
34
35
  Continuity,
@@ -56,9 +57,6 @@ from .core import (
56
57
  McpScope,
57
58
  McpServer,
58
59
  McpTransport,
59
- ModelPricing,
60
- ModelSpec,
61
- ModelTable,
62
60
  NativeMode,
63
61
  NativePermissions,
64
62
  NoopInstallation,
@@ -69,7 +67,7 @@ from .core import (
69
67
  ParsedTurn,
70
68
  ParserContext,
71
69
  PolicyConfig,
72
- PricingTable,
70
+ PricingContext,
73
71
  ProcessClosedError,
74
72
  Provider,
75
73
  ProviderCapabilityError,
@@ -126,22 +124,16 @@ from .core import (
126
124
  UsageReport,
127
125
  compact_json,
128
126
  compose_event_handlers,
129
- context_window,
130
- cost_usd,
131
- default_models,
132
- default_pricing,
133
127
  dialect_problems,
134
128
  host_auth_plan,
135
129
  install_config_file,
136
130
  interactive_env,
137
131
  materialize,
138
- model_spec,
139
132
  normalize,
140
133
  normalized_usage,
141
134
  parse_json_object,
142
135
  plan_auth,
143
136
  prepare_config_home,
144
- price_for,
145
137
  )
146
138
  from .execution import (
147
139
  CallbackCommandStreamSink,
@@ -164,10 +156,26 @@ from .execution import (
164
156
  TransformingExecutor,
165
157
  confine,
166
158
  )
159
+ from .models import ModelSpec, ModelTable, context_window, default_models, model_spec
167
160
  from .oneshot import OneShotTransport
161
+ from .pricing import ModelPricing, PricingTable, cost_usd, default_pricing, price_for
168
162
  from .probe import probe_provider
169
- from .providers import get_provider, provider_names, stream_provider_names
170
- from .providers.claude import ClaudeHook, ClaudeProvider, ClaudeStreamTransport, SandboxConfig
163
+ from .providers import (
164
+ DEFAULT_CATALOG,
165
+ ProviderCatalog,
166
+ ProviderPlugin,
167
+ ProviderTestSupport,
168
+ get_provider,
169
+ provider_names,
170
+ stream_provider_names,
171
+ )
172
+ from .providers.claude import (
173
+ ClaudeHook,
174
+ ClaudeProvider,
175
+ ClaudeStreamTransport,
176
+ ClaudeToolPolicy,
177
+ SandboxConfig,
178
+ )
171
179
  from .providers.codex import CodexProvider, CodexSandboxConfig
172
180
  from .providers.codex.app_server import CodexAppServerTransport
173
181
  from .providers.copilot import CopilotProvider
@@ -176,9 +184,9 @@ from .providers.opencode import OpencodeProvider
176
184
  from .runtime import Agent
177
185
  from .session import Session, Turn, TurnTicket, map_mcp_servers
178
186
 
179
- __version__ = "0.18.3"
187
+ __version__ = "0.20.0"
180
188
 
181
- __all__ = [
189
+ __all__ = [ # noqa: RUF022 - public API test uses Python's case-sensitive order
182
190
  "Agent",
183
191
  "AgentEvent",
184
192
  "AgentEventHandler",
@@ -198,6 +206,7 @@ __all__ = [
198
206
  "ClaudeHook",
199
207
  "ClaudeProvider",
200
208
  "ClaudeStreamTransport",
209
+ "ClaudeToolPolicy",
201
210
  "CliAgent",
202
211
  "CliCheckError",
203
212
  "CliExitError",
@@ -216,6 +225,7 @@ __all__ = [
216
225
  "ConfigFileInstallation",
217
226
  "ConfigScope",
218
227
  "Confinement",
228
+ "ConnectionOptions",
219
229
  "ConsoleEventHandler",
220
230
  "ContextualStreamParser",
221
231
  "Continuity",
@@ -225,6 +235,7 @@ __all__ = [
225
235
  "CopilotProvider",
226
236
  "CredentialKind",
227
237
  "CredentialSource",
238
+ "DEFAULT_CATALOG",
228
239
  "DockerExecConfinement",
229
240
  "EndpointSpec",
230
241
  "EnvCredential",
@@ -264,6 +275,7 @@ __all__ = [
264
275
  "ParsedTurn",
265
276
  "ParserContext",
266
277
  "PolicyConfig",
278
+ "PricingContext",
267
279
  "PricingTable",
268
280
  "Process",
269
281
  "ProcessClosedError",
@@ -271,9 +283,12 @@ __all__ = [
271
283
  "ProcessOutput",
272
284
  "Provider",
273
285
  "ProviderCapabilityError",
286
+ "ProviderCatalog",
274
287
  "ProviderError",
288
+ "ProviderPlugin",
275
289
  "ProviderProfile",
276
290
  "ProviderStatus",
291
+ "ProviderTestSupport",
277
292
  "ProviderUsage",
278
293
  "RandomIds",
279
294
  "RateLimitStatus",
@@ -38,7 +38,7 @@ from agentshim.core.skills import SkillTracker
38
38
  from agentshim.core.turn import TurnRequest, TurnResult, coerce_request
39
39
  from agentshim.execution.executor import CommandRequest
40
40
  from agentshim.execution.host import HostCommandExecutor
41
- from agentshim.providers import get_provider
41
+ from agentshim.providers import DEFAULT_CATALOG, ProviderCatalog, get_provider
42
42
 
43
43
  if TYPE_CHECKING:
44
44
  from collections.abc import Callable, Mapping, Sequence
@@ -51,14 +51,14 @@ if TYPE_CHECKING:
51
51
  from agentshim.execution.executor import CommandExecutor, CommandHandle, CommandResult
52
52
 
53
53
 
54
- def _resolve_provider(provider: str | Provider) -> Provider:
54
+ def _resolve_provider(provider: str | Provider, catalog: ProviderCatalog) -> Provider:
55
55
  """Resolve a provider name to a provider instance.
56
56
 
57
57
  This module sits above ``providers`` precisely so the documented
58
58
  ``CliAgent("claude")`` spelling can be a plain module-level import.
59
59
  """
60
60
  if isinstance(provider, str):
61
- return get_provider(provider)
61
+ return get_provider(provider, catalog=catalog)
62
62
  return provider
63
63
 
64
64
 
@@ -82,6 +82,7 @@ class CliAgent:
82
82
  event_handlers: Sequence[AgentEventHandler] = (),
83
83
  check_timeout: float = 15.0,
84
84
  log: Callable[[str], None] | None = None,
85
+ catalog: ProviderCatalog = DEFAULT_CATALOG,
85
86
  ) -> None:
86
87
  """Configure a provider CLI and prove it is usable before any turn runs.
87
88
 
@@ -104,8 +105,9 @@ class CliAgent:
104
105
  check_timeout: Seconds allowed for the one-off health check. It is
105
106
  not a budget for a turn.
106
107
  log: Sink for agentshim's own progress lines; dropped by default.
108
+ catalog: Provider names and implementations to use; defaults to the built-ins.
107
109
  """
108
- self.provider: Provider = _resolve_provider(provider)
110
+ self.provider: Provider = _resolve_provider(provider, catalog)
109
111
  self.profile: ProviderProfile = self.provider.profile
110
112
  self.model = model
111
113
  self.executor: CommandExecutor = executor if executor is not None else HostCommandExecutor()
@@ -95,9 +95,10 @@ from .mcp import (
95
95
  StdioMcpServer,
96
96
  install_config_file,
97
97
  )
98
- from .models import ModelSpec, ModelTable, context_window, default_models, model_spec
98
+ from .models import ModelSpec, ModelTable
99
99
  from .permissions import ApprovalPolicy, NativeMode, NativePermissions
100
- from .pricing import ModelPricing, PricingTable, cost_usd, default_pricing, price_for
100
+ from .plugin import ConnectionOptions, PricingContext, ProviderPlugin, ProviderTestSupport
101
+ from .pricing import ModelPricing, PricingTable, cost_usd
101
102
  from .profile import (
102
103
  ConfigScope,
103
104
  McpMechanism,
@@ -148,6 +149,7 @@ __all__ = [
148
149
  "CompositeEventHandler",
149
150
  "ConfigFileInstallation",
150
151
  "ConfigScope",
152
+ "ConnectionOptions",
151
153
  "ConsoleEventHandler",
152
154
  "ContextualStreamParser",
153
155
  "Continuity",
@@ -188,13 +190,16 @@ __all__ = [
188
190
  "ParsedTurn",
189
191
  "ParserContext",
190
192
  "PolicyConfig",
193
+ "PricingContext",
191
194
  "PricingTable",
192
195
  "ProcessClosedError",
193
196
  "Provider",
194
197
  "ProviderCapabilityError",
195
198
  "ProviderError",
199
+ "ProviderPlugin",
196
200
  "ProviderProfile",
197
201
  "ProviderStatus",
202
+ "ProviderTestSupport",
198
203
  "ProviderUsage",
199
204
  "RandomIds",
200
205
  "RateLimitStatus",
@@ -245,20 +250,15 @@ __all__ = [
245
250
  "UsageReport",
246
251
  "compact_json",
247
252
  "compose_event_handlers",
248
- "context_window",
249
253
  "cost_usd",
250
- "default_models",
251
- "default_pricing",
252
254
  "dialect_problems",
253
255
  "host_auth_plan",
254
256
  "install_config_file",
255
257
  "interactive_env",
256
258
  "materialize",
257
- "model_spec",
258
259
  "normalize",
259
260
  "normalized_usage",
260
261
  "parse_json_object",
261
262
  "plan_auth",
262
263
  "prepare_config_home",
263
- "price_for",
264
264
  ]
@@ -0,0 +1,153 @@
1
+ """Provider-independent model facts and tables."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import datetime as dt
6
+ from dataclasses import dataclass, field, replace
7
+ from typing import TYPE_CHECKING, cast
8
+
9
+ if TYPE_CHECKING:
10
+ from collections.abc import Iterable
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class ModelSpec:
15
+ """One model's context window, with where and when it was read.
16
+
17
+ Attributes:
18
+ vendor: Who serves the model (``"openai"``, ``"anthropic"``).
19
+ model: The model id as the vendor names it.
20
+ context_window: The model's context window in tokens (input and
21
+ output together), at the vendor's default for that id.
22
+ source: URL of the official page the number was read from.
23
+ checked: Date the number was last checked against ``source``.
24
+ display_name: A readable name for the model (``"Opus 5.5"``), or
25
+ ``None`` for a caller-built entry that has none.
26
+ family: The model line the model belongs to (``"Claude"``, ``"GPT"``,
27
+ ``"Gemini"``); set together with ``display_name``.
28
+ tier: The model's rank within its family, ``1`` the smallest and
29
+ cheapest; versions of one tier share a rank. ``None`` for a model
30
+ that sits on no tier ladder.
31
+ tier_name: The tier's readable name (``"Opus"``); set together with
32
+ ``tier``.
33
+ """
34
+
35
+ vendor: str
36
+ model: str
37
+ context_window: int
38
+ source: str
39
+ checked: dt.date
40
+ display_name: str | None = None
41
+ family: str | None = None
42
+ tier: int | None = None
43
+ tier_name: str | None = None
44
+
45
+ def __post_init__(self) -> None:
46
+ """Reject a missing name, source or date, and a window that is not a positive int."""
47
+ for name in ("vendor", "model"):
48
+ value: object = getattr(self, name)
49
+ if not isinstance(value, str) or not value.strip():
50
+ msg = f"{name} must be a non-empty string"
51
+ raise ValueError(msg)
52
+ source = cast("object", self.source)
53
+ if not isinstance(source, str) or not source.startswith("https://"):
54
+ msg = f"{self.vendor}/{self.model}: source must be an https:// URL"
55
+ raise ValueError(msg)
56
+ checked = cast("object", self.checked)
57
+ if not isinstance(checked, dt.date) or isinstance(checked, dt.datetime):
58
+ msg = f"{self.vendor}/{self.model}: checked must be a datetime.date"
59
+ raise TypeError(msg)
60
+ window = cast("object", self.context_window)
61
+ if isinstance(window, bool) or not isinstance(window, int) or window <= 0:
62
+ msg = f"{self.vendor}/{self.model}: context_window must be a positive int"
63
+ raise ValueError(msg)
64
+ self._check_presentation()
65
+
66
+ def _check_presentation(self) -> None:
67
+ """Reject a half-set name pair or tier pair, and a tier that is not a positive int."""
68
+ for first, second in (("display_name", "family"), ("tier", "tier_name")):
69
+ if (getattr(self, first) is None) != (getattr(self, second) is None):
70
+ msg = f"{self.vendor}/{self.model}: {first} and {second} are set together"
71
+ raise ValueError(msg)
72
+ for name in ("display_name", "family", "tier_name"):
73
+ value: object = getattr(self, name)
74
+ if value is not None and (not isinstance(value, str) or not value.strip()):
75
+ msg = f"{self.vendor}/{self.model}: {name} must be a non-empty string"
76
+ raise ValueError(msg)
77
+ tier = cast("object", self.tier)
78
+ if tier is not None and (isinstance(tier, bool) or not isinstance(tier, int) or tier < 1):
79
+ msg = f"{self.vendor}/{self.model}: tier must be a positive int"
80
+ raise ValueError(msg)
81
+ if self.tier is not None and self.family is None:
82
+ msg = f"{self.vendor}/{self.model}: tier needs a family"
83
+ raise ValueError(msg)
84
+
85
+ @property
86
+ def key(self) -> tuple[str, str]:
87
+ """The ``(vendor, model)`` this entry describes."""
88
+ return (self.vendor, self.model)
89
+
90
+
91
+ @dataclass(frozen=True)
92
+ class ModelTable:
93
+ """A versioned set of :class:`ModelSpec` entries.
94
+
95
+ Attributes:
96
+ version: Names this set of facts; change it whenever a fact changes.
97
+ last_updated: Date the table as a whole was last updated. No entry
98
+ may have been checked later.
99
+ entries: One entry per ``(vendor, model)``.
100
+ """
101
+
102
+ version: str
103
+ last_updated: dt.date
104
+ entries: tuple[ModelSpec, ...] = field(default=())
105
+
106
+ def __post_init__(self) -> None:
107
+ """Reject a missing version, duplicate keys, and entries checked after ``last_updated``."""
108
+ version = cast("object", self.version)
109
+ if not isinstance(version, str) or not version.strip():
110
+ msg = "version must be a non-empty string"
111
+ raise ValueError(msg)
112
+ object.__setattr__(self, "entries", tuple(self.entries))
113
+ seen: set[tuple[str, str]] = set()
114
+ for entry in self.entries:
115
+ if not isinstance(cast("object", entry), ModelSpec):
116
+ msg = f"entries must be ModelSpec, got {entry!r}"
117
+ raise TypeError(msg)
118
+ if entry.key in seen:
119
+ msg = f"duplicate model entry for {entry.vendor}/{entry.model}"
120
+ raise ValueError(msg)
121
+ seen.add(entry.key)
122
+ if entry.checked > self.last_updated:
123
+ msg = (
124
+ f"{entry.vendor}/{entry.model} was checked {entry.checked}, "
125
+ f"after the table's last_updated {self.last_updated}"
126
+ )
127
+ raise ValueError(msg)
128
+
129
+ def get(self, vendor: str, model: str) -> ModelSpec | None:
130
+ """The entry for exactly ``(vendor, model)``, or ``None``."""
131
+ for entry in self.entries:
132
+ if entry.key == (vendor, model):
133
+ return entry
134
+ return None
135
+
136
+ def with_entries(
137
+ self,
138
+ entries: Iterable[ModelSpec],
139
+ *,
140
+ version: str,
141
+ last_updated: dt.date | None = None,
142
+ ) -> ModelTable:
143
+ """A new table with *entries* added, replacing any with the same key."""
144
+ added = tuple(entries)
145
+ by_key = {entry.key: entry for entry in self.entries}
146
+ by_key.update({entry.key: entry for entry in added})
147
+ latest = max([self.last_updated, *(entry.checked for entry in added)])
148
+ return replace(
149
+ self,
150
+ version=version,
151
+ last_updated=last_updated or latest,
152
+ entries=tuple(by_key.values()),
153
+ )
@@ -0,0 +1,172 @@
1
+ """Method-based contract implemented by built-in and third-party plugins."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
7
+
8
+ if TYPE_CHECKING:
9
+ import datetime as dt
10
+ from collections.abc import Callable, Mapping, Sequence
11
+ from pathlib import Path
12
+
13
+ from agentshim.execution.executor import CommandExecutor, CommandRequest
14
+
15
+ from .clock import Clock
16
+ from .conversation import Transport, TransportKind
17
+ from .errors import FailureKind
18
+ from .ids import IdAllocator
19
+ from .models import ModelSpec, ModelTable
20
+ from .pricing import ModelPricing, PricingTable
21
+ from .profile import ProviderProfile
22
+ from .provider import Provider
23
+ from .status import AuthState, ProviderStatus
24
+ from .usage import TokenUsage
25
+
26
+
27
+ class ScriptedLines(Protocol):
28
+ """Build one turn of a provider's real stdout for consumer tests."""
29
+
30
+ def __call__( # noqa: PLR0913
31
+ self,
32
+ *,
33
+ text: str = "",
34
+ session_id: str | None = None,
35
+ usage: TokenUsage | None = None,
36
+ tool_calls: Sequence[tuple[str, Mapping[str, Any], str]] = (),
37
+ structured_output: object | None = None,
38
+ skills_offered: Sequence[str] | None = None,
39
+ skills_invoked: Sequence[str] = (),
40
+ ) -> list[str]:
41
+ """Return stdout lines in this provider's stream format."""
42
+ ...
43
+
44
+
45
+ class ResumeFailureLines(Protocol):
46
+ """Build the stream and exit status of a failed resume."""
47
+
48
+ def __call__(
49
+ self, *, session_id: str | None = None
50
+ ) -> tuple[Sequence[str], Sequence[str], int]:
51
+ """Return stdout, stderr, and exit status."""
52
+ ...
53
+
54
+
55
+ class FailureLines(Protocol):
56
+ """Build the stream and exit status of a classified failure."""
57
+
58
+ def __call__(
59
+ self, kind: FailureKind, *, session_id: str | None = None
60
+ ) -> tuple[Sequence[str], Sequence[str], int]:
61
+ """Return stdout, stderr, and exit status."""
62
+ ...
63
+
64
+
65
+ class StreamTransportFactory(Protocol):
66
+ """Legacy type for provider-internal stream constructors."""
67
+
68
+ def __call__( # noqa: PLR0913
69
+ self,
70
+ *,
71
+ executor: CommandExecutor,
72
+ env: Mapping[str, str] | None,
73
+ clock: Clock,
74
+ ids: IdAllocator,
75
+ check_timeout: float,
76
+ log: Callable[[str], None] | None,
77
+ ) -> Transport:
78
+ """Create a stream transport."""
79
+ ...
80
+
81
+
82
+ class ProbeFixture(Protocol):
83
+ """Build fake CLI output for a readiness command."""
84
+
85
+ def __call__(
86
+ self, args: Sequence[str], *, version: str, auth: AuthState
87
+ ) -> tuple[str, str, int]:
88
+ """Return stdout, stderr, and exit status."""
89
+ ...
90
+
91
+
92
+ @dataclass(frozen=True)
93
+ class ProviderTestSupport:
94
+ """Optional provider-owned adapters for the generic test helpers."""
95
+
96
+ scripted_lines: ScriptedLines
97
+ resume_failure_lines: ResumeFailureLines
98
+ failure_lines: FailureLines | None = None
99
+ installed_mcp_servers: Callable[[CommandRequest, Path], dict[str, dict[str, Any]]] | None = None
100
+ probe_fixture: ProbeFixture | None = None
101
+
102
+
103
+ @dataclass(frozen=True)
104
+ class ConnectionOptions:
105
+ """Dependencies for a requested connection mode."""
106
+
107
+ kind: TransportKind
108
+ executor: CommandExecutor | None = None
109
+ env: Mapping[str, str] | None = None
110
+ clock: Clock | None = None
111
+ ids: IdAllocator | None = None
112
+ check_timeout: float = 15.0
113
+ log: Callable[[str], None] | None = None
114
+
115
+
116
+ @dataclass(frozen=True)
117
+ class PricingContext:
118
+ """Conditions under which a rate is requested; omitted values mean standard tier."""
119
+
120
+ as_of: dt.date | None = None
121
+ region: str | None = None
122
+ tier: str | None = None
123
+
124
+
125
+ @runtime_checkable
126
+ class ProviderPlugin(Protocol):
127
+ """Required methods for every provider; unsupported facts return ``None``."""
128
+
129
+ @property
130
+ def name(self) -> str:
131
+ """Stable catalog key."""
132
+ ...
133
+
134
+ def describe(self) -> ProviderProfile:
135
+ """Describe capabilities independently of local installation state."""
136
+ ...
137
+
138
+ def connection_modes(self) -> frozenset[TransportKind]:
139
+ """Declare which connection modes ``connect`` accepts."""
140
+ ...
141
+
142
+ def connect(self, options: ConnectionOptions) -> Provider | Transport:
143
+ """Create the requested one-shot provider or stream transport."""
144
+ ...
145
+
146
+ def probe(
147
+ self, executor: CommandExecutor, env: Mapping[str, str], timeout: float
148
+ ) -> ProviderStatus:
149
+ """Check readiness on the supplied executor, without running a model."""
150
+ ...
151
+
152
+ def model_info(self, model: str, table: ModelTable | None = None) -> ModelSpec | None:
153
+ """Resolve context-window facts; *table* overrides built-in facts."""
154
+ ...
155
+
156
+ def pricing_info(
157
+ self, model: str, context: PricingContext, table: PricingTable | None = None
158
+ ) -> ModelPricing | None:
159
+ """Resolve a rate for *model* and *context*, or report it unknown."""
160
+ ...
161
+
162
+ def known_models(self) -> tuple[ModelSpec, ...]:
163
+ """Enumerate facts available for a reproducible table snapshot."""
164
+ ...
165
+
166
+ def known_prices(self) -> tuple[ModelPricing, ...]:
167
+ """Enumerate standard-tier prices for a reproducible table snapshot."""
168
+ ...
169
+
170
+ def test_support(self) -> ProviderTestSupport | None:
171
+ """Return consumer-test adapters, if provided."""
172
+ ...