Skip to content

Commit c829449

Browse files
authored
Ignore empty text deltas when streaming gpt-oss on Bedrock (#3215)
1 parent 0a7d2ac commit c829449

File tree

3 files changed

+100
-2
lines changed

3 files changed

+100
-2
lines changed

pydantic_ai_slim/pydantic_ai/models/bedrock.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -701,8 +701,8 @@ async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]:
701701
signature=signature,
702702
provider_name=self.provider_name if signature else None,
703703
)
704-
if 'text' in delta:
705-
maybe_event = self._parts_manager.handle_text_delta(vendor_part_id=index, content=delta['text'])
704+
if text := delta.get('text'):
705+
maybe_event = self._parts_manager.handle_text_delta(vendor_part_id=index, content=text)
706706
if maybe_event is not None: # pragma: no branch
707707
yield maybe_event
708708
if 'toolUse' in delta:
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
interactions:
2+
- request:
3+
body: '{"messages": [{"role": "user", "content": [{"text": "Hi"}]}], "system": [], "inferenceConfig": {}}'
4+
headers:
5+
amz-sdk-invocation-id:
6+
- !!binary |
7+
Zjg3NDYxYzEtZDQzZS00YThkLTkwNDMtNDcwOTNjMWZlMGM2
8+
amz-sdk-request:
9+
- !!binary |
10+
YXR0ZW1wdD0x
11+
content-length:
12+
- '98'
13+
content-type:
14+
- !!binary |
15+
YXBwbGljYXRpb24vanNvbg==
16+
method: POST
17+
uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/openai.gpt-oss-120b-1%3A0/converse-stream
18+
response:
19+
body:
20+
string: !!binary |
21+
AAAAsgAAAFKKQLTFCzpldmVudC10eXBlBwAMbWVzc2FnZVN0YXJ0DTpjb250ZW50LXR5cGUHABBh
22+
cHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1u
23+
b3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowIiwicm9sZSI6ImFzc2lzdGFu
24+
dCJ9QP+CaAAAAKIAAABXmsrXyAs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250
25+
ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVu
26+
dEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIifSwicCI6ImFiY2RlZmdoaSJ9NTLrNAAA
27+
ALkAAABW+v1BzQs6ZXZlbnQtdHlwZQcAEGNvbnRlbnRCbG9ja1N0b3ANOmNvbnRlbnQtdHlwZQcA
28+
EGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRl
29+
eCI6MCwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RV
30+
VldYWVowIn2IAETUAAABWQAAAFducmUhCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGEN
31+
OmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJj
32+
b250ZW50QmxvY2tJbmRleCI6MSwiZGVsdGEiOnsicmVhc29uaW5nQ29udGVudCI6eyJ0ZXh0Ijoi
33+
VGhlIHVzZXIganVzdCBzYXlzIFwiSGlcIi4gV2UgbmVlZCB0byByZXNwb25kIGFwcHJvcHJpYXRl
34+
bHksIGZyaWVuZGx5IGdyZWV0aW5nLiBObyBzcGVjaWFsIGluc3RydWN0aW9ucy4gU2hvdWxkIGJl
35+
IHNob3J0LiJ9fSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5P
36+
UFFSU1RVVldYIn2DKrHfAAAAsQAAAFbKjQoMCzpldmVudC10eXBlBwAQY29udGVudEJsb2NrU3Rv
37+
cA06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7
38+
ImNvbnRlbnRCbG9ja0luZGV4IjoxLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNE
39+
RUZHSElKS0xNTk9QUVJTIn1Cad1JAAAA5AAAAFdNeXohCzpldmVudC10eXBlBwARY29udGVudEJs
40+
b2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcA
41+
BWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MiwiZGVsdGEiOnsidGV4dCI6IkhlbGxvISBIb3cg
42+
Y2FuIEkgaGVscCJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xN
43+
Tk9QUVJTVFVWV1hZWjAxIn2L4Y7bAAAAuwAAAFf3OiI7CzpldmVudC10eXBlBwARY29udGVudEJs
44+
b2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcA
45+
BWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MiwiZGVsdGEiOnsidGV4dCI6IiB5b3UgdG9kYXk/
46+
In0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2dyJ9dKsF8AAAAI4AAABW6fwlWws6ZXZlbnQt
47+
dHlwZQcAEGNvbnRlbnRCbG9ja1N0b3ANOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24N
48+
Om1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MiwicCI6ImFiY2RlZmdo
49+
aWoifbla37UAAACDAAAAUY8IdEkLOmV2ZW50LXR5cGUHAAttZXNzYWdlU3RvcA06Y29udGVudC10
50+
eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7InAiOiJhYiIsInN0
51+
b3BSZWFzb24iOiJlbmRfdHVybiJ9kFbppAAAAOQAAABOKRLS4Qs6ZXZlbnQtdHlwZQcACG1ldGFk
52+
YXRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVu
53+
dHsibWV0cmljcyI6eyJsYXRlbmN5TXMiOjc1M30sInAiOiJhYmNkZWZnaGlqa2xtbiIsInVzYWdl
54+
Ijp7ImlucHV0VG9rZW5zIjo3MCwib3V0cHV0VG9rZW5zIjo0Mywic2VydmVyVG9vbFVzYWdlIjp7
55+
fSwidG90YWxUb2tlbnMiOjExM319saJH4A==
56+
headers:
57+
connection:
58+
- keep-alive
59+
content-type:
60+
- application/vnd.amazon.eventstream
61+
transfer-encoding:
62+
- chunked
63+
status:
64+
code: 200
65+
message: OK
66+
version: 1

