Skip to content

Commit 71e9ebc

Browse files
authored
Merge branch 'pewdiepie-archdaemon:dev' into feat/oidc-sso
2 parents 233d2f7 + a35384e commit 71e9ebc

5 files changed

Lines changed: 96 additions & 3 deletions

File tree

routes/chat_routes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -876,6 +876,7 @@ async def chat_stream(request: Request) -> StreamingResponse:
876876
# by default without having to send allow_bash in every request.
877877
if allow_bash is not None and str(allow_bash).lower() != "true":
878878
disabled_tools.add("bash")
879+
_explicit_web_intent = bool(_tool_intent and _tool_intent.category == "web")
879880
if (
880881
allow_web_search is not None
881882
and str(allow_web_search).lower() != "true"

src/copilot.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,10 @@ def request_flags(messages) -> tuple:
230230
"""
231231
msgs = messages or []
232232
last = msgs[-1] if msgs else None
233-
agent = bool(last) and last.get("role") != "user"
233+
# A message element can be a non-dict (clients send `"messages": ["hi"]`);
234+
# the vision loop below already guards each element with isinstance, so do
235+
# the same here rather than call .get() on a bare string.
236+
agent = isinstance(last, dict) and last.get("role") != "user"
234237
vision = False
235238
for m in msgs:
236239
content = m.get("content") if isinstance(m, dict) else None

src/document_processor.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,10 @@ def build_user_content(
440440
try:
441441
with open(path, "rb") as image_file:
442442
encoded_string = base64.b64encode(image_file.read()).decode("utf-8")
443-
image_format = ext[1:]
443+
# Extensionless uploads (e.g. a pasted screenshot) have no ext,
444+
# so fall back to the resolved MIME subtype rather than emitting
445+
# an invalid "data:image/;base64," with an empty subtype.
446+
image_format = ext[1:] or (mime.split("/", 1)[1] if mime.startswith("image/") else "png")
444447
content.append({
445448
"type": "image_url",
446449
"image_url": {"url": f"data:image/{image_format};base64,{encoded_string}"},
@@ -456,7 +459,7 @@ def build_user_content(
456459
try:
457460
with open(path, "rb") as audio_file:
458461
encoded_string = base64.b64encode(audio_file.read()).decode("utf-8")
459-
audio_format = ext[1:]
462+
audio_format = ext[1:] or (mime.split("/", 1)[1] if mime.startswith("audio/") else "mpeg")
460463
content.append({
461464
"type": "audio",
462465
"audio": {"url": f"data:audio/{audio_format};base64,{encoded_string}"},

tests/test_copilot.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,18 @@ def test_request_flags_vision():
8989
assert vision is True
9090

9191

92+
def test_request_flags_non_dict_last_message_does_not_crash():
93+
# A client can send a bare-string (non-dict) last element; before the
94+
# isinstance guard this raised AttributeError on last.get("role").
95+
assert copilot.request_flags(["hi"]) == (False, False)
96+
assert copilot.request_flags([{"role": "user"}, "trailing"]) == (False, False)
97+
98+
99+
def test_request_flags_empty_and_none():
100+
assert copilot.request_flags([]) == (False, False)
101+
assert copilot.request_flags(None) == (False, False)
102+
103+
92104
def test_apply_request_headers_mutates():
93105
h = {"X-GitHub-Api-Version": "v"}
94106
copilot.apply_request_headers(h, [{"role": "tool", "content": "x"}])
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""Regression: extensionless image/audio uploads must get a valid MIME subtype.
2+
3+
The data-URL subtype was derived only from the stored file's extension
4+
(`image_format = ext[1:]`). A pasted screenshot or any file whose stored id
5+
carries no extension yields `ext == ""`, so the emitted URL was
6+
`data:image/;base64,...` — an empty MIME subtype (invalid per RFC 2046) that
7+
vision/audio endpoints reject, silently dropping the attachment. When the
8+
extension is missing, fall back to the resolved MIME subtype. Extensions that
9+
are present are unchanged.
10+
"""
11+
12+
13+
class _Handler:
14+
def __init__(self, uploads, image=False, audio=False):
15+
self.uploads = uploads
16+
self._image = image
17+
self._audio = audio
18+
19+
def resolve_upload(self, fid, owner=None):
20+
return self.uploads.get(fid)
21+
22+
def _inside_upload_dir(self, path):
23+
return True
24+
25+
def is_image_file(self, name, mime):
26+
return self._image and (mime or "").startswith("image/")
27+
28+
def is_audio_file(self, name, mime):
29+
return self._audio and (mime or "").startswith("audio/")
30+
31+
def is_document_file(self, name, mime):
32+
return False
33+
34+
35+
def _blocks(content, block_type):
36+
return [b for b in content if isinstance(b, dict) and b.get("type") == block_type]
37+
38+
39+
def test_extensionless_image_uses_mime_subtype(tmp_path):
40+
import src.document_processor as dp
41+
42+
p = tmp_path / ("a" * 32) # bare id, no extension
43+
p.write_bytes(b"\x89PNG\r\n\x1a\nfake")
44+
uploads = {"img": {"path": str(p), "name": "screenshot", "mime": "image/png"}}
45+
46+
content = dp.build_user_content("look", ["img"], str(tmp_path), _Handler(uploads, image=True), owner="t")
47+
imgs = _blocks(content, "image_url")
48+
assert imgs, content
49+
assert imgs[0]["image_url"]["url"].startswith("data:image/png;base64,")
50+
51+
52+
def test_extensionless_audio_uses_mime_subtype(tmp_path):
53+
import src.document_processor as dp
54+
55+
p = tmp_path / ("b" * 32)
56+
p.write_bytes(b"fakeaudio")
57+
uploads = {"aud": {"path": str(p), "name": "recording", "mime": "audio/mpeg"}}
58+
59+
content = dp.build_user_content("listen", ["aud"], str(tmp_path), _Handler(uploads, audio=True), owner="t")
60+
auds = _blocks(content, "audio")
61+
assert auds, content
62+
assert auds[0]["audio"]["url"].startswith("data:audio/mpeg;base64,")
63+
64+
65+
def test_extension_present_is_unchanged(tmp_path):
66+
import src.document_processor as dp
67+
68+
p = tmp_path / "pic.png"
69+
p.write_bytes(b"\x89PNG\r\n\x1a\n")
70+
uploads = {"img": {"path": str(p), "name": "pic.png", "mime": "image/png"}}
71+
72+
content = dp.build_user_content("look", ["img"], str(tmp_path), _Handler(uploads, image=True), owner="t")
73+
imgs = _blocks(content, "image_url")
74+
assert imgs[0]["image_url"]["url"].startswith("data:image/png;base64,")

0 commit comments

Comments
 (0)