Skip to content

Commit 09cc988

Browse files
committed
[owl] Handle bedrock restriction on blank content (#924)
* Handle bedrock restriction on blank content
1 parent 96dd12c commit 09cc988

5 files changed

Lines changed: 151 additions & 5 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ jobs:
134134
echo 'OWL_FLUSH_CLICKHOUSE_BUFFER_SEC=5' >> .env
135135
env:
136136
OWL_ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
137+
OWL_BEDROCK_API_KEY: ${{ secrets.BEDROCK_API_KEY }}
137138
OWL_COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }}
138139
OWL_DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
139140
OWL_ELLM_API_KEY: ${{ secrets.CUSTOM_API_KEY }}

services/api/src/owl/utils/lm.py

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -760,6 +760,35 @@ async def _get_deployment(
760760

761761
### --- Chat Completion --- ###
762762

763+
@staticmethod
764+
def _prepare_bedrock_messages(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
765+
"""Replace empty string with a dot as Bedrock provider treats empty string as no content and throws error."""
766+
messages = deepcopy(messages)
767+
for message in messages:
768+
content = message.get("content", None)
769+
if isinstance(content, str):
770+
if content.strip() == "":
771+
message["content"] = "."
772+
elif isinstance(content, list):
773+
if len(content) == 0:
774+
message["content"] = [{"type": "text", "text": "."}]
775+
continue
776+
for part in content:
777+
if (
778+
isinstance(part, dict)
779+
and part.get("type") == "text"
780+
and (part.get("text", "") or "").strip() == ""
781+
):
782+
part["text"] = "."
783+
return messages
784+
785+
def _prepare_provider_specific_messages(
786+
self, messages: list[dict], ctx: DeploymentContext
787+
) -> list[dict]:
788+
if ctx.deployment.provider == CloudProvider.BEDROCK:
789+
return self._prepare_bedrock_messages(messages)
790+
return messages
791+
763792
async def _prepare_chat(
764793
self,
765794
*,
@@ -1154,10 +1183,10 @@ async def _completion_stream(
11541183
with attempt:
11551184
async with self._get_deployment(messages=messages, **hyperparams) as ctx:
11561185
self._prepare_hyperparams(ctx, hyperparams)
1157-
# logger.warning(f"{hyperparams=}")
1186+
prepared_messages = self._prepare_provider_specific_messages(messages, ctx)
11581187
if ctx.use_openai_responses:
11591188
async for chunk in self._openai_responses_stream(
1160-
ctx, messages, **hyperparams
1189+
ctx, prepared_messages, **hyperparams
11611190
):
11621191
yield chunk
11631192
else:
@@ -1166,7 +1195,7 @@ async def _completion_stream(
11661195
api_key=ctx.api_key,
11671196
base_url=ctx.deployment.api_base or None,
11681197
model=ctx.routing_id,
1169-
messages=messages,
1198+
messages=prepared_messages,
11701199
stream=True,
11711200
stream_options={"include_usage": True},
11721201
**hyperparams,
@@ -1191,14 +1220,15 @@ async def _completion(
11911220
with attempt:
11921221
async with self._get_deployment(messages=messages, **hyperparams) as ctx:
11931222
self._prepare_hyperparams(ctx, hyperparams)
1223+
prepared_messages = self._prepare_provider_specific_messages(messages, ctx)
11941224
if ctx.use_openai_responses:
1195-
return await self._openai_responses(ctx, messages, **hyperparams)
1225+
return await self._openai_responses(ctx, prepared_messages, **hyperparams)
11961226
response = await acompletion(
11971227
timeout=self.config.timeout,
11981228
api_key=ctx.api_key,
11991229
base_url=ctx.deployment.api_base or None,
12001230
model=ctx.routing_id,
1201-
messages=messages,
1231+
messages=prepared_messages,
12021232
stream=False,
12031233
**hyperparams,
12041234
)

services/api/src/owl/utils/test.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,20 @@ def setup_projects():
395395
context_length=1280000,
396396
languages=["en"],
397397
)
398+
BEDROCK_CLAUDE_HAIKU_CONFIG = ModelConfigCreate(
399+
id="anthropic/claude-haiku-4-5-bedrock",
400+
name="Bedrock Claude 4.5 Haiku",
401+
type=ModelType.LLM,
402+
capabilities=[
403+
ModelCapability.CHAT,
404+
ModelCapability.IMAGE,
405+
ModelCapability.REASONING,
406+
ModelCapability.TOOL,
407+
],
408+
context_length=128000,
409+
languages=["en"],
410+
owned_by="anthropic",
411+
)
398412
ELLM_DESCRIBE_CONFIG = ModelConfigCreate(
399413
id="ellm/describe",
400414
name="Describe Message",
@@ -481,6 +495,13 @@ def setup_projects():
481495
routing_id=OPENAI_O4_MINI_CONFIG.id,
482496
api_base="",
483497
)
498+
BEDROCK_CLAUDE_HAIKU_DEPLOYMENT = DeploymentCreate(
499+
model_id=BEDROCK_CLAUDE_HAIKU_CONFIG.id,
500+
name=f"{BEDROCK_CLAUDE_HAIKU_CONFIG.name} Deployment",
501+
provider=CloudProvider.BEDROCK,
502+
routing_id="global.anthropic.claude-haiku-4-5-20251001-v1:0",
503+
api_base="",
504+
)
484505
ELLM_DESCRIBE_DEPLOYMENT = DeploymentCreate(
485506
model_id=ELLM_DESCRIBE_CONFIG.id,
486507
name=f"{ELLM_DESCRIBE_CONFIG.name} Deployment",

services/api/tests/gen_table/test_row_ops.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@
5454
ResourceNotFoundError,
5555
)
5656
from owl.utils.test import (
57+
BEDROCK_CLAUDE_HAIKU_CONFIG,
58+
BEDROCK_CLAUDE_HAIKU_DEPLOYMENT,
5759
ELLM_EMBEDDING_CONFIG,
5860
ELLM_EMBEDDING_DEPLOYMENT,
5961
GPT_4O_MINI_CONFIG,
@@ -146,6 +148,7 @@ def setup():
146148
create_model_config(GPT_4O_MINI_CONFIG),
147149
create_model_config(GPT_5_MINI_CONFIG),
148150
create_model_config(OPENAI_O4_MINI_CONFIG),
151+
create_model_config(BEDROCK_CLAUDE_HAIKU_CONFIG),
149152
create_model_config(
150153
{
151154
# "id": "openai/Qwen/Qwen-2-Audio-7B",
@@ -166,6 +169,7 @@ def setup():
166169
create_deployment(GPT_4O_MINI_DEPLOYMENT),
167170
create_deployment(GPT_5_MINI_DEPLOYMENT),
168171
create_deployment(OPENAI_O4_MINI_DEPLOYMENT),
172+
create_deployment(BEDROCK_CLAUDE_HAIKU_DEPLOYMENT),
169173
create_deployment(
170174
DeploymentCreate(
171175
model_id=llm_config_audio.id,
@@ -1761,6 +1765,60 @@ def test_chat_history_and_sequential_regen(
17611765
assert "8" in output, output
17621766

17631767

1768+
@pytest.mark.parametrize("table_type", TABLE_TYPES)
1769+
@pytest.mark.parametrize("stream", **STREAM_PARAMS)
1770+
def test_bedrock_multiturn_handle_blank_content(
1771+
setup: ServingContext,
1772+
table_type: TableType,
1773+
stream: bool,
1774+
):
1775+
client = JamAI(user_id=setup.user_id, project_id=setup.project_id)
1776+
cols = [
1777+
ColumnSchemaCreate(id="input", dtype="str"),
1778+
ColumnSchemaCreate(
1779+
id="output",
1780+
dtype="str",
1781+
gen_config=LLMGenConfig(
1782+
system_prompt="You are a calculator.",
1783+
prompt="${input}",
1784+
multi_turn=True,
1785+
temperature=0.001,
1786+
top_p=0.001,
1787+
max_tokens=1050, # higher than thinking.budget_tokens
1788+
model="anthropic/claude-haiku-4-5-bedrock",
1789+
reasoning_effort="low",
1790+
),
1791+
),
1792+
]
1793+
with _create_table(client, table_type, cols=cols) as table:
1794+
assert isinstance(table, TableMetaResponse)
1795+
# Initialise chat thread and set output format
1796+
response = client.table.add_table_rows(
1797+
table_type,
1798+
MultiRowAddRequest(
1799+
table_id=table.id,
1800+
data=[
1801+
dict(input="x = 0", output="0"),
1802+
dict(input="Add 1", output=""),
1803+
dict(input="Add 2", output=""),
1804+
dict(input="Add 1", output="4"),
1805+
],
1806+
stream=False,
1807+
),
1808+
)
1809+
# Test adding one row
1810+
response = client.table.add_table_rows(
1811+
table_type,
1812+
MultiRowAddRequest(
1813+
table_id=table.id,
1814+
data=[dict(input="Add 1")],
1815+
stream=stream,
1816+
),
1817+
)
1818+
output = _collect_text(response, "output")
1819+
assert "5" in output, output
1820+
1821+
17641822
@pytest.mark.parametrize("table_type", TABLE_TYPES)
17651823
@pytest.mark.parametrize("stream", **STREAM_PARAMS)
17661824
def test_convert_into_multi_turn(

services/api/tests/utils/test_lm.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,42 @@ def _make_vllm_context(*, is_reasoning_model: bool = True) -> DeploymentContext:
159159
)
160160

161161

162+
def test_prepare_bedrock_messages_should_replace_blank_content() -> None:
163+
messages = [
164+
{"role": "system", "content": ""},
165+
{"role": "user", "content": " "},
166+
{"role": "assistant", "content": ""},
167+
{
168+
"role": "user",
169+
"content": [
170+
{"type": "text", "text": ""},
171+
{"type": "image_url", "image_url": {"url": "data:image/png;base64,abc"}},
172+
],
173+
},
174+
{"role": "user", "content": []},
175+
{"role": "assistant", "content": "ok"},
176+
]
177+
178+
prepared = DeploymentRouter._prepare_bedrock_messages(messages)
179+
180+
assert prepared == [
181+
{"role": "system", "content": "."},
182+
{"role": "user", "content": "."},
183+
{"role": "assistant", "content": "."},
184+
{
185+
"role": "user",
186+
"content": [
187+
{"type": "text", "text": "."},
188+
{"type": "image_url", "image_url": {"url": "data:image/png;base64,abc"}},
189+
],
190+
},
191+
{"role": "user", "content": [{"type": "text", "text": "."}]},
192+
{"role": "assistant", "content": "ok"},
193+
]
194+
assert messages[0]["content"] == ""
195+
assert messages[3]["content"][0]["text"] == ""
196+
197+
162198
def test_inference_provider_should_prefer_vllm_cloud_over_owned_by() -> None:
163199
router = _make_router()
164200

0 commit comments

Comments
 (0)