tests/models/test_bedrock.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@
3232
)
3333
from pydantic_ai.agent import Agent
3434
from pydantic_ai.exceptions import ModelRetry
35+
from pydantic_ai.messages import AgentStreamEvent
3536
from pydantic_ai.models import ModelRequestParameters
37+
from pydantic_ai.run import AgentRunResult, AgentRunResultEvent
3638
from pydantic_ai.tools import ToolDefinition
3739
from pydantic_ai.usage import RequestUsage, RunUsage
3840

@@ -1272,3 +1274,33 @@ async def test_bedrock_no_tool_choice(bedrock_provider: BedrockProvider):
12721274
]
12731275
}
12741276
)
1277+
1278+
1279+
async def test_bedrock_model_stream_empty_text_delta(allow_model_requests: None, bedrock_provider: BedrockProvider):
1280+
model = BedrockConverseModel(model_name='openai.gpt-oss-120b-1:0', provider=bedrock_provider)
1281+
agent = Agent(model)
1282+
1283+
result: AgentRunResult | None = None
1284+
events: list[AgentStreamEvent] = []
1285+
async for event in agent.run_stream_events('Hi'):
1286+
if isinstance(event, AgentRunResultEvent):
1287+
result = event.result
1288+
else:
1289+
events.append(event)
1290+
1291+
assert result is not None
1292+
# The response stream contains `{'contentBlockDelta': {'delta': {'text': ''}, 'contentBlockIndex': 0}}`, but our response should not have any empty text parts.
1293+
assert not any(part.content == '' for part in result.response.parts if isinstance(part, TextPart))
1294+
assert events == snapshot(
1295+
[
1296+
PartStartEvent(
1297+
index=0,
1298+
part=ThinkingPart(
1299+
content='The user just says "Hi". We need to respond appropriately, friendly greeting. No special instructions. Should be short.'
1300+
),
1301+
),
1302+
PartStartEvent(index=1, part=TextPart(content='Hello! How can I help')),
1303+
FinalResultEvent(tool_name=None, tool_call_id=None),
1304+
PartDeltaEvent(index=1, delta=TextPartDelta(content_delta=' you today?')),
1305+
]
1306+
)

0 commit comments

Comments
 (0)