3 Commits

Author SHA1 Message Date
RaresKeY 9844a2f9a1 chore(release): bump version to 1.0.2 2026-07-12 08:20:59 +02:00
RaresKeY 724305b65d fix(chat): require explicit web search enable
(cherry picked from commit 54f1d015b5)
2026-07-12 08:20:59 +02:00
Steve Holloway 543534924a fix(chat): restore missing _explicit_web_intent definition (#5290)
chat_stream() references `_explicit_web_intent` in three places
(disabled-tools gating, global-disabled web allowance, and the
per-turn tool filter) but the assignment was dropped during a
branch merge. Every chat request raised

    NameError: name '_explicit_web_intent' is not defined

at routes/chat_routes.py, surfacing to the client as a bare
"Internal Server Error" before any LLM call was made — chat was
fully broken on dev and main.

Restore the original definition, computed from the already-derived
tool intent, immediately before its first use:

    _explicit_web_intent = bool(_tool_intent and _tool_intent.category == "web")

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 2d8177035b)
2026-07-12 08:20:59 +02:00
6 changed files with 6 additions and 98 deletions
+2 -2
View File
@@ -3,9 +3,9 @@ uvicorn
python-multipart
python-dotenv
httpx
httpcore>=1.0.9,<2.0
httpcore>=1.0,<2.0
pydantic>=2.13.4
pydantic-settings>=2.14.2
pydantic-settings>=2.14.1
SQLAlchemy
pypdf
beautifulsoup4
+1 -1
View File
@@ -4,7 +4,7 @@ import os
from src.runtime_paths import get_app_root, get_default_data_dir
APP_VERSION = "1.0.1"
APP_VERSION = "1.0.2"
# Base paths
BASE_DIR = os.path.join(get_app_root(), "")
+1 -4
View File
@@ -230,10 +230,7 @@ def request_flags(messages) -> tuple:
"""
msgs = messages or []
last = msgs[-1] if msgs else None
# A message element can be a non-dict (clients send `"messages": ["hi"]`);
# the vision loop below already guards each element with isinstance, so do
# the same here rather than call .get() on a bare string.
agent = isinstance(last, dict) and last.get("role") != "user"
agent = bool(last) and last.get("role") != "user"
vision = False
for m in msgs:
content = m.get("content") if isinstance(m, dict) else None
+2 -5
View File
@@ -440,10 +440,7 @@ def build_user_content(
try:
with open(path, "rb") as image_file:
encoded_string = base64.b64encode(image_file.read()).decode("utf-8")
# Extensionless uploads (e.g. a pasted screenshot) have no ext,
# so fall back to the resolved MIME subtype rather than emitting
# an invalid "data:image/;base64," with an empty subtype.
image_format = ext[1:] or (mime.split("/", 1)[1] if mime.startswith("image/") else "png")
image_format = ext[1:]
content.append({
"type": "image_url",
"image_url": {"url": f"data:image/{image_format};base64,{encoded_string}"},
@@ -459,7 +456,7 @@ def build_user_content(
try:
with open(path, "rb") as audio_file:
encoded_string = base64.b64encode(audio_file.read()).decode("utf-8")
audio_format = ext[1:] or (mime.split("/", 1)[1] if mime.startswith("audio/") else "mpeg")
audio_format = ext[1:]
content.append({
"type": "audio",
"audio": {"url": f"data:audio/{audio_format};base64,{encoded_string}"},
-12
View File
@@ -89,18 +89,6 @@ def test_request_flags_vision():
assert vision is True
def test_request_flags_non_dict_last_message_does_not_crash():
# A client can send a bare-string (non-dict) last element; before the
# isinstance guard this raised AttributeError on last.get("role").
assert copilot.request_flags(["hi"]) == (False, False)
assert copilot.request_flags([{"role": "user"}, "trailing"]) == (False, False)
def test_request_flags_empty_and_none():
assert copilot.request_flags([]) == (False, False)
assert copilot.request_flags(None) == (False, False)
def test_apply_request_headers_mutates():
h = {"X-GitHub-Api-Version": "v"}
copilot.apply_request_headers(h, [{"role": "tool", "content": "x"}])
@@ -1,74 +0,0 @@
"""Regression: extensionless image/audio uploads must get a valid MIME subtype.
The data-URL subtype was derived only from the stored file's extension
(`image_format = ext[1:]`). A pasted screenshot or any file whose stored id
carries no extension yields `ext == ""`, so the emitted URL was
`data:image/;base64,...` — an empty MIME subtype (invalid per RFC 2046) that
vision/audio endpoints reject, silently dropping the attachment. When the
extension is missing, fall back to the resolved MIME subtype. Extensions that
are present are unchanged.
"""
class _Handler:
def __init__(self, uploads, image=False, audio=False):
self.uploads = uploads
self._image = image
self._audio = audio
def resolve_upload(self, fid, owner=None):
return self.uploads.get(fid)
def _inside_upload_dir(self, path):
return True
def is_image_file(self, name, mime):
return self._image and (mime or "").startswith("image/")
def is_audio_file(self, name, mime):
return self._audio and (mime or "").startswith("audio/")
def is_document_file(self, name, mime):
return False
def _blocks(content, block_type):
return [b for b in content if isinstance(b, dict) and b.get("type") == block_type]
def test_extensionless_image_uses_mime_subtype(tmp_path):
import src.document_processor as dp
p = tmp_path / ("a" * 32) # bare id, no extension
p.write_bytes(b"\x89PNG\r\n\x1a\nfake")
uploads = {"img": {"path": str(p), "name": "screenshot", "mime": "image/png"}}
content = dp.build_user_content("look", ["img"], str(tmp_path), _Handler(uploads, image=True), owner="t")
imgs = _blocks(content, "image_url")
assert imgs, content
assert imgs[0]["image_url"]["url"].startswith("data:image/png;base64,")
def test_extensionless_audio_uses_mime_subtype(tmp_path):
import src.document_processor as dp
p = tmp_path / ("b" * 32)
p.write_bytes(b"fakeaudio")
uploads = {"aud": {"path": str(p), "name": "recording", "mime": "audio/mpeg"}}
content = dp.build_user_content("listen", ["aud"], str(tmp_path), _Handler(uploads, audio=True), owner="t")
auds = _blocks(content, "audio")
assert auds, content
assert auds[0]["audio"]["url"].startswith("data:audio/mpeg;base64,")
def test_extension_present_is_unchanged(tmp_path):
import src.document_processor as dp
p = tmp_path / "pic.png"
p.write_bytes(b"\x89PNG\r\n\x1a\n")
uploads = {"img": {"path": str(p), "name": "pic.png", "mime": "image/png"}}
content = dp.build_user_content("look", ["img"], str(tmp_path), _Handler(uploads, image=True), owner="t")
imgs = _blocks(content, "image_url")
assert imgs[0]["image_url"]["url"].startswith("data:image/png;base64,")