Skip to content

Commit b210c08

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 92a2599 commit b210c08

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
@@ -392,6 +392,7 @@ def _read_anthropic_stream(
392392
stop_reason_str = None
393393
model_name = self._model
394394
usage = Usage()
395+
in_text_block = False
395396

396397
while True:
397398
line_bytes = response.readline()
@@ -437,6 +438,13 @@ def _read_anthropic_stream(
437438
'name': block.get('name', '')
438439
}
439440
tool_input_json = ''
441+
elif block.get('type') == 'text':
442+
# Emit a separator between text blocks to
443+
# match _parse_response() which joins with '\n'
444+
if in_text_block:
445+
content_parts.append('\n')
446+
yield '\n'
447+
in_text_block = True
440448

441449
elif event_type == 'content_block_delta':
442450
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
@@ -554,6 +575,13 @@ def _read_openai_stream(
554575
finish_reason or '', StopReason.UNKNOWN
555576
)
556577

578+
if not content and not tool_calls:
579+
raise LLMClientError(LLMError(
580+
message='No response content returned from API',
581+
provider=self.provider_name,
582+
retryable=False
583+
))
584+
557585
yield LLMResponse(
558586
content=content,
559587
tool_calls=tool_calls,

web/pgadmin/llm/providers/openai.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,13 @@ def _read_openai_stream(
559559
finish_reason or '', StopReason.UNKNOWN
560560
)
561561

562+
if not content and not tool_calls:
563+
raise LLMClientError(LLMError(
564+
message='No response content returned from API',
565+
provider=self.provider_name,
566+
retryable=False
567+
))
568+
562569
yield LLMResponse(
563570
content=content,
564571
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)