mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 05:50:44 +08:00
feat(chat): image specialist legacy lane, Responses fixes, ACL + UI attachments
- Image expert: DashScope-style /chat/completions via image_legacy_client; early exit in direct_loop when skill_binding_role is image; shared placeholder helper; docs/IMAGE_SPECIALIST_LANE.md. - Strict attachment ACL: link_attachment_acl on assistant chat_message rows (sqlite_store); chat attachment rate limit when user_id empty; admin chat tests updated. - Admin chat UI: aggregate bubbles render assistant_text attachments (image_ref); WS expand path. - turn_runner: persisted_chat_attachments_nonempty for final_msg selection. - OpenAI Responses transport + agent_messages/agent_core_attempt adjustments; env docs and tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
d1bcc4debe
commit
faeb067856
21 changed files with 2516 additions and 331 deletions
106
runtime/operations/scripts/probe_openai_responses_image.py
Normal file
106
runtime/operations/scripts/probe_openai_responses_image.py
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
#!/usr/bin/env python3
|
||||
"""POST minimal vision payloads to an OpenAI-compatible ``/responses`` endpoint.
|
||||
|
||||
Run locally: loads ``oclaw/_local/system.env`` the same way the gateway does, then POSTs variants.
|
||||
|
||||
Example::
|
||||
|
||||
set OPENAI_BASE_URL=https://...
|
||||
set OPENAI_API_KEY=sk-...
|
||||
set OPENAI_MODEL=qwen-vl-plus
|
||||
python runtime/operations/scripts/probe_openai_responses_image.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
_REPO_ROOT = Path(__file__).resolve().parents[3]
|
||||
if str(_REPO_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(_REPO_ROOT))
|
||||
|
||||
# Same bootstrap as gateway: ``interfaces/http/fastapi_app.py`` calls ``load_system_env()`` so
|
||||
# ``oclaw/_local/system.env`` is merged before reading ``OPENAI_*``.
|
||||
try:
|
||||
from oclaw.platform.config.bootstrap_env import load_system_env
|
||||
|
||||
load_system_env()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
import httpx
|
||||
except ImportError:
|
||||
print("install httpx: pip install httpx", file=sys.stderr)
|
||||
raise SystemExit(2)
|
||||
|
||||
# 1x1 transparent PNG
|
||||
_PNG_B64 = (
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="
|
||||
)
|
||||
|
||||
|
||||
def _variants(model: str, data_uri: str) -> list[tuple[str, dict]]:
|
||||
user_block_resp = {
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "input_text", "text": "What color is this pixel image? One word."},
|
||||
{"type": "input_image", "image_url": data_uri, "detail": "auto"},
|
||||
],
|
||||
}
|
||||
user_plain_resp = {
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "input_text", "text": "What color is this pixel image? One word."},
|
||||
{"type": "input_image", "image_url": data_uri, "detail": "auto"},
|
||||
],
|
||||
}
|
||||
user_chat = {
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "What color is this pixel image? One word."},
|
||||
{"type": "image_url", "image_url": {"url": data_uri}},
|
||||
],
|
||||
}
|
||||
return [
|
||||
("flat_resp_envelope", {"model": model, "input": [user_block_resp]}),
|
||||
("flat_resp_plain", {"model": model, "input": [user_plain_resp]}),
|
||||
("nested_messages_resp_env", {"model": model, "input": {"messages": [user_block_resp]}}),
|
||||
("nested_messages_resp_plain", {"model": model, "input": {"messages": [user_plain_resp]}}),
|
||||
("nested_messages_chat", {"model": model, "input": {"messages": [user_chat]}}),
|
||||
]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
base = (os.getenv("OPENAI_BASE_URL") or "https://api.openai.com/v1").strip().rstrip("/")
|
||||
key = (os.getenv("OPENAI_API_KEY") or "").strip()
|
||||
model = (os.getenv("OPENAI_MODEL") or "gpt-4o-mini").strip()
|
||||
if not key:
|
||||
print("OPENAI_API_KEY is required", file=sys.stderr)
|
||||
raise SystemExit(2)
|
||||
|
||||
url = f"{base}/responses"
|
||||
data_uri = f"data:image/png;base64,{_PNG_B64}"
|
||||
headers = {"Authorization": f"Bearer {key}", "Content-Type": "application/json"}
|
||||
|
||||
print(f"POST {url}", flush=True)
|
||||
for label, body in _variants(model, data_uri):
|
||||
try:
|
||||
r = httpx.post(url, headers=headers, json=body, timeout=120.0)
|
||||
except httpx.HTTPError as exc:
|
||||
print(f"\n=== {label} transport_error {exc}", flush=True)
|
||||
continue
|
||||
tail = (r.text or "")[:2400]
|
||||
print(f"\n=== {label} status={r.status_code}", flush=True)
|
||||
print(tail, flush=True)
|
||||
if r.status_code < 400:
|
||||
print("(first successful variant — use this shape for your gateway)", flush=True)
|
||||
break
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue