anthropics / anthropics/anthropic-sdk-python

`BetaCitationPageLocation` contains `file_id` that cannot be roundtripped as a request

Đang mở
#1,450 2 bình luận 0 reaction 0 người được giao Xem trên GitHub
Ngôn ngữ chính
Python
Star
3.9k
Fork
853
Merge trung bình
1 ngày 18 giờ
Pull request đã merge (30 ngày)
11

Mô tả

**SDK version:** `0.96.0`
**Python version:** 3.x

### Description

When an assistant message contains citations sourced from a URL-based document (a PDF provided via `"source": {"type": "url", ...}`), the API returns `BetaCitationPageLocation` objects that include `file_id: null`. The corresponding param type used to build the *next* request — `BetaCitationPageLocationParam` — has no `file_id` field at all. This means that using `model_dump(mode="json")` to echo the assistant message back in a multi-turn conversation (the standard pattern) causes the API to reject the subsequent request.

This breaks the **roundtrip property**: a response from the API should be usable as-is in the next request without requiring manual transformation.

---

### Steps to reproduce

```python
import anthropic

client = anthropic.Anthropic()

# Turn 1: send a URL-sourced PDF with citations enabled
turn_1_response = client.beta.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
betas=["citations-2023-11-01"],
messages=[
{
"role": "user",
"content": [
{
"type": "document",
"source": {
"type": "url",
"url": "https://www.example.com/sample.pdf", # any publicly accessible PDF
},
"citations": {"enabled": True},
},
{"type": "text", "text": "Summarise page 1."},
],
}
],
)

# Inspect: file_id is null on every page_location citation
for block in turn_1_response.content:
if hasattr(block, "citations") and block.citations:
for citation in block.citations:
if citation.type == "page_location":
print(citation.model_dump(mode="json"))
# Output:
# {
# 'type': 'page_location',
# 'cited_text': '...',
# 'document_index': 0,
# 'document_title': '...',
# 'start_page_number': 1,
# 'end_page_number': 2,
# 'file_id': None, # <-- present in response, not accepted in request
# }

# Turn 2: echo the assistant message back and ask a follow-up — standard multi-turn pattern
turn_2_response = client.beta.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
betas=["citations-2023-11-01"],
messages=[
# ... original user turn ...
{
"role": "assistant",
# model_dump is the natural way to convert a response back into a message param
"content": [block.model_dump(mode="json") for block in turn_1_response.content],
},
{
"role": "user",
"content": "What is the main conclusion?",
},
],
)
# ❌ Raises BadRequestError: file_id is not a valid field in BetaCitationPageLocationParam
```

---

### The type mismatch

**Response type** (`BetaCitationPageLocation`) — includes `file_id`:

```python
class BetaCitationPageLocation(BaseModel):
cited_text: str
document_index: int
document_title: Optional[str] = None
end_page_number: int
file_id: Optional[str] = None # <-- present
start_page_number: int
type: Literal["page_location"]
```

**Request param type** (`BetaCitationPageLocationParam`) — no `file_id`:

```python
class BetaCitationPageLocationParam(TypedDict, total=False):
cited_text: Required[str]
document_index: Required[int]
document_title: Required[Optional[str]]
end_page_number: Required[int]
start_page_number: Required[int]
type: Required[Literal["page_location"]]
# <-- no file_id
```

---

### Why the SDK does not save you automatically

One might expect the transform pipeline to strip `None` values before the HTTP request is sent. It does not. `is_given(None)` returns `True` because the SDK intentionally distinguishes between *"the caller passed `None`"* and *"the caller did not pass this field at all"* (via the `NotGiven` sentinel). This is correct and by design.

The second reason the field is not dropped is that `file_id` is unknown to `_transform_typeddict` when the citation is being processed against `BetaCitationPageLocationParam`. Because the field has no type annotation in the param type, it falls through to the unknown-field branch and is forwarded verbatim:

```python
# anthropic/_utils/_transform.py
type_ = annotations.get(key) # None — file_id is not in BetaCitationPageLocationParam
if type_ is None:
result[key] = value # kept as-is, even when value is None
```

This was verified with a direct call:

```python
from anthropic._utils._transform import _transform_typeddict
import anthropic.types.beta as b

citation = {
"type": "page_location",
"cited_text": "some text",
"document_index": 0,
"document_title": "My Doc",
"start_page_number": 1,
"end_page_number": 2,
"file_id": None,
}

print(_transform_typeddict(citation, b.BetaCitationPageLocationParam))
# {'type': 'page_location', 'cited_text': 'some text', 'document_index': 0,
# 'document_title': 'My Doc', 'start_page_number': 1, 'end_page_number': 2,
# 'file_id': None} <-- file_id survives
```

---

### Expected behaviour

Either:

- **Option A (preferred):** The API should not emit `file_id` in the response when the document was provided via URL. A `null` value for a field that has no meaning in this context and cannot be sent back is actively harmful.
- **Option B:** `BetaCitationPageLocationParam` should accept `file_id: Optional[str]` and the server should tolerate it being `null`, restoring the roundtrip property.

---

### Workaround

Until this is fixed, `file_id` must be stripped manually from `page_location` citations before building subsequent requests:

```python
def sanitize_content_blocks(content_blocks: list) -> list:
result = []
for block in content_blocks:
if isinstance(block, dict) and block.get("type") == "text" and block.get("citations"):
block = {
**block,
"citations": [
{k: v for k, v in c.items() if k != "file_id"}
if isinstance(c, dict) and c.get("type") == "page_location"
else c
for c in block["citations"]
],
}
result.append(block)
return result

# Usage
"content": sanitize_content_blocks([block.model_dump(mode="json") for block in turn_1_response.content])
```

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Đánh giá

Issue này chưa được đánh giá.

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.