# pydantic-ai: AnthropicModel forces single-string `system`, blocking multi-block system prompts (e.g. Anthropic OAuth / Claude Code subscription gate)

- Repo: https://github.com/pydantic/pydantic-ai
- Filed: _not yet_ — paste this into a new issue and add the URL above when filed.
- Workaround in tree: `src/quber/agents/_oauth_gate.py`
- Wired by: `src/quber/agents/llm_client.py::PydanticAIClient`
- Tested against: pydantic-ai 1.93.0, anthropic SDK current

## Summary

`pydantic_ai.Agent(system_prompt=...)` only accepts a string (or a sequence of strings, which gets concatenated). When the `AnthropicModel` builds the request, the `system` field sent to the Anthropic Messages API is always a single string. There is no supported way to send `system` as a list of typed content blocks via the Agent API.

This rules out any use case that requires the Anthropic Messages API's documented array-form `system` parameter, where each entry is `{"type": "text", "text": "..."}` (optionally with `cache_control`, etc.).

The concrete case we hit: Anthropic's OAuth subscription endpoint (the same backend that powers `claude -p` / Claude Code) enforces an identity gate that requires the **first** system block to be one of a fixed set of identity strings (e.g. `"You are Claude Code, Anthropic's official CLI for Claude."`). With pydantic-ai's current behavior, the gate cannot be satisfied — pydantic-ai collapses everything into one string, and the caller has no hook to prepend a separate block.

## Reproduction

```python
from pydantic_ai import Agent
from pydantic_ai.models.anthropic import AnthropicModel
from pydantic_ai.providers.anthropic import AnthropicProvider
from anthropic import AsyncAnthropic

# OAuth subscription token (sk-ant-oat-...), not an API key.
client = AsyncAnthropic(
    auth_token="<OAUTH_TOKEN>",
    default_headers={
        "anthropic-beta": "oauth-2025-04-20,claude-code-20250219",
        "User-Agent": "claude-cli/2.1.119 (external, claude-vscode, agent-sdk/0.1.75)",
    },
)
model = AnthropicModel(
    "claude-haiku-4-5-20251001",
    provider=AnthropicProvider(anthropic_client=client),
)
agent = Agent(model, system_prompt="You are a helpful assistant.")
agent.run_sync("hello")
# -> 401 from Anthropic: subscription gate rejects request because
#    `system` is a plain string. The gate requires `system` to be a list
#    whose first entry is the Claude Code identity TextBlock.
```

The same call against the API-key endpoint succeeds, because that endpoint does not enforce the identity gate.

## Expected behavior

One of:

1. `Agent(..., system_prompt=...)` accepts a list of `{"type": "text", "text": ...}` blocks and passes them through to the Anthropic `system` parameter unchanged.
2. `AnthropicModel` exposes a documented hook (e.g. `system_prompt_blocks=[...]` or a `before_request` callback) that allows the caller to prepend / replace system blocks before the SDK call.

Either lets callers satisfy provider-specific gates without monkey-patching the SDK client.

## Current workaround

We construct the `AsyncAnthropic` client ourselves and monkey-patch `client.beta.messages.create` to rewrite `kwargs["system"]` from a string into a 2-block list before delegating to the original method. The patched client is handed to `AnthropicModel` via `AnthropicProvider(anthropic_client=client)`. Full source:

- `src/quber/agents/_oauth_gate.py` — the patch.
- `src/quber/agents/llm_client.py::PydanticAIClient` — selects the patched client only on the OAuth code path.

Excerpt:

```python
orig_create = client.beta.messages.create

async def create_with_identity_block(*args, **kwargs):
    system = kwargs.get("system")
    if isinstance(system, str):
        kwargs["system"] = [
            {"type": "text", "text": CLAUDE_CODE_IDENTITY},
            {"type": "text", "text": system},
        ]
    return await orig_create(*args, **kwargs)

client.beta.messages.create = create_with_identity_block
```

This is fragile: it reaches into the SDK's internals (`beta.messages.create`), depends on pydantic-ai routing through that exact attribute, and silently breaks if pydantic-ai ever starts sending `system` as a list.

## Suggested resolution

Accept `list[dict]` (or a typed equivalent) for `system_prompt` on `Agent` / `AnthropicModel`, and forward verbatim to the Anthropic SDK's `system` parameter when the underlying provider is Anthropic. For non-Anthropic providers, either flatten with a documented rule or reject at construction.

This also unlocks `cache_control` on individual system blocks for prompt caching, which is currently inaccessible through the high-level Agent API for the same reason.

## Removal checklist (for the quber repo)

When the upstream fix ships:

1. Delete `src/quber/agents/_oauth_gate.py`.
2. In `PydanticAIClient.__init__`, replace the OAuth branch with a stock `AnthropicModel` plus `system_prompt=[CLAUDE_CODE_IDENTITY, CORRECT_STRUCTURE_PROMPT]` (and the equivalent for `count_agent`). Keep the custom headers (`anthropic-beta`, `User-Agent`) on the `AsyncAnthropic` client — those are still required by the gate.
3. Delete this file.
