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.
Files changed (36) hide show
  1. pydantic_claude_code-0.4.0/.github/workflows/ci.yml +49 -0
  2. pydantic_claude_code-0.4.0/.github/workflows/publish.yml +38 -0
  3. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.4.0}/.gitignore +2 -0
  4. pydantic_claude_code-0.4.0/PKG-INFO +300 -0
  5. pydantic_claude_code-0.4.0/README.md +273 -0
  6. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.4.0}/examples/basic.py +2 -3
  7. pydantic_claude_code-0.4.0/pyproject.toml +65 -0
  8. pydantic_claude_code-0.4.0/src/pydantic_ai_claude_code/__init__.py +57 -0
  9. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.4.0}/src/pydantic_ai_claude_code/auth.py +20 -1
  10. pydantic_claude_code-0.4.0/src/pydantic_ai_claude_code/clai2.py +229 -0
  11. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.4.0}/src/pydantic_ai_claude_code/config.py +11 -3
  12. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.4.0}/src/pydantic_ai_claude_code/credentials.py +3 -3
  13. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.4.0}/src/pydantic_ai_claude_code/flow.py +18 -7
  14. pydantic_claude_code-0.4.0/src/pydantic_ai_claude_code/model.py +107 -0
  15. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.4.0}/src/pydantic_ai_claude_code/provider.py +2 -14
  16. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.4.0}/src/pydantic_ai_claude_code/storage.py +52 -25
  17. pydantic_claude_code-0.4.0/tests/conftest.py +107 -0
  18. pydantic_claude_code-0.4.0/tests/test_clai2.py +280 -0
  19. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.4.0}/tests/test_flow.py +60 -1
  20. pydantic_claude_code-0.4.0/tests/test_integration.py +63 -0
  21. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.4.0}/tests/test_keyring_store.py +31 -0
  22. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.4.0}/tests/test_provider.py +11 -2
  23. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.4.0}/tests/test_storage.py +12 -2
  24. pydantic_claude_code-0.4.0/uv.lock +2199 -0
  25. pydantic_claude_code-0.2.0/PKG-INFO +0 -172
  26. pydantic_claude_code-0.2.0/README.md +0 -148
  27. pydantic_claude_code-0.2.0/pyproject.toml +0 -47
  28. pydantic_claude_code-0.2.0/src/pydantic_ai_claude_code/__init__.py +0 -37
  29. pydantic_claude_code-0.2.0/src/pydantic_ai_claude_code/model.py +0 -40
  30. pydantic_claude_code-0.2.0/tests/test_integration.py +0 -79
  31. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.4.0}/LICENSE +0 -0
  32. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.4.0}/src/pydantic_ai_claude_code/__main__.py +0 -0
  33. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.4.0}/src/pydantic_ai_claude_code/py.typed +0 -0
  34. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.4.0}/tests/test_callback_server.py +0 -0
  35. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.4.0}/tests/test_credentials.py +0 -0
  36. {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 }}
@@ -8,3 +8,5 @@ build/
8
8
  .ruff_cache/
9
9
  .coverage
10
10
  htmlcov/
11
+ # Local secrets, such as a PyPI token; releases publish from GitHub Actions instead.
12
+ .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 ClaudeCodeProvider, default_store, login
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
- provider = ClaudeCodeProvider()
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)