agentshim 0.19.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.
- {agentshim-0.19.0 → agentshim-0.21.0}/CHANGELOG.md +34 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/PKG-INFO +1 -1
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/__init__.py +31 -14
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/agent.py +6 -4
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/core/__init__.py +9 -7
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/core/auth.py +38 -0
- agentshim-0.21.0/agentshim/core/models.py +209 -0
- agentshim-0.21.0/agentshim/core/plugin.py +172 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/core/pricing.py +2 -159
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/core/profile.py +5 -1
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/core/usage.py +1 -1
- agentshim-0.21.0/agentshim/execution/readiness.py +76 -0
- agentshim-0.21.0/agentshim/models.py +91 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/oneshot.py +9 -2
- agentshim-0.21.0/agentshim/pricing.py +109 -0
- agentshim-0.21.0/agentshim/probe.py +73 -0
- agentshim-0.21.0/agentshim/providers/__init__.py +139 -0
- agentshim-0.21.0/agentshim/providers/_testing.py +25 -0
- agentshim-0.21.0/agentshim/providers/catalog.py +150 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/claude/__init__.py +2 -0
- agentshim-0.21.0/agentshim/providers/claude/model_data.py +95 -0
- agentshim-0.21.0/agentshim/providers/claude/plugin.py +32 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/claude/provider.py +20 -10
- agentshim-0.21.0/agentshim/providers/claude/testing.py +48 -0
- agentshim-0.21.0/agentshim/providers/claude/tools.py +113 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/codex/app_server/transport.py +2 -2
- agentshim-0.21.0/agentshim/providers/codex/model_data.py +106 -0
- agentshim-0.21.0/agentshim/providers/codex/plugin.py +32 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/codex/provider.py +5 -1
- agentshim-0.21.0/agentshim/providers/codex/testing.py +37 -0
- agentshim-0.21.0/agentshim/providers/copilot/plugin.py +24 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/copilot/provider.py +3 -1
- agentshim-0.21.0/agentshim/providers/copilot/testing.py +28 -0
- agentshim-0.21.0/agentshim/providers/gemini/plugin.py +24 -0
- agentshim-0.21.0/agentshim/providers/gemini/testing.py +42 -0
- agentshim-0.21.0/agentshim/providers/opencode/model_data.py +31 -0
- agentshim-0.21.0/agentshim/providers/opencode/plugin.py +27 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/opencode/provider.py +3 -0
- agentshim-0.21.0/agentshim/providers/opencode/testing.py +41 -0
- agentshim-0.21.0/agentshim/providers/static_plugin.py +162 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/runtime.py +17 -3
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/testing/__init__.py +39 -89
- agentshim-0.21.0/agentshim/testing/probe.py +34 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/pyproject.toml +1 -1
- agentshim-0.19.0/agentshim/core/models.py +0 -335
- agentshim-0.19.0/agentshim/probe.py +0 -117
- agentshim-0.19.0/agentshim/providers/__init__.py +0 -273
- agentshim-0.19.0/agentshim/testing/probe.py +0 -63
- {agentshim-0.19.0 → agentshim-0.21.0}/.gitignore +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/LICENSE +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/README.md +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/core/_files.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/core/checkpoints.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/core/clock.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/core/conversation.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/core/env.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/core/errors.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/core/events.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/core/home.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/core/ids.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/core/mcp.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/core/permissions.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/core/provider.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/core/schema.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/core/session_policy.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/core/skills.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/core/status.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/core/stream.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/core/turn.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/execution/__init__.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/execution/confinement.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/execution/docker.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/execution/executor.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/execution/host.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/execution/process.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/execution/transform.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/claude/events.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/claude/failures.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/claude/hooks/__init__.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/claude/hooks/confine_reads.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/claude/parser.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/claude/probe.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/claude/rate_limits.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/claude/sandbox.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/claude/scripted.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/claude/stream_argv.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/claude/stream_transport.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/claude/user_hooks.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/codex/__init__.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/codex/_toml.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/codex/app_server/__init__.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/codex/app_server/_wire.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/codex/app_server/approvals.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/codex/app_server/channel.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/codex/app_server/errors.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/codex/app_server/items.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/codex/app_server/mcp.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/codex/app_server/permissions.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/codex/app_server/profile.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/codex/app_server/protocol.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/codex/app_server/rate_limits.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/codex/app_server/usage.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/codex/events.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/codex/failures.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/codex/mcp_scope.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/codex/parser.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/codex/probe.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/codex/rules.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/codex/sandbox.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/codex/scripted.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/codex/skills.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/copilot/__init__.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/copilot/events.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/copilot/parser.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/copilot/probe.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/copilot/scripted.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/gemini/__init__.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/gemini/events.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/gemini/parser.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/gemini/probe.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/gemini/provider.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/gemini/scripted.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/opencode/__init__.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/opencode/events.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/opencode/parser.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/opencode/probe.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/providers/opencode/scripted.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/py.typed +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/session.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/testing/claude_stream.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/testing/clock.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/testing/codex_app_server.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/testing/confinement.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/testing/contracts.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/testing/conversation.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/agentshim/testing/process.py +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/tests/fixtures/claude_stream/README.md +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/tests/fixtures/codex/README.md +0 -0
- {agentshim-0.19.0 → agentshim-0.21.0}/tests/fixtures/codex_app_server/README.md +0 -0
|
@@ -1,5 +1,39 @@
|
|
|
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
|
+
|
|
20
|
+
## 0.20.0 (2026-10-18)
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- `ClaudeProvider(tools=ClaudeToolPolicy(...))`: run Claude Code with only the built-in tools
|
|
25
|
+
and allow rules the policy names, under `--permission-mode dontAsk`, instead of the
|
|
26
|
+
always-on `--dangerously-skip-permissions`. `ClaudeToolPolicy.only(["mcp__rs"])` gives a
|
|
27
|
+
turn no built-in tools and lets it call only that MCP server; `builtin=()` renders
|
|
28
|
+
`--tools ""`, and each allow rule is its own `--allowedTools` argument so a scoped rule
|
|
29
|
+
such as `Bash(git log:*)` arrives whole. A turn with an output schema also allows
|
|
30
|
+
`StructuredOutput`. Without a policy the argv is unchanged.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- Provider integrations are resolved through an extensible `ProviderCatalog`; `CliAgent`,
|
|
35
|
+
`Agent`, `probe_provider` and the consumer test helpers take `catalog=` (#70).
|
|
36
|
+
|
|
3
37
|
## 0.19.0 (2026-10-17)
|
|
4
38
|
|
|
5
39
|
### Added
|
|
@@ -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,
|
|
@@ -50,15 +51,13 @@ from .core import (
|
|
|
50
51
|
IdAllocator,
|
|
51
52
|
InMemoryCheckpointStore,
|
|
52
53
|
Lifecycle,
|
|
54
|
+
LoginSpec,
|
|
53
55
|
McpConfigError,
|
|
54
56
|
McpInstallation,
|
|
55
57
|
McpMechanism,
|
|
56
58
|
McpScope,
|
|
57
59
|
McpServer,
|
|
58
60
|
McpTransport,
|
|
59
|
-
ModelPricing,
|
|
60
|
-
ModelSpec,
|
|
61
|
-
ModelTable,
|
|
62
61
|
NativeMode,
|
|
63
62
|
NativePermissions,
|
|
64
63
|
NoopInstallation,
|
|
@@ -69,7 +68,7 @@ from .core import (
|
|
|
69
68
|
ParsedTurn,
|
|
70
69
|
ParserContext,
|
|
71
70
|
PolicyConfig,
|
|
72
|
-
|
|
71
|
+
PricingContext,
|
|
73
72
|
ProcessClosedError,
|
|
74
73
|
Provider,
|
|
75
74
|
ProviderCapabilityError,
|
|
@@ -126,22 +125,16 @@ from .core import (
|
|
|
126
125
|
UsageReport,
|
|
127
126
|
compact_json,
|
|
128
127
|
compose_event_handlers,
|
|
129
|
-
context_window,
|
|
130
|
-
cost_usd,
|
|
131
|
-
default_models,
|
|
132
|
-
default_pricing,
|
|
133
128
|
dialect_problems,
|
|
134
129
|
host_auth_plan,
|
|
135
130
|
install_config_file,
|
|
136
131
|
interactive_env,
|
|
137
132
|
materialize,
|
|
138
|
-
model_spec,
|
|
139
133
|
normalize,
|
|
140
134
|
normalized_usage,
|
|
141
135
|
parse_json_object,
|
|
142
136
|
plan_auth,
|
|
143
137
|
prepare_config_home,
|
|
144
|
-
price_for,
|
|
145
138
|
)
|
|
146
139
|
from .execution import (
|
|
147
140
|
CallbackCommandStreamSink,
|
|
@@ -164,10 +157,26 @@ from .execution import (
|
|
|
164
157
|
TransformingExecutor,
|
|
165
158
|
confine,
|
|
166
159
|
)
|
|
160
|
+
from .models import ModelSpec, ModelTable, context_window, default_models, model_spec
|
|
167
161
|
from .oneshot import OneShotTransport
|
|
162
|
+
from .pricing import ModelPricing, PricingTable, cost_usd, default_pricing, price_for
|
|
168
163
|
from .probe import probe_provider
|
|
169
|
-
from .providers import
|
|
170
|
-
|
|
164
|
+
from .providers import (
|
|
165
|
+
DEFAULT_CATALOG,
|
|
166
|
+
ProviderCatalog,
|
|
167
|
+
ProviderPlugin,
|
|
168
|
+
ProviderTestSupport,
|
|
169
|
+
get_provider,
|
|
170
|
+
provider_names,
|
|
171
|
+
stream_provider_names,
|
|
172
|
+
)
|
|
173
|
+
from .providers.claude import (
|
|
174
|
+
ClaudeHook,
|
|
175
|
+
ClaudeProvider,
|
|
176
|
+
ClaudeStreamTransport,
|
|
177
|
+
ClaudeToolPolicy,
|
|
178
|
+
SandboxConfig,
|
|
179
|
+
)
|
|
171
180
|
from .providers.codex import CodexProvider, CodexSandboxConfig
|
|
172
181
|
from .providers.codex.app_server import CodexAppServerTransport
|
|
173
182
|
from .providers.copilot import CopilotProvider
|
|
@@ -176,9 +185,9 @@ from .providers.opencode import OpencodeProvider
|
|
|
176
185
|
from .runtime import Agent
|
|
177
186
|
from .session import Session, Turn, TurnTicket, map_mcp_servers
|
|
178
187
|
|
|
179
|
-
__version__ = "0.
|
|
188
|
+
__version__ = "0.21.0"
|
|
180
189
|
|
|
181
|
-
__all__ = [
|
|
190
|
+
__all__ = [ # noqa: RUF022 - public API test uses Python's case-sensitive order
|
|
182
191
|
"Agent",
|
|
183
192
|
"AgentEvent",
|
|
184
193
|
"AgentEventHandler",
|
|
@@ -198,6 +207,7 @@ __all__ = [
|
|
|
198
207
|
"ClaudeHook",
|
|
199
208
|
"ClaudeProvider",
|
|
200
209
|
"ClaudeStreamTransport",
|
|
210
|
+
"ClaudeToolPolicy",
|
|
201
211
|
"CliAgent",
|
|
202
212
|
"CliCheckError",
|
|
203
213
|
"CliExitError",
|
|
@@ -216,6 +226,7 @@ __all__ = [
|
|
|
216
226
|
"ConfigFileInstallation",
|
|
217
227
|
"ConfigScope",
|
|
218
228
|
"Confinement",
|
|
229
|
+
"ConnectionOptions",
|
|
219
230
|
"ConsoleEventHandler",
|
|
220
231
|
"ContextualStreamParser",
|
|
221
232
|
"Continuity",
|
|
@@ -225,6 +236,7 @@ __all__ = [
|
|
|
225
236
|
"CopilotProvider",
|
|
226
237
|
"CredentialKind",
|
|
227
238
|
"CredentialSource",
|
|
239
|
+
"DEFAULT_CATALOG",
|
|
228
240
|
"DockerExecConfinement",
|
|
229
241
|
"EndpointSpec",
|
|
230
242
|
"EnvCredential",
|
|
@@ -242,6 +254,7 @@ __all__ = [
|
|
|
242
254
|
"IdAllocator",
|
|
243
255
|
"InMemoryCheckpointStore",
|
|
244
256
|
"Lifecycle",
|
|
257
|
+
"LoginSpec",
|
|
245
258
|
"McpConfigError",
|
|
246
259
|
"McpInstallation",
|
|
247
260
|
"McpMechanism",
|
|
@@ -264,6 +277,7 @@ __all__ = [
|
|
|
264
277
|
"ParsedTurn",
|
|
265
278
|
"ParserContext",
|
|
266
279
|
"PolicyConfig",
|
|
280
|
+
"PricingContext",
|
|
267
281
|
"PricingTable",
|
|
268
282
|
"Process",
|
|
269
283
|
"ProcessClosedError",
|
|
@@ -271,9 +285,12 @@ __all__ = [
|
|
|
271
285
|
"ProcessOutput",
|
|
272
286
|
"Provider",
|
|
273
287
|
"ProviderCapabilityError",
|
|
288
|
+
"ProviderCatalog",
|
|
274
289
|
"ProviderError",
|
|
290
|
+
"ProviderPlugin",
|
|
275
291
|
"ProviderProfile",
|
|
276
292
|
"ProviderStatus",
|
|
293
|
+
"ProviderTestSupport",
|
|
277
294
|
"ProviderUsage",
|
|
278
295
|
"RandomIds",
|
|
279
296
|
"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()
|
|
@@ -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,
|
|
@@ -95,9 +96,10 @@ from .mcp import (
|
|
|
95
96
|
StdioMcpServer,
|
|
96
97
|
install_config_file,
|
|
97
98
|
)
|
|
98
|
-
from .models import ModelSpec, ModelTable
|
|
99
|
+
from .models import ModelSpec, ModelTable
|
|
99
100
|
from .permissions import ApprovalPolicy, NativeMode, NativePermissions
|
|
100
|
-
from .
|
|
101
|
+
from .plugin import ConnectionOptions, PricingContext, ProviderPlugin, ProviderTestSupport
|
|
102
|
+
from .pricing import ModelPricing, PricingTable, cost_usd
|
|
101
103
|
from .profile import (
|
|
102
104
|
ConfigScope,
|
|
103
105
|
McpMechanism,
|
|
@@ -148,6 +150,7 @@ __all__ = [
|
|
|
148
150
|
"CompositeEventHandler",
|
|
149
151
|
"ConfigFileInstallation",
|
|
150
152
|
"ConfigScope",
|
|
153
|
+
"ConnectionOptions",
|
|
151
154
|
"ConsoleEventHandler",
|
|
152
155
|
"ContextualStreamParser",
|
|
153
156
|
"Continuity",
|
|
@@ -169,6 +172,7 @@ __all__ = [
|
|
|
169
172
|
"IdAllocator",
|
|
170
173
|
"InMemoryCheckpointStore",
|
|
171
174
|
"Lifecycle",
|
|
175
|
+
"LoginSpec",
|
|
172
176
|
"McpConfigError",
|
|
173
177
|
"McpInstallation",
|
|
174
178
|
"McpMechanism",
|
|
@@ -188,13 +192,16 @@ __all__ = [
|
|
|
188
192
|
"ParsedTurn",
|
|
189
193
|
"ParserContext",
|
|
190
194
|
"PolicyConfig",
|
|
195
|
+
"PricingContext",
|
|
191
196
|
"PricingTable",
|
|
192
197
|
"ProcessClosedError",
|
|
193
198
|
"Provider",
|
|
194
199
|
"ProviderCapabilityError",
|
|
195
200
|
"ProviderError",
|
|
201
|
+
"ProviderPlugin",
|
|
196
202
|
"ProviderProfile",
|
|
197
203
|
"ProviderStatus",
|
|
204
|
+
"ProviderTestSupport",
|
|
198
205
|
"ProviderUsage",
|
|
199
206
|
"RandomIds",
|
|
200
207
|
"RateLimitStatus",
|
|
@@ -245,20 +252,15 @@ __all__ = [
|
|
|
245
252
|
"UsageReport",
|
|
246
253
|
"compact_json",
|
|
247
254
|
"compose_event_handlers",
|
|
248
|
-
"context_window",
|
|
249
255
|
"cost_usd",
|
|
250
|
-
"default_models",
|
|
251
|
-
"default_pricing",
|
|
252
256
|
"dialect_problems",
|
|
253
257
|
"host_auth_plan",
|
|
254
258
|
"install_config_file",
|
|
255
259
|
"interactive_env",
|
|
256
260
|
"materialize",
|
|
257
|
-
"model_spec",
|
|
258
261
|
"normalize",
|
|
259
262
|
"normalized_usage",
|
|
260
263
|
"parse_json_object",
|
|
261
264
|
"plan_auth",
|
|
262
265
|
"prepare_config_home",
|
|
263
|
-
"price_for",
|
|
264
266
|
]
|
|
@@ -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.
|
|
@@ -0,0 +1,209 @@
|
|
|
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
|
+
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.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
vendor: str
|
|
44
|
+
model: str
|
|
45
|
+
context_window: int
|
|
46
|
+
source: str
|
|
47
|
+
checked: dt.date
|
|
48
|
+
display_name: str | None = None
|
|
49
|
+
family: str | None = None
|
|
50
|
+
tier: int | None = None
|
|
51
|
+
tier_name: str | None = None
|
|
52
|
+
generation_id: str | None = None
|
|
53
|
+
generation: str | None = None
|
|
54
|
+
generation_rank: int | None = None
|
|
55
|
+
|
|
56
|
+
def __post_init__(self) -> None:
|
|
57
|
+
"""Reject a missing name, source or date, and a window that is not a positive int."""
|
|
58
|
+
for name in ("vendor", "model"):
|
|
59
|
+
value: object = getattr(self, name)
|
|
60
|
+
if not isinstance(value, str) or not value.strip():
|
|
61
|
+
msg = f"{name} must be a non-empty string"
|
|
62
|
+
raise ValueError(msg)
|
|
63
|
+
source = cast("object", self.source)
|
|
64
|
+
if not isinstance(source, str) or not source.startswith("https://"):
|
|
65
|
+
msg = f"{self.vendor}/{self.model}: source must be an https:// URL"
|
|
66
|
+
raise ValueError(msg)
|
|
67
|
+
checked = cast("object", self.checked)
|
|
68
|
+
if not isinstance(checked, dt.date) or isinstance(checked, dt.datetime):
|
|
69
|
+
msg = f"{self.vendor}/{self.model}: checked must be a datetime.date"
|
|
70
|
+
raise TypeError(msg)
|
|
71
|
+
window = cast("object", self.context_window)
|
|
72
|
+
if isinstance(window, bool) or not isinstance(window, int) or window <= 0:
|
|
73
|
+
msg = f"{self.vendor}/{self.model}: context_window must be a positive int"
|
|
74
|
+
raise ValueError(msg)
|
|
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)
|
|
96
|
+
|
|
97
|
+
def _check_presentation(self) -> None:
|
|
98
|
+
"""Reject a half-set name pair or tier pair, and a tier that is not a positive int."""
|
|
99
|
+
for first, second in (("display_name", "family"), ("tier", "tier_name")):
|
|
100
|
+
if (getattr(self, first) is None) != (getattr(self, second) is None):
|
|
101
|
+
msg = f"{self.vendor}/{self.model}: {first} and {second} are set together"
|
|
102
|
+
raise ValueError(msg)
|
|
103
|
+
for name in ("display_name", "family", "tier_name"):
|
|
104
|
+
value: object = getattr(self, name)
|
|
105
|
+
if value is not None and (not isinstance(value, str) or not value.strip()):
|
|
106
|
+
msg = f"{self.vendor}/{self.model}: {name} must be a non-empty string"
|
|
107
|
+
raise ValueError(msg)
|
|
108
|
+
tier = cast("object", self.tier)
|
|
109
|
+
if tier is not None and (isinstance(tier, bool) or not isinstance(tier, int) or tier < 1):
|
|
110
|
+
msg = f"{self.vendor}/{self.model}: tier must be a positive int"
|
|
111
|
+
raise ValueError(msg)
|
|
112
|
+
if self.tier is not None and self.family is None:
|
|
113
|
+
msg = f"{self.vendor}/{self.model}: tier needs a family"
|
|
114
|
+
raise ValueError(msg)
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def key(self) -> tuple[str, str]:
|
|
118
|
+
"""The ``(vendor, model)`` this entry describes."""
|
|
119
|
+
return (self.vendor, self.model)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass(frozen=True)
|
|
123
|
+
class ModelTable:
|
|
124
|
+
"""A versioned set of :class:`ModelSpec` entries.
|
|
125
|
+
|
|
126
|
+
Attributes:
|
|
127
|
+
version: Names this set of facts; change it whenever a fact changes.
|
|
128
|
+
last_updated: Date the table as a whole was last updated. No entry
|
|
129
|
+
may have been checked later.
|
|
130
|
+
entries: One entry per ``(vendor, model)``.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
version: str
|
|
134
|
+
last_updated: dt.date
|
|
135
|
+
entries: tuple[ModelSpec, ...] = field(default=())
|
|
136
|
+
|
|
137
|
+
def __post_init__(self) -> None:
|
|
138
|
+
"""Reject a missing version, duplicate keys, and entries checked after ``last_updated``."""
|
|
139
|
+
version = cast("object", self.version)
|
|
140
|
+
if not isinstance(version, str) or not version.strip():
|
|
141
|
+
msg = "version must be a non-empty string"
|
|
142
|
+
raise ValueError(msg)
|
|
143
|
+
object.__setattr__(self, "entries", tuple(self.entries))
|
|
144
|
+
seen: set[tuple[str, str]] = set()
|
|
145
|
+
for entry in self.entries:
|
|
146
|
+
if not isinstance(cast("object", entry), ModelSpec):
|
|
147
|
+
msg = f"entries must be ModelSpec, got {entry!r}"
|
|
148
|
+
raise TypeError(msg)
|
|
149
|
+
if entry.key in seen:
|
|
150
|
+
msg = f"duplicate model entry for {entry.vendor}/{entry.model}"
|
|
151
|
+
raise ValueError(msg)
|
|
152
|
+
seen.add(entry.key)
|
|
153
|
+
if entry.checked > self.last_updated:
|
|
154
|
+
msg = (
|
|
155
|
+
f"{entry.vendor}/{entry.model} was checked {entry.checked}, "
|
|
156
|
+
f"after the table's last_updated {self.last_updated}"
|
|
157
|
+
)
|
|
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)
|
|
184
|
+
|
|
185
|
+
def get(self, vendor: str, model: str) -> ModelSpec | None:
|
|
186
|
+
"""The entry for exactly ``(vendor, model)``, or ``None``."""
|
|
187
|
+
for entry in self.entries:
|
|
188
|
+
if entry.key == (vendor, model):
|
|
189
|
+
return entry
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
def with_entries(
|
|
193
|
+
self,
|
|
194
|
+
entries: Iterable[ModelSpec],
|
|
195
|
+
*,
|
|
196
|
+
version: str,
|
|
197
|
+
last_updated: dt.date | None = None,
|
|
198
|
+
) -> ModelTable:
|
|
199
|
+
"""A new table with *entries* added, replacing any with the same key."""
|
|
200
|
+
added = tuple(entries)
|
|
201
|
+
by_key = {entry.key: entry for entry in self.entries}
|
|
202
|
+
by_key.update({entry.key: entry for entry in added})
|
|
203
|
+
latest = max([self.last_updated, *(entry.checked for entry in added)])
|
|
204
|
+
return replace(
|
|
205
|
+
self,
|
|
206
|
+
version=version,
|
|
207
|
+
last_updated=last_updated or latest,
|
|
208
|
+
entries=tuple(by_key.values()),
|
|
209
|
+
)
|