Skip to content

Commit 04b846c

Browse files
dpageclaude
andcommitted
Address CodeRabbit review feedback for streaming and SQL extraction.
- Anthropic: preserve separators between text blocks in streaming to match _parse_response() behavior. - Docker: validate that the API URL points to a loopback address to constrain the request surface. - Docker/OpenAI: raise LLMClientError on empty streams instead of yielding blank LLMResponse objects, matching non-streaming behavior. - SQL extraction: strip trailing semicolons before joining blocks to avoid double semicolons in output. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent fc0af6f commit 04b846c

File tree

5 files changed

+45
-2
lines changed

5 files changed

+45
-2
lines changed

web/pgadmin/llm/providers/anthropic.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,7 @@ def _read_anthropic_stream(
382382
stop_reason_str = None
383383
model_name = self._model
384384
usage = Usage()
385+
in_text_block = False
385386

386387
while True:
387388
line_bytes = response.readline()
@@ -427,6 +428,13 @@ def _read_anthropic_stream(
427428
'name': block.get('name', '')
428429
}
429430
tool_input_json = ''
431+
elif block.get('type') == 'text':
432+
# Emit a separator between text blocks to
433+
# match _parse_response() which joins with '\n'
434+
if in_text_block:
435+
content_parts.append('\n')
436+
yield '\n'
437+
in_text_block = True
430438

431439
elif event_type == 'content_block_delta':
432440
delta = data.get('delta', {})

web/pgadmin/llm/providers/docker.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import json
1717
import socket
1818
import ssl
19+
import urllib.parse
1920
import urllib.request
2021
import urllib.error
2122
from collections.abc import Generator
@@ -43,6 +44,25 @@
4344
DEFAULT_API_URL = 'http://localhost:12434'
4445
DEFAULT_MODEL = 'ai/qwen3-coder'
4546

47+
# Allowed loopback hostnames for the Docker endpoint
48+
_LOOPBACK_HOSTS = {'localhost', '127.0.0.1', '::1', '[::1]'}
49+
50+
51+
def _validate_loopback_url(url: str) -> None:
52+
"""Ensure the URL uses HTTP(S) and points to a loopback address."""
53+
parsed = urllib.parse.urlparse(url)
54+
if parsed.scheme not in ('http', 'https'):
55+
raise ValueError(
56+
f"Docker Model Runner URL must use http or https, "
57+
f"got: {parsed.scheme}"
58+
)
59+
hostname = (parsed.hostname or '').lower()
60+
if hostname not in _LOOPBACK_HOSTS:
61+
raise ValueError(
62+
f"Docker Model Runner URL must point to a loopback address "
63+
f"(localhost/127.0.0.1/::1), got: {hostname}"
64+
)
65+
4666

4767
class DockerClient(LLMClient):
4868
"""
@@ -64,6 +84,7 @@ def __init__(
6484
model: Optional model name. Defaults to ai/qwen3-coder.
6585
"""
6686
self._api_url = (api_url or DEFAULT_API_URL).rstrip('/')
87+
_validate_loopback_url(self._api_url)
6788
self._model = model or DEFAULT_MODEL
6889

6990
@property
@@ -557,6 +578,13 @@ def _read_openai_stream(
557578
finish_reason or '', StopReason.UNKNOWN
558579
)
559580

581+
if not content and not tool_calls:
582+
raise LLMClientError(LLMError(
583+
message='No response content returned from API',
584+
provider=self.provider_name,
585+
retryable=False
586+
))
587+
560588
yield LLMResponse(
561589
content=content,
562590
tool_calls=tool_calls,

web/pgadmin/llm/providers/openai.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,13 @@ def _read_openai_stream(
547547
finish_reason or '', StopReason.UNKNOWN
548548
)
549549

550+
if not content and not tool_calls:
551+
raise LLMClientError(LLMError(
552+
message='No response content returned from API',
553+
provider=self.provider_name,
554+
retryable=False
555+
))
556+
550557
yield LLMResponse(
551558
content=content,
552559
tool_calls=tool_calls,

web/pgadmin/tools/sqleditor/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2927,7 +2927,7 @@ def generate():
29272927
re.DOTALL
29282928
)
29292929
sql = ';\n\n'.join(
2930-
block.strip() for block in sql_blocks
2930+
block.strip().rstrip(';') for block in sql_blocks
29312931
) if sql_blocks else None
29322932

29332933
# Fallback: try JSON format in case LLM ignored

web/pgadmin/tools/sqleditor/tests/test_nlq_chat.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ class NLQSqlExtractionTestCase(BaseTestGenerator):
197197
'Then get orders:\n\n'
198198
'```sql\nSELECT * FROM orders;\n```'
199199
),
200-
expected_sql='SELECT * FROM users;;\n\nSELECT * FROM orders;'
200+
expected_sql='SELECT * FROM users;\n\nSELECT * FROM orders'
201201
)),
202202
('SQL Extraction - pgsql language tag', dict(
203203
response_text='```pgsql\nSELECT 1;\n```',

0 commit comments

Comments
 (0)