pydantic-claude-code 0.2.0__tar.gz → 0.4.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.
- pydantic_claude_code-0.4.0/.github/workflows/ci.yml +49 -0
- pydantic_claude_code-0.4.0/.github/workflows/publish.yml +38 -0
- {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.4.0}/.gitignore +2 -0
- pydantic_claude_code-0.4.0/PKG-INFO +300 -0
- pydantic_claude_code-0.4.0/README.md +273 -0
- {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.4.0}/examples/basic.py +2 -3
- pydantic_claude_code-0.4.0/pyproject.toml +65 -0
- pydantic_claude_code-0.4.0/src/pydantic_ai_claude_code/__init__.py +57 -0
- {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.4.0}/src/pydantic_ai_claude_code/auth.py +20 -1
- pydantic_claude_code-0.4.0/src/pydantic_ai_claude_code/clai2.py +229 -0
- {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.4.0}/src/pydantic_ai_claude_code/config.py +11 -3
- {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.4.0}/src/pydantic_ai_claude_code/credentials.py +3 -3
- {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.4.0}/src/pydantic_ai_claude_code/flow.py +18 -7
- pydantic_claude_code-0.4.0/src/pydantic_ai_claude_code/model.py +107 -0
- {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.4.0}/src/pydantic_ai_claude_code/provider.py +2 -14
- {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.4.0}/src/pydantic_ai_claude_code/storage.py +52 -25
- pydantic_claude_code-0.4.0/tests/conftest.py +107 -0
- pydantic_claude_code-0.4.0/tests/test_clai2.py +280 -0
- {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.4.0}/tests/test_flow.py +60 -1
- pydantic_claude_code-0.4.0/tests/test_integration.py +63 -0
- {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.4.0}/tests/test_keyring_store.py +31 -0
- {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.4.0}/tests/test_provider.py +11 -2
- {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.4.0}/tests/test_storage.py +12 -2
- pydantic_claude_code-0.4.0/uv.lock +2199 -0
- pydantic_claude_code-0.2.0/PKG-INFO +0 -172
- pydantic_claude_code-0.2.0/README.md +0 -148
- pydantic_claude_code-0.2.0/pyproject.toml +0 -47
- pydantic_claude_code-0.2.0/src/pydantic_ai_claude_code/__init__.py +0 -37
- pydantic_claude_code-0.2.0/src/pydantic_ai_claude_code/model.py +0 -40
- pydantic_claude_code-0.2.0/tests/test_integration.py +0 -79
- {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.4.0}/LICENSE +0 -0
- {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.4.0}/src/pydantic_ai_claude_code/__main__.py +0 -0
- {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.4.0}/src/pydantic_ai_claude_code/py.typed +0 -0
- {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.4.0}/tests/test_callback_server.py +0 -0
- {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.4.0}/tests/test_credentials.py +0 -0
- {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.4.0}/tests/test_model.py +0 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
concurrency:
|
|
12
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
13
|
+
cancel-in-progress: true
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
lint:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
|
|
20
|
+
with:
|
|
21
|
+
persist-credentials: false
|
|
22
|
+
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
|
|
23
|
+
with:
|
|
24
|
+
python-version: "3.13"
|
|
25
|
+
- run: uv sync --locked --extra dev --extra clai2
|
|
26
|
+
- run: uv run ruff format --check src tests examples
|
|
27
|
+
- run: uv run ruff check src tests examples
|
|
28
|
+
- run: uv run pyright
|
|
29
|
+
|
|
30
|
+
test:
|
|
31
|
+
name: test (${{ matrix.os }}, ${{ matrix.python-version }})
|
|
32
|
+
runs-on: ${{ matrix.os }}
|
|
33
|
+
strategy:
|
|
34
|
+
fail-fast: false
|
|
35
|
+
matrix:
|
|
36
|
+
os: [ubuntu-latest]
|
|
37
|
+
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
|
38
|
+
include:
|
|
39
|
+
- os: macos-latest
|
|
40
|
+
python-version: "3.14"
|
|
41
|
+
steps:
|
|
42
|
+
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
|
|
43
|
+
with:
|
|
44
|
+
persist-credentials: false
|
|
45
|
+
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
|
|
46
|
+
with:
|
|
47
|
+
python-version: ${{ matrix.python-version }}
|
|
48
|
+
- run: uv sync --locked --extra dev --extra clai2
|
|
49
|
+
- run: uv run pytest -q
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
# Push a `vX.Y.Z` tag matching `version` in pyproject.toml to test, build, and publish that release to PyPI.
|
|
4
|
+
on:
|
|
5
|
+
push:
|
|
6
|
+
tags: ["v*"]
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
publish:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
|
|
16
|
+
with:
|
|
17
|
+
persist-credentials: false
|
|
18
|
+
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.13"
|
|
21
|
+
# The job holds the PyPI token, so it never restores a cache another run could have written.
|
|
22
|
+
enable-cache: false
|
|
23
|
+
- name: Check the tag matches the package version
|
|
24
|
+
env:
|
|
25
|
+
TAG: ${{ github.ref_name }}
|
|
26
|
+
run: |
|
|
27
|
+
version="$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')"
|
|
28
|
+
if [ "$TAG" != "v$version" ]; then
|
|
29
|
+
echo "Tag $TAG does not match pyproject.toml version $version" >&2
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
32
|
+
- run: uv sync --locked --extra dev --extra clai2
|
|
33
|
+
- run: uv run pytest -q
|
|
34
|
+
- run: uv build
|
|
35
|
+
- run: uvx twine check dist/*
|
|
36
|
+
- run: uv publish
|
|
37
|
+
env:
|
|
38
|
+
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pydantic-claude-code
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Use your Claude Code subscription from a Pydantic AI Agent or CLAI2, with full tool support
|
|
5
|
+
Project-URL: Repository, https://github.com/mpfaffenberger/pydantic-ai-claude-code
|
|
6
|
+
Project-URL: Homepage, https://pypi.org/project/pydantic-claude-code/
|
|
7
|
+
Author: Michael Pfaffenberger
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Requires-Python: <3.15,>=3.11
|
|
17
|
+
Requires-Dist: keyring<26,>=23
|
|
18
|
+
Requires-Dist: pydantic-ai-slim[anthropic]<3,>=2.31.0
|
|
19
|
+
Provides-Extra: clai2
|
|
20
|
+
Requires-Dist: pydantic-clai2>0.52.0; extra == 'clai2'
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pyright>=1.1.400; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest-asyncio>=0.23.1; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest>=8.3.4; extra == 'dev'
|
|
25
|
+
Requires-Dist: ruff<0.16,>=0.15; extra == 'dev'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# pydantic-claude-code
|
|
29
|
+
|
|
30
|
+
Use your Claude Code subscription from a plain pydantic-ai `Agent` or from
|
|
31
|
+
[CLAI2](https://github.com/pydantic/pydantic-ai/tree/main/src/pydantic_clai2),
|
|
32
|
+
with full pydantic-ai tool support. No API key, no separate billing: if Claude
|
|
33
|
+
Code works from your terminal, this works too.
|
|
34
|
+
|
|
35
|
+
The repo is `mpfaffenberger/pydantic-ai-claude-code` and the import is
|
|
36
|
+
`pydantic_ai_claude_code`; the PyPI project is `pydantic-claude-code`
|
|
37
|
+
(`pip install pydantic-claude-code`).
|
|
38
|
+
|
|
39
|
+
## Models
|
|
40
|
+
|
|
41
|
+
| Model | ID |
|
|
42
|
+
|---|---|
|
|
43
|
+
| Claude Opus 5.5 | `claude-opus-5-5` |
|
|
44
|
+
| Claude Sonnet 5.5 | `claude-sonnet-5-5` |
|
|
45
|
+
| Claude Fable 5.1 | `claude-fable-5-1` |
|
|
46
|
+
| Claude Haiku 4.5 | `claude-haiku-4-5` |
|
|
47
|
+
|
|
48
|
+
These are the current models, the ones CLAI2's menus offer (`config.MODELS`).
|
|
49
|
+
They were checked on 2026-09-30 against Anthropic's
|
|
50
|
+
[models overview](https://docs.anthropic.com/en/docs/about-claude/models/overview),
|
|
51
|
+
against the `anthropic` SDK's model list that pydantic-ai's `KnownModelName` is
|
|
52
|
+
built from, and live against a Claude subscription. Any other model ID your
|
|
53
|
+
subscription serves, such as `claude-opus-4-8`, works too; it just isn't listed.
|
|
54
|
+
|
|
55
|
+
## Use it in CLAI2
|
|
56
|
+
|
|
57
|
+
The plugin adds `claude-code:MODEL` models to CLAI2, next to its own providers,
|
|
58
|
+
so the stock agent, Coder, and your other plugins all keep working.
|
|
59
|
+
|
|
60
|
+
### Install: drop the folder in
|
|
61
|
+
|
|
62
|
+
The plugin is the `src/pydantic_ai_claude_code` folder, as is. Copy it into
|
|
63
|
+
CLAI2's plugins folder under the name `claude_code`. Nothing to `pip install`:
|
|
64
|
+
everything it imports (`pydantic-ai` with Anthropic support, `httpx2`,
|
|
65
|
+
`keyring`) already ships with CLAI2.
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
git clone --depth 1 https://github.com/mpfaffenberger/pydantic-ai-claude-code /tmp/claude-code-plugin
|
|
69
|
+
mkdir -p ~/.config/pydantic-clai2/plugins
|
|
70
|
+
cp -R /tmp/claude-code-plugin/src/pydantic_ai_claude_code ~/.config/pydantic-clai2/plugins/claude_code
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The plugins folder is `$XDG_CONFIG_HOME/pydantic-clai2/plugins/`, which is
|
|
74
|
+
`~/.config/pydantic-clai2/plugins/` by default on macOS and Linux: the folder
|
|
75
|
+
next to CLAI2's `config.db`. To update, delete `claude_code` and copy again. To hack on the plugin, symlink the folder instead of copying it:
|
|
76
|
+
`ln -s "$PWD/src/pydantic_ai_claude_code" ~/.config/pydantic-clai2/plugins/claude_code`.
|
|
77
|
+
|
|
78
|
+
Dropped-in plugins load when CLAI2 starts. Inside CLAI2, `/plugins` lists it as
|
|
79
|
+
`claude_code`, where Space turns it off and on. (The shell's `clai2 plugins list`
|
|
80
|
+
shows only plugins added by name, so it won't appear there.)
|
|
81
|
+
|
|
82
|
+
**CLAI2 version:** the plugin needs `PluginHost.model_provider`
|
|
83
|
+
([pydantic/pydantic-ai#9468](https://github.com/pydantic/pydantic-ai/pull/9468)),
|
|
84
|
+
which the first `pydantic-clai2` release after 0.52.0 includes. On an older
|
|
85
|
+
CLAI2 the plugin fails to load with `This pydantic-clai2 cannot run plugin
|
|
86
|
+
models`. Until that release, run CLAI2 from that pull request's commit:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
uvx --from "git+https://github.com/pydantic/pydantic-ai@8d714fc7b6cbf1e47c3364fa8b8cf28a8d350585#subdirectory=src/pydantic_clai2" \
|
|
90
|
+
--with "pydantic-ai-slim[anthropic,mcp,openai] @ git+https://github.com/pydantic/pydantic-ai@8d714fc7b6cbf1e47c3364fa8b8cf28a8d350585#subdirectory=pydantic_ai_slim" \
|
|
91
|
+
--with "pydantic-ai-harness[coder] @ git+https://github.com/pydantic/pydantic-ai@8d714fc7b6cbf1e47c3364fa8b8cf28a8d350585#subdirectory=src/pydantic_ai_harness" \
|
|
92
|
+
--with "pydantic-graph @ git+https://github.com/pydantic/pydantic-ai@8d714fc7b6cbf1e47c3364fa8b8cf28a8d350585#subdirectory=pydantic_graph" \
|
|
93
|
+
clai2
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Or install it as a package instead, into CLAI2's environment, and point CLAI2
|
|
97
|
+
at it: `uv tool install pydantic-clai2 --with pydantic-claude-code`, then
|
|
98
|
+
`/plugins add claude_code pydantic_ai_claude_code`.
|
|
99
|
+
|
|
100
|
+
### Sign in
|
|
101
|
+
|
|
102
|
+
Run `/claude_code login`. Your browser opens Claude's sign-in page, and CLAI2
|
|
103
|
+
prints the URL in case it doesn't. Or open the settings menu with `/claude_code`
|
|
104
|
+
(or `C` on the plugin in `/plugins`), choose **Sign-in**, and press Enter.
|
|
105
|
+
|
|
106
|
+
### Pick a model
|
|
107
|
+
|
|
108
|
+
Open `/add_model` and choose the `claude-code` provider, or type one directly:
|
|
109
|
+
|
|
110
|
+
```text
|
|
111
|
+
/add_model claude-code:claude-opus-5-5
|
|
112
|
+
/model claude-code:claude-sonnet-5-5
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Headless runs work the same way:
|
|
116
|
+
`clai2 -p "Summarize this repo" -m claude-code:claude-fable-5-1`.
|
|
117
|
+
|
|
118
|
+
### Settings menu
|
|
119
|
+
|
|
120
|
+
`/plugins configure claude_code`, `C` in `/plugins`, or a bare `/claude_code`
|
|
121
|
+
opens it. Every change is saved right away and applies from the next run.
|
|
122
|
+
|
|
123
|
+
| Row | What it does | Stored in |
|
|
124
|
+
|---|---|---|
|
|
125
|
+
| Sign-in | Enter signs in through the browser; R signs out | the token store below, never in plugin settings |
|
|
126
|
+
| Credential storage | `auto` (`CLAUDE_CODE_CREDENTIALS`, else keyring, else a file), `keyring`, or `file` | plugin settings (`credentials`) |
|
|
127
|
+
|
|
128
|
+
`/claude_code login`, `/claude_code logout`, and `/claude_code status` do the
|
|
129
|
+
same without the menu.
|
|
130
|
+
|
|
131
|
+
### Where the sign-in lives
|
|
132
|
+
|
|
133
|
+
The sign-in is an OAuth token pair, not an API key, so it does not go in
|
|
134
|
+
`/keys`. It is kept where this package keeps it outside CLAI2, which means one
|
|
135
|
+
sign-in serves both CLAI2 and your own agents: the OS keyring (service
|
|
136
|
+
`pydantic-ai-claude-code`), or, without a keychain, a file readable only by
|
|
137
|
+
you. CLAI2's plugin settings are plaintext SQLite, so they hold only the
|
|
138
|
+
storage choice. Tokens are refreshed automatically and the refreshed pair is
|
|
139
|
+
saved back. The **Credential storage** row's `auto` follows the
|
|
140
|
+
`CLAUDE_CODE_CREDENTIALS` variable described below when it is set; `keyring`
|
|
141
|
+
and `file` override it.
|
|
142
|
+
|
|
143
|
+
Signing out deletes the stored tokens; they stay valid at Anthropic until they
|
|
144
|
+
expire. Switching storage does not move an existing sign-in, so sign in again
|
|
145
|
+
after switching.
|
|
146
|
+
|
|
147
|
+
If a run says `Sign in to Claude Code first` or `Your Claude Code sign-in has
|
|
148
|
+
expired`, run `/claude_code login`. If the
|
|
149
|
+
plugin fails to load with `This pydantic-clai2 cannot run plugin models`,
|
|
150
|
+
upgrade CLAI2 as described under [Install](#install-drop-the-folder-in).
|
|
151
|
+
|
|
152
|
+
## Use it from Python
|
|
153
|
+
|
|
154
|
+
pydantic-ai owns the loop: its `Agent` drives the conversation, runs your
|
|
155
|
+
tools, and validates structured output. This package is just a model plus a
|
|
156
|
+
provider. Instead of a `claude-code:` model string, construct
|
|
157
|
+
`ClaudeCodeModel('claude-fable-5-1')` directly; it connects itself to a
|
|
158
|
+
`ClaudeCodeProvider` by default.
|
|
159
|
+
|
|
160
|
+
### Quick start
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
import asyncio
|
|
164
|
+
|
|
165
|
+
from pydantic_ai import Agent
|
|
166
|
+
|
|
167
|
+
from pydantic_ai_claude_code import ClaudeCodeModel, default_store, login
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
async def main() -> None:
|
|
171
|
+
# One-time: opens your browser, mints tokens, stores them
|
|
172
|
+
# (it only needs to run again when the tokens are revoked).
|
|
173
|
+
if default_store().load() is None:
|
|
174
|
+
await login()
|
|
175
|
+
|
|
176
|
+
agent = Agent(ClaudeCodeModel('claude-opus-5-5'))
|
|
177
|
+
|
|
178
|
+
result = await agent.run('Say hi in three words.')
|
|
179
|
+
print(result.output)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
asyncio.run(main())
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
You can also sign in from the shell: `python -m pydantic_ai_claude_code login`.
|
|
186
|
+
|
|
187
|
+
### With tools
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
from pydantic_ai import Agent
|
|
191
|
+
from pydantic_ai_claude_code import ClaudeCodeModel
|
|
192
|
+
|
|
193
|
+
agent = Agent(ClaudeCodeModel('claude-sonnet-5-5'))
|
|
194
|
+
|
|
195
|
+
@agent.tool_plain
|
|
196
|
+
def add(a: int, b: int) -> int:
|
|
197
|
+
"""Add two numbers."""
|
|
198
|
+
return a + b
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Tools defined on the agent are sent to the API as standard Anthropic tool
|
|
202
|
+
definitions. Structured output works the same way as with the built-in
|
|
203
|
+
`anthropic` provider.
|
|
204
|
+
|
|
205
|
+
### Structured output
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
from pydantic import BaseModel
|
|
209
|
+
from pydantic_ai import Agent
|
|
210
|
+
from pydantic_ai_claude_code import ClaudeCodeModel
|
|
211
|
+
|
|
212
|
+
class Weather(BaseModel):
|
|
213
|
+
city: str
|
|
214
|
+
temperature_c: float
|
|
215
|
+
|
|
216
|
+
agent = Agent(ClaudeCodeModel('claude-fable-5-1'), output_type=Weather)
|
|
217
|
+
result = await agent.run('Weather in Paris right now?')
|
|
218
|
+
assert result.output.city == 'Paris'
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Streaming
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
from pydantic_ai import Agent
|
|
225
|
+
from pydantic_ai_claude_code import ClaudeCodeModel
|
|
226
|
+
|
|
227
|
+
agent = Agent(ClaudeCodeModel('claude-haiku-4-5'))
|
|
228
|
+
async with agent.run_stream('Count from 1 to 3.') as stream:
|
|
229
|
+
async for chunk in stream.stream_text():
|
|
230
|
+
print(chunk, end='', flush=True)
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## How auth works
|
|
234
|
+
|
|
235
|
+
The flow uses the same shared public OAuth client that the Claude Code CLI
|
|
236
|
+
uses:
|
|
237
|
+
|
|
238
|
+
- Authorization URL: `https://claude.ai/oauth/authorize`
|
|
239
|
+
- Token URL: `https://platform.claude.com/v1/oauth/token`
|
|
240
|
+
- Scopes: `org:create_api_key user:profile user:inference`
|
|
241
|
+
|
|
242
|
+
Tokens are saved to the OS keyring by default: the Keychain on macOS,
|
|
243
|
+
Credential Manager on Windows, and Secret Service on Linux. On machines without
|
|
244
|
+
a keychain (including headless Linux, where `keyring` reports its `fail`
|
|
245
|
+
backend), credentials go to a JSON file instead, created `0600`. You can
|
|
246
|
+
override its path with `CLAUDE_CODE_AUTH_FILE`:
|
|
247
|
+
|
|
248
|
+
```
|
|
249
|
+
~/.local/share/pydantic-ai-claude-code/auth.json
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
The file only ever contains what the issuer gave us. We never read the CLI's
|
|
253
|
+
own credential files.
|
|
254
|
+
|
|
255
|
+
Force a backend with the `CLAUDE_CODE_CREDENTIALS` env var (`keyring` or
|
|
256
|
+
`file`), or pass one to `default_store('file')`.
|
|
257
|
+
|
|
258
|
+
Refreshes happen automatically: the auth shim refreshes before expiry and
|
|
259
|
+
retries once on a 401, exactly like pydantic-ai's Codex provider. When the
|
|
260
|
+
refresh token itself is rejected, runs raise `ClaudeCodeSignInExpiredError`
|
|
261
|
+
(a `UserError`) telling you to sign in again.
|
|
262
|
+
|
|
263
|
+
Requests identify as Claude Code 2.1.285. The persona
|
|
264
|
+
`"You are Claude Code, Anthropic's official CLI for Claude."` is prepended to
|
|
265
|
+
the system context (position 0), as the CLI sends it. The client version
|
|
266
|
+
matters: the subscription backend refuses newer models to older CLI versions
|
|
267
|
+
(Opus 5.5 needs 2.1.280 or newer).
|
|
268
|
+
|
|
269
|
+
## Security and scope
|
|
270
|
+
|
|
271
|
+
This is a plain Anthropic Messages API client, authenticated by your Claude
|
|
272
|
+
subscription tokens. It does not run the Claude Code CLI in a subprocess, so it
|
|
273
|
+
does not inherit Claude Code's sandboxing, permission prompts, or hooks. Treat
|
|
274
|
+
it like any agent that runs code: only give it tools you trust.
|
|
275
|
+
|
|
276
|
+
Projects using this are responsible for following Anthropic's rules for using
|
|
277
|
+
Claude Code credentials in their own products.
|
|
278
|
+
|
|
279
|
+
## Development
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
uv sync --extra dev --extra clai2
|
|
283
|
+
uv run ruff check src tests && uv run ruff format --check src tests
|
|
284
|
+
uv run pyright
|
|
285
|
+
uv run pytest
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
To release, bump `version` in `pyproject.toml`, merge, and push a matching tag
|
|
289
|
+
(`git tag v0.4.0 && git push origin v0.4.0`). The `Publish` workflow tests,
|
|
290
|
+
builds, and uploads it to PyPI with the `PYPI_API_TOKEN` repository secret.
|
|
291
|
+
|
|
292
|
+
The tests never touch your real keychain or token file. They run against a
|
|
293
|
+
local Messages API stub, including a full CLAI2 turn through the plugin.
|
|
294
|
+
|
|
295
|
+
## Prior art
|
|
296
|
+
|
|
297
|
+
The OAuth mechanics (shared client ID, PKCE, token storage and refresh) are
|
|
298
|
+
lifted from the `claude_code_oauth` plugin in
|
|
299
|
+
[`code_puppy_core_plugins`](https://github.com/mpfaffenberger/code_puppy_core_plugins),
|
|
300
|
+
cleaned up and reshaped around the provider pattern in pydantic-ai.
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# pydantic-claude-code
|
|
2
|
+
|
|
3
|
+
Use your Claude Code subscription from a plain pydantic-ai `Agent` or from
|
|
4
|
+
[CLAI2](https://github.com/pydantic/pydantic-ai/tree/main/src/pydantic_clai2),
|
|
5
|
+
with full pydantic-ai tool support. No API key, no separate billing: if Claude
|
|
6
|
+
Code works from your terminal, this works too.
|
|
7
|
+
|
|
8
|
+
The repo is `mpfaffenberger/pydantic-ai-claude-code` and the import is
|
|
9
|
+
`pydantic_ai_claude_code`; the PyPI project is `pydantic-claude-code`
|
|
10
|
+
(`pip install pydantic-claude-code`).
|
|
11
|
+
|
|
12
|
+
## Models
|
|
13
|
+
|
|
14
|
+
| Model | ID |
|
|
15
|
+
|---|---|
|
|
16
|
+
| Claude Opus 5.5 | `claude-opus-5-5` |
|
|
17
|
+
| Claude Sonnet 5.5 | `claude-sonnet-5-5` |
|
|
18
|
+
| Claude Fable 5.1 | `claude-fable-5-1` |
|
|
19
|
+
| Claude Haiku 4.5 | `claude-haiku-4-5` |
|
|
20
|
+
|
|
21
|
+
These are the current models, the ones CLAI2's menus offer (`config.MODELS`).
|
|
22
|
+
They were checked on 2026-09-30 against Anthropic's
|
|
23
|
+
[models overview](https://docs.anthropic.com/en/docs/about-claude/models/overview),
|
|
24
|
+
against the `anthropic` SDK's model list that pydantic-ai's `KnownModelName` is
|
|
25
|
+
built from, and live against a Claude subscription. Any other model ID your
|
|
26
|
+
subscription serves, such as `claude-opus-4-8`, works too; it just isn't listed.
|
|
27
|
+
|
|
28
|
+
## Use it in CLAI2
|
|
29
|
+
|
|
30
|
+
The plugin adds `claude-code:MODEL` models to CLAI2, next to its own providers,
|
|
31
|
+
so the stock agent, Coder, and your other plugins all keep working.
|
|
32
|
+
|
|
33
|
+
### Install: drop the folder in
|
|
34
|
+
|
|
35
|
+
The plugin is the `src/pydantic_ai_claude_code` folder, as is. Copy it into
|
|
36
|
+
CLAI2's plugins folder under the name `claude_code`. Nothing to `pip install`:
|
|
37
|
+
everything it imports (`pydantic-ai` with Anthropic support, `httpx2`,
|
|
38
|
+
`keyring`) already ships with CLAI2.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
git clone --depth 1 https://github.com/mpfaffenberger/pydantic-ai-claude-code /tmp/claude-code-plugin
|
|
42
|
+
mkdir -p ~/.config/pydantic-clai2/plugins
|
|
43
|
+
cp -R /tmp/claude-code-plugin/src/pydantic_ai_claude_code ~/.config/pydantic-clai2/plugins/claude_code
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The plugins folder is `$XDG_CONFIG_HOME/pydantic-clai2/plugins/`, which is
|
|
47
|
+
`~/.config/pydantic-clai2/plugins/` by default on macOS and Linux: the folder
|
|
48
|
+
next to CLAI2's `config.db`. To update, delete `claude_code` and copy again. To hack on the plugin, symlink the folder instead of copying it:
|
|
49
|
+
`ln -s "$PWD/src/pydantic_ai_claude_code" ~/.config/pydantic-clai2/plugins/claude_code`.
|
|
50
|
+
|
|
51
|
+
Dropped-in plugins load when CLAI2 starts. Inside CLAI2, `/plugins` lists it as
|
|
52
|
+
`claude_code`, where Space turns it off and on. (The shell's `clai2 plugins list`
|
|
53
|
+
shows only plugins added by name, so it won't appear there.)
|
|
54
|
+
|
|
55
|
+
**CLAI2 version:** the plugin needs `PluginHost.model_provider`
|
|
56
|
+
([pydantic/pydantic-ai#9468](https://github.com/pydantic/pydantic-ai/pull/9468)),
|
|
57
|
+
which the first `pydantic-clai2` release after 0.52.0 includes. On an older
|
|
58
|
+
CLAI2 the plugin fails to load with `This pydantic-clai2 cannot run plugin
|
|
59
|
+
models`. Until that release, run CLAI2 from that pull request's commit:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
uvx --from "git+https://github.com/pydantic/pydantic-ai@8d714fc7b6cbf1e47c3364fa8b8cf28a8d350585#subdirectory=src/pydantic_clai2" \
|
|
63
|
+
--with "pydantic-ai-slim[anthropic,mcp,openai] @ git+https://github.com/pydantic/pydantic-ai@8d714fc7b6cbf1e47c3364fa8b8cf28a8d350585#subdirectory=pydantic_ai_slim" \
|
|
64
|
+
--with "pydantic-ai-harness[coder] @ git+https://github.com/pydantic/pydantic-ai@8d714fc7b6cbf1e47c3364fa8b8cf28a8d350585#subdirectory=src/pydantic_ai_harness" \
|
|
65
|
+
--with "pydantic-graph @ git+https://github.com/pydantic/pydantic-ai@8d714fc7b6cbf1e47c3364fa8b8cf28a8d350585#subdirectory=pydantic_graph" \
|
|
66
|
+
clai2
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Or install it as a package instead, into CLAI2's environment, and point CLAI2
|
|
70
|
+
at it: `uv tool install pydantic-clai2 --with pydantic-claude-code`, then
|
|
71
|
+
`/plugins add claude_code pydantic_ai_claude_code`.
|
|
72
|
+
|
|
73
|
+
### Sign in
|
|
74
|
+
|
|
75
|
+
Run `/claude_code login`. Your browser opens Claude's sign-in page, and CLAI2
|
|
76
|
+
prints the URL in case it doesn't. Or open the settings menu with `/claude_code`
|
|
77
|
+
(or `C` on the plugin in `/plugins`), choose **Sign-in**, and press Enter.
|
|
78
|
+
|
|
79
|
+
### Pick a model
|
|
80
|
+
|
|
81
|
+
Open `/add_model` and choose the `claude-code` provider, or type one directly:
|
|
82
|
+
|
|
83
|
+
```text
|
|
84
|
+
/add_model claude-code:claude-opus-5-5
|
|
85
|
+
/model claude-code:claude-sonnet-5-5
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Headless runs work the same way:
|
|
89
|
+
`clai2 -p "Summarize this repo" -m claude-code:claude-fable-5-1`.
|
|
90
|
+
|
|
91
|
+
### Settings menu
|
|
92
|
+
|
|
93
|
+
`/plugins configure claude_code`, `C` in `/plugins`, or a bare `/claude_code`
|
|
94
|
+
opens it. Every change is saved right away and applies from the next run.
|
|
95
|
+
|
|
96
|
+
| Row | What it does | Stored in |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| Sign-in | Enter signs in through the browser; R signs out | the token store below, never in plugin settings |
|
|
99
|
+
| Credential storage | `auto` (`CLAUDE_CODE_CREDENTIALS`, else keyring, else a file), `keyring`, or `file` | plugin settings (`credentials`) |
|
|
100
|
+
|
|
101
|
+
`/claude_code login`, `/claude_code logout`, and `/claude_code status` do the
|
|
102
|
+
same without the menu.
|
|
103
|
+
|
|
104
|
+
### Where the sign-in lives
|
|
105
|
+
|
|
106
|
+
The sign-in is an OAuth token pair, not an API key, so it does not go in
|
|
107
|
+
`/keys`. It is kept where this package keeps it outside CLAI2, which means one
|
|
108
|
+
sign-in serves both CLAI2 and your own agents: the OS keyring (service
|
|
109
|
+
`pydantic-ai-claude-code`), or, without a keychain, a file readable only by
|
|
110
|
+
you. CLAI2's plugin settings are plaintext SQLite, so they hold only the
|
|
111
|
+
storage choice. Tokens are refreshed automatically and the refreshed pair is
|
|
112
|
+
saved back. The **Credential storage** row's `auto` follows the
|
|
113
|
+
`CLAUDE_CODE_CREDENTIALS` variable described below when it is set; `keyring`
|
|
114
|
+
and `file` override it.
|
|
115
|
+
|
|
116
|
+
Signing out deletes the stored tokens; they stay valid at Anthropic until they
|
|
117
|
+
expire. Switching storage does not move an existing sign-in, so sign in again
|
|
118
|
+
after switching.
|
|
119
|
+
|
|
120
|
+
If a run says `Sign in to Claude Code first` or `Your Claude Code sign-in has
|
|
121
|
+
expired`, run `/claude_code login`. If the
|
|
122
|
+
plugin fails to load with `This pydantic-clai2 cannot run plugin models`,
|
|
123
|
+
upgrade CLAI2 as described under [Install](#install-drop-the-folder-in).
|
|
124
|
+
|
|
125
|
+
## Use it from Python
|
|
126
|
+
|
|
127
|
+
pydantic-ai owns the loop: its `Agent` drives the conversation, runs your
|
|
128
|
+
tools, and validates structured output. This package is just a model plus a
|
|
129
|
+
provider. Instead of a `claude-code:` model string, construct
|
|
130
|
+
`ClaudeCodeModel('claude-fable-5-1')` directly; it connects itself to a
|
|
131
|
+
`ClaudeCodeProvider` by default.
|
|
132
|
+
|
|
133
|
+
### Quick start
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
import asyncio
|
|
137
|
+
|
|
138
|
+
from pydantic_ai import Agent
|
|
139
|
+
|
|
140
|
+
from pydantic_ai_claude_code import ClaudeCodeModel, default_store, login
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
async def main() -> None:
|
|
144
|
+
# One-time: opens your browser, mints tokens, stores them
|
|
145
|
+
# (it only needs to run again when the tokens are revoked).
|
|
146
|
+
if default_store().load() is None:
|
|
147
|
+
await login()
|
|
148
|
+
|
|
149
|
+
agent = Agent(ClaudeCodeModel('claude-opus-5-5'))
|
|
150
|
+
|
|
151
|
+
result = await agent.run('Say hi in three words.')
|
|
152
|
+
print(result.output)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
asyncio.run(main())
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
You can also sign in from the shell: `python -m pydantic_ai_claude_code login`.
|
|
159
|
+
|
|
160
|
+
### With tools
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from pydantic_ai import Agent
|
|
164
|
+
from pydantic_ai_claude_code import ClaudeCodeModel
|
|
165
|
+
|
|
166
|
+
agent = Agent(ClaudeCodeModel('claude-sonnet-5-5'))
|
|
167
|
+
|
|
168
|
+
@agent.tool_plain
|
|
169
|
+
def add(a: int, b: int) -> int:
|
|
170
|
+
"""Add two numbers."""
|
|
171
|
+
return a + b
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Tools defined on the agent are sent to the API as standard Anthropic tool
|
|
175
|
+
definitions. Structured output works the same way as with the built-in
|
|
176
|
+
`anthropic` provider.
|
|
177
|
+
|
|
178
|
+
### Structured output
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
from pydantic import BaseModel
|
|
182
|
+
from pydantic_ai import Agent
|
|
183
|
+
from pydantic_ai_claude_code import ClaudeCodeModel
|
|
184
|
+
|
|
185
|
+
class Weather(BaseModel):
|
|
186
|
+
city: str
|
|
187
|
+
temperature_c: float
|
|
188
|
+
|
|
189
|
+
agent = Agent(ClaudeCodeModel('claude-fable-5-1'), output_type=Weather)
|
|
190
|
+
result = await agent.run('Weather in Paris right now?')
|
|
191
|
+
assert result.output.city == 'Paris'
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Streaming
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
from pydantic_ai import Agent
|
|
198
|
+
from pydantic_ai_claude_code import ClaudeCodeModel
|
|
199
|
+
|
|
200
|
+
agent = Agent(ClaudeCodeModel('claude-haiku-4-5'))
|
|
201
|
+
async with agent.run_stream('Count from 1 to 3.') as stream:
|
|
202
|
+
async for chunk in stream.stream_text():
|
|
203
|
+
print(chunk, end='', flush=True)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## How auth works
|
|
207
|
+
|
|
208
|
+
The flow uses the same shared public OAuth client that the Claude Code CLI
|
|
209
|
+
uses:
|
|
210
|
+
|
|
211
|
+
- Authorization URL: `https://claude.ai/oauth/authorize`
|
|
212
|
+
- Token URL: `https://platform.claude.com/v1/oauth/token`
|
|
213
|
+
- Scopes: `org:create_api_key user:profile user:inference`
|
|
214
|
+
|
|
215
|
+
Tokens are saved to the OS keyring by default: the Keychain on macOS,
|
|
216
|
+
Credential Manager on Windows, and Secret Service on Linux. On machines without
|
|
217
|
+
a keychain (including headless Linux, where `keyring` reports its `fail`
|
|
218
|
+
backend), credentials go to a JSON file instead, created `0600`. You can
|
|
219
|
+
override its path with `CLAUDE_CODE_AUTH_FILE`:
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
~/.local/share/pydantic-ai-claude-code/auth.json
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The file only ever contains what the issuer gave us. We never read the CLI's
|
|
226
|
+
own credential files.
|
|
227
|
+
|
|
228
|
+
Force a backend with the `CLAUDE_CODE_CREDENTIALS` env var (`keyring` or
|
|
229
|
+
`file`), or pass one to `default_store('file')`.
|
|
230
|
+
|
|
231
|
+
Refreshes happen automatically: the auth shim refreshes before expiry and
|
|
232
|
+
retries once on a 401, exactly like pydantic-ai's Codex provider. When the
|
|
233
|
+
refresh token itself is rejected, runs raise `ClaudeCodeSignInExpiredError`
|
|
234
|
+
(a `UserError`) telling you to sign in again.
|
|
235
|
+
|
|
236
|
+
Requests identify as Claude Code 2.1.285. The persona
|
|
237
|
+
`"You are Claude Code, Anthropic's official CLI for Claude."` is prepended to
|
|
238
|
+
the system context (position 0), as the CLI sends it. The client version
|
|
239
|
+
matters: the subscription backend refuses newer models to older CLI versions
|
|
240
|
+
(Opus 5.5 needs 2.1.280 or newer).
|
|
241
|
+
|
|
242
|
+
## Security and scope
|
|
243
|
+
|
|
244
|
+
This is a plain Anthropic Messages API client, authenticated by your Claude
|
|
245
|
+
subscription tokens. It does not run the Claude Code CLI in a subprocess, so it
|
|
246
|
+
does not inherit Claude Code's sandboxing, permission prompts, or hooks. Treat
|
|
247
|
+
it like any agent that runs code: only give it tools you trust.
|
|
248
|
+
|
|
249
|
+
Projects using this are responsible for following Anthropic's rules for using
|
|
250
|
+
Claude Code credentials in their own products.
|
|
251
|
+
|
|
252
|
+
## Development
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
uv sync --extra dev --extra clai2
|
|
256
|
+
uv run ruff check src tests && uv run ruff format --check src tests
|
|
257
|
+
uv run pyright
|
|
258
|
+
uv run pytest
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
To release, bump `version` in `pyproject.toml`, merge, and push a matching tag
|
|
262
|
+
(`git tag v0.4.0 && git push origin v0.4.0`). The `Publish` workflow tests,
|
|
263
|
+
builds, and uploads it to PyPI with the `PYPI_API_TOKEN` repository secret.
|
|
264
|
+
|
|
265
|
+
The tests never touch your real keychain or token file. They run against a
|
|
266
|
+
local Messages API stub, including a full CLAI2 turn through the plugin.
|
|
267
|
+
|
|
268
|
+
## Prior art
|
|
269
|
+
|
|
270
|
+
The OAuth mechanics (shared client ID, PKCE, token storage and refresh) are
|
|
271
|
+
lifted from the `claude_code_oauth` plugin in
|
|
272
|
+
[`code_puppy_core_plugins`](https://github.com/mpfaffenberger/code_puppy_core_plugins),
|
|
273
|
+
cleaned up and reshaped around the provider pattern in pydantic-ai.
|
|
@@ -11,7 +11,7 @@ import asyncio
|
|
|
11
11
|
|
|
12
12
|
from pydantic_ai import Agent
|
|
13
13
|
|
|
14
|
-
from pydantic_ai_claude_code import
|
|
14
|
+
from pydantic_ai_claude_code import ClaudeCodeModel, default_store, login
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
async def main() -> None:
|
|
@@ -19,8 +19,7 @@ async def main() -> None:
|
|
|
19
19
|
if default_store().load() is None:
|
|
20
20
|
await login()
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
agent = Agent(provider.model("claude-fable-5-1"))
|
|
22
|
+
agent = Agent(ClaudeCodeModel("claude-fable-5-1"))
|
|
24
23
|
|
|
25
24
|
result = await agent.run("Say hi in exactly three words.")
|
|
26
25
|
print("Agent said:", result.output)
|