+5
-9
@@ -1,9 +1,7 @@
|
||||
"""The app module, containing the app factory function."""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from flask import Flask
|
||||
from rich.traceback import install as install_rich_traceback
|
||||
|
||||
from . import commands
|
||||
from .blueprint import blueprint
|
||||
@@ -16,9 +14,9 @@ def create_app(config_object="app.settings"):
|
||||
"""
|
||||
app = Flask(__name__.split(".")[0])
|
||||
app.config.from_object(config_object)
|
||||
configure_logging(app)
|
||||
register_commands(app)
|
||||
register_blueprints(app)
|
||||
configure_logger(app)
|
||||
return app
|
||||
|
||||
|
||||
@@ -34,8 +32,6 @@ def register_commands(app):
|
||||
app.cli.add_command(commands.lint)
|
||||
|
||||
|
||||
def configure_logger(app):
|
||||
"""Configure loggers."""
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
if not app.logger.handlers:
|
||||
app.logger.addHandler(handler)
|
||||
def configure_logging(app):
|
||||
"""Configure logging."""
|
||||
install_rich_traceback()
|
||||
|
||||
+5
-4
@@ -2,7 +2,9 @@
|
||||
|
||||
from functools import wraps
|
||||
|
||||
from flask import Response, current_app, request
|
||||
from flask import current_app, request
|
||||
|
||||
from .exceptions import CursorConfigurationError
|
||||
|
||||
|
||||
def valid_brearer_token():
|
||||
@@ -20,8 +22,8 @@ def require_auth(func):
|
||||
if valid_brearer_token():
|
||||
return func(*args, **kwargs)
|
||||
else:
|
||||
error_message = (
|
||||
"\nAuthentication with Cursor-Azure-GPT-5 service failed.\n\n"
|
||||
raise CursorConfigurationError(
|
||||
"Authentication with Cursor-Azure-GPT-5 service failed.\n\n"
|
||||
"These value of:\n"
|
||||
"\tCursor Settings > Models > API Keys > OpenAI API Key\n\n"
|
||||
"Must match the value of:\n"
|
||||
@@ -29,6 +31,5 @@ def require_auth(func):
|
||||
"Ensure the values match exactly, and try again.\n"
|
||||
"If modifying the .env file, restart the service for the changes to apply."
|
||||
)
|
||||
return Response(error_message, 400)
|
||||
|
||||
return wrapper
|
||||
|
||||
+10
-13
@@ -64,19 +64,16 @@ class AzureAdapter:
|
||||
resp_content = resp.text
|
||||
|
||||
body = request_kwargs.get("json", {})
|
||||
if "instructions" in body:
|
||||
body["instructions"] = body["instructions"][:7] + "..."
|
||||
|
||||
if "tools" in body:
|
||||
body["tools"] = f"...redacted {len(body['tools'])} tools..."
|
||||
|
||||
if "input" in body:
|
||||
body["input"] = f"...redacted {len(body['input'])} input items..."
|
||||
|
||||
if "prompt_cache_key" in body:
|
||||
body["prompt_cache_key"] = re.sub(
|
||||
r"(...)(.*)(...)", "\\1***\\3", body["prompt_cache_key"]
|
||||
)
|
||||
body["instructions"] = body.get("instructions", "no instructions")[:16] + "..."
|
||||
body["tools"] = f"...redacted {len(body.get('tools', 'no tools'))} tools..."
|
||||
body["input"] = (
|
||||
f"...redacted {len(body.get('input', 'no input'))} input items..."
|
||||
)
|
||||
body["prompt_cache_key"] = re.sub(
|
||||
r"(...)(.*)(...)",
|
||||
"\\1***\\3",
|
||||
body.get("prompt_cache_key", "no prompt_cache_key"),
|
||||
)
|
||||
report = {
|
||||
"endpoint": re.sub(
|
||||
r"(//.)(.*?)(.\.)", "\\1***\\3", request_kwargs.get("url")
|
||||
|
||||
@@ -10,6 +10,8 @@ from typing import Any, Dict, List
|
||||
|
||||
from flask import Request, current_app
|
||||
|
||||
from ..exceptions import CursorConfigurationError, ServiceConfigurationError
|
||||
|
||||
|
||||
class RequestAdapter:
|
||||
"""Handle pre-request adaptation for the Azure Responses API.
|
||||
@@ -43,11 +45,9 @@ class RequestAdapter:
|
||||
|
||||
for m in messages:
|
||||
role = m.get("role")
|
||||
c = m.get("content")
|
||||
content = m.get("content")
|
||||
if role in {"system", "developer"}:
|
||||
text = c
|
||||
if text:
|
||||
instructions_parts.append(text)
|
||||
instructions_parts.append(content)
|
||||
continue
|
||||
# For user/assistant/tools as inputs
|
||||
if role == "tool":
|
||||
@@ -55,19 +55,18 @@ class RequestAdapter:
|
||||
|
||||
item = {
|
||||
"type": "function_call_output",
|
||||
"output": c,
|
||||
"output": content,
|
||||
"status": "completed",
|
||||
"call_id": call_id,
|
||||
}
|
||||
input_items.append(item)
|
||||
else:
|
||||
text = c
|
||||
item = {
|
||||
"role": role or "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_text" if role == "user" else "output_text",
|
||||
"text": text,
|
||||
"text": content,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -87,26 +86,22 @@ class RequestAdapter:
|
||||
|
||||
instructions = "\n\n".join(instructions_parts) if instructions_parts else None
|
||||
return {
|
||||
"input": input_items if input_items else None,
|
||||
"instructions": instructions,
|
||||
"input": input_items if input_items else None,
|
||||
}
|
||||
|
||||
def _transform_tools_for_responses(self, tools: Any) -> Any:
|
||||
out: List[Dict[str, Any]] = []
|
||||
for t in tools:
|
||||
ttype = t.get("type")
|
||||
if ttype == "function" and isinstance(t.get("function"), dict):
|
||||
f = t["function"]
|
||||
transformed: Dict[str, Any] = {
|
||||
"type": "function",
|
||||
"name": f.get("name"),
|
||||
}
|
||||
if "description" in f:
|
||||
transformed["description"] = f["description"]
|
||||
if "parameters" in f:
|
||||
transformed["parameters"] = f["parameters"]
|
||||
transformed["strict"] = False
|
||||
out.append(transformed)
|
||||
for tool in tools:
|
||||
function = tool.get("function")
|
||||
transformed: Dict[str, Any] = {
|
||||
"type": "function",
|
||||
"name": function.get("name"),
|
||||
"description": function.get("description"),
|
||||
"parameters": function.get("parameters"),
|
||||
"strict": False,
|
||||
}
|
||||
out.append(transformed)
|
||||
return out
|
||||
|
||||
# ---- Main adaptation (always streaming completions-like) ----
|
||||
@@ -134,39 +129,31 @@ class RequestAdapter:
|
||||
|
||||
# Map Chat/Completions to Responses (always streaming)
|
||||
messages = payload.get("messages") or []
|
||||
tools_in = payload.get("tools") or []
|
||||
tool_choice_in = payload.get("tool_choice")
|
||||
prompt_cache_key = payload.get("user") or payload.get("prompt_cache_key")
|
||||
|
||||
mapped = (
|
||||
responses_body = (
|
||||
self._messages_to_responses_input_and_instructions(messages)
|
||||
if isinstance(messages, list)
|
||||
else {"input": None, "instructions": None}
|
||||
)
|
||||
|
||||
responses_body: Dict[str, Any] = {}
|
||||
if mapped.get("instructions"):
|
||||
responses_body["instructions"] = mapped["instructions"]
|
||||
if mapped.get("input") is not None:
|
||||
responses_body["input"] = mapped["input"]
|
||||
responses_body["model"] = settings["AZURE_DEPLOYMENT"]
|
||||
|
||||
# Transform tools and tool choice
|
||||
if tools_in:
|
||||
responses_body["tools"] = self._transform_tools_for_responses(tools_in)
|
||||
if tool_choice_in is not None:
|
||||
responses_body["tool_choice"] = tool_choice_in
|
||||
responses_body["tools"] = self._transform_tools_for_responses(
|
||||
payload.get("tools", [])
|
||||
)
|
||||
responses_body["tool_choice"] = payload.get("tool_choice")
|
||||
|
||||
if prompt_cache_key is not None:
|
||||
responses_body["prompt_cache_key"] = prompt_cache_key
|
||||
responses_body["prompt_cache_key"] = payload.get("user")
|
||||
|
||||
# Always streaming
|
||||
responses_body["stream"] = True
|
||||
|
||||
reasoning_effort = inbound_model.replace("gpt-", "").lower()
|
||||
if reasoning_effort not in {"high", "medium", "low", "minimal"}:
|
||||
raise ValueError(
|
||||
"Model name must be either gpt-high, gpt-medium, gpt-low, or gpt-minimal"
|
||||
raise CursorConfigurationError(
|
||||
"Model name must be either gpt-high, gpt-medium, gpt-low, or gpt-minimal."
|
||||
f"\n\nGot: {inbound_model}"
|
||||
)
|
||||
|
||||
responses_body["reasoning"] = {
|
||||
@@ -177,6 +164,11 @@ class RequestAdapter:
|
||||
# but allowing it for now to be able to test it on other models
|
||||
if settings["AZURE_SUMMARY_LEVEL"] in {"auto", "detailed", "concise"}:
|
||||
responses_body["reasoning"]["summary"] = settings["AZURE_SUMMARY_LEVEL"]
|
||||
else:
|
||||
raise ServiceConfigurationError(
|
||||
"AZURE_SUMMARY_LEVEL must be either auto, detailed, or concise."
|
||||
f"\n\nGot: {settings['AZURE_SUMMARY_LEVEL']}"
|
||||
)
|
||||
|
||||
# No need to pass verbosity if it's set to medium, as it's the model's default
|
||||
if settings["AZURE_VERBOSITY_LEVEL"] in {"low", "high"}:
|
||||
|
||||
@@ -15,6 +15,9 @@ from flask import Response, stream_with_context
|
||||
|
||||
from ..common.sse import chunks_to_sse, sse_to_events
|
||||
|
||||
# Centralized events that should end a <think> block before handling
|
||||
THINKING_STOP_EVENTS = {"response.output_text.delta", "response.output_item.added"}
|
||||
|
||||
|
||||
class ResponseAdapter:
|
||||
"""Handle post-request adaptation from Azure Responses API to Flask.
|
||||
@@ -64,115 +67,84 @@ class ResponseAdapter:
|
||||
# ---- Event handlers (per SSE event) ----
|
||||
def _output_item__added(
|
||||
self, obj: Optional[Dict[str, Any]]
|
||||
) -> Iterable[Dict[str, Any]]:
|
||||
"""Handle response.output_item.added events and emit chunks as needed."""
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Handle response.output_item.added events and emit a single chunk."""
|
||||
|
||||
item_type = obj.get("item", {}).get("type")
|
||||
if item_type == "reasoning":
|
||||
self._thinking = True
|
||||
return [
|
||||
self._build_completion_chunk(
|
||||
delta={"role": "assistant", "content": "<think>\n\n"}
|
||||
)
|
||||
]
|
||||
return self._build_completion_chunk(
|
||||
delta={"role": "assistant", "content": "<think>\n\n"}
|
||||
)
|
||||
if item_type == "function_call":
|
||||
out: list[Dict[str, Any]] = []
|
||||
if self._thinking:
|
||||
out.append(
|
||||
self._build_completion_chunk(
|
||||
delta={"role": "assistant", "content": "</think>\n\n"}
|
||||
)
|
||||
)
|
||||
self._thinking = False
|
||||
self._tool_calls += 1
|
||||
name = obj.get("item", {}).get("name")
|
||||
arguments = obj.get("item", {}).get("arguments")
|
||||
call_id = obj.get("item", {}).get("call_id")
|
||||
out.append(
|
||||
self._build_completion_chunk(
|
||||
delta={
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"index": self._tool_calls - 1,
|
||||
"id": call_id or "",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": name or "",
|
||||
"arguments": arguments or "",
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
)
|
||||
self._called_function = True
|
||||
return out
|
||||
return []
|
||||
|
||||
def _function_call_arguments__delta(
|
||||
self, obj: Optional[Dict[str, Any]]
|
||||
) -> Iterable[Dict[str, Any]]:
|
||||
"""Handle response.function_call.arguments.delta events."""
|
||||
out: list[Dict[str, Any]] = []
|
||||
arguments_delta = obj.get("delta", "") if isinstance(obj, dict) else ""
|
||||
out.append(
|
||||
self._build_completion_chunk(
|
||||
return self._build_completion_chunk(
|
||||
delta={
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"index": self._tool_calls - 1,
|
||||
"function": {"arguments": arguments_delta},
|
||||
"id": call_id or "",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": name or "",
|
||||
"arguments": arguments or "",
|
||||
},
|
||||
}
|
||||
]
|
||||
],
|
||||
}
|
||||
)
|
||||
return None
|
||||
|
||||
def _function_call_arguments__delta(
|
||||
self, obj: Optional[Dict[str, Any]]
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Handle response.function_call.arguments.delta events."""
|
||||
arguments_delta = obj.get("delta", "") if isinstance(obj, dict) else ""
|
||||
return self._build_completion_chunk(
|
||||
delta={
|
||||
"tool_calls": [
|
||||
{
|
||||
"index": self._tool_calls - 1,
|
||||
"function": {"arguments": arguments_delta},
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
def _reasoning_summary_text__delta(
|
||||
self, obj: Optional[Dict[str, Any]]
|
||||
) -> Iterable[Dict[str, Any]]:
|
||||
"""Handle reasoning.summary_text.delta events and emit text chunks."""
|
||||
return [
|
||||
self._build_completion_chunk(
|
||||
delta={
|
||||
"role": "assistant",
|
||||
"content": (obj.get("delta", "") if isinstance(obj, dict) else ""),
|
||||
}
|
||||
)
|
||||
]
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Handle reasoning.summary_text.delta events and emit text chunk."""
|
||||
return self._build_completion_chunk(
|
||||
delta={
|
||||
"role": "assistant",
|
||||
"content": (obj.get("delta", "") if isinstance(obj, dict) else ""),
|
||||
}
|
||||
)
|
||||
|
||||
def _reasoning_summary_text__done(
|
||||
self, obj: Optional[Dict[str, Any]]
|
||||
) -> Iterable[Dict[str, Any]]:
|
||||
"""Handle reasoning.summary_text.done events and close think block."""
|
||||
return [
|
||||
self._build_completion_chunk(delta={"role": "assistant", "content": "\n\n"})
|
||||
]
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Handle reasoning.summary_text.done events and close with blank line inside think block."""
|
||||
return self._build_completion_chunk(
|
||||
delta={"role": "assistant", "content": "\n\n"}
|
||||
)
|
||||
|
||||
def _output_text__delta(
|
||||
self, obj: Optional[Dict[str, Any]]
|
||||
) -> Iterable[Dict[str, Any]]:
|
||||
"""Handle response.output_text.delta events and emit text chunks."""
|
||||
out: list[Dict[str, Any]] = []
|
||||
if self._thinking:
|
||||
out.append(
|
||||
self._build_completion_chunk(
|
||||
delta={"role": "assistant", "content": "</think>\n\n"}
|
||||
)
|
||||
)
|
||||
self._thinking = False
|
||||
out.append(
|
||||
self._build_completion_chunk(
|
||||
delta={
|
||||
"role": "assistant",
|
||||
"content": (obj.get("delta", "") if isinstance(obj, dict) else ""),
|
||||
}
|
||||
)
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Handle response.output_text.delta events and emit text chunk."""
|
||||
return self._build_completion_chunk(
|
||||
delta={
|
||||
"role": "assistant",
|
||||
"content": (obj.get("delta", "") if isinstance(obj, dict) else ""),
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
def adapt(self, upstream_resp: Any) -> Response:
|
||||
"""Adapt an upstream Azure streaming response into SSE for Flask."""
|
||||
@@ -196,10 +168,17 @@ class ResponseAdapter:
|
||||
handler = getattr(self, handler_name, None)
|
||||
if not handler:
|
||||
continue
|
||||
|
||||
# Centrally close <think> blocks whenever a stop event is seen
|
||||
if self._thinking and (ev.event in THINKING_STOP_EVENTS):
|
||||
yield self._build_completion_chunk(
|
||||
delta={"role": "assistant", "content": "</think>\n\n"}
|
||||
)
|
||||
self._thinking = False
|
||||
|
||||
res = handler(ev.json)
|
||||
if res is not None:
|
||||
for chunk in res:
|
||||
yield chunk
|
||||
yield res
|
||||
finally:
|
||||
# Emit finish reason at the end of stream
|
||||
if self._tool_calls > 0:
|
||||
|
||||
+7
-25
@@ -4,11 +4,7 @@ This module defines the application blueprint, configures logging, and
|
||||
forwards incoming HTTP requests to the configured backend implementation.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from flask import Blueprint, jsonify, request
|
||||
from loguru import logger
|
||||
from rich.traceback import install as install_rich_traceback
|
||||
|
||||
from .auth import require_auth
|
||||
from .azure.adapter import AzureAdapter
|
||||
@@ -18,30 +14,10 @@ from .common.recording import (
|
||||
init_last_recording,
|
||||
record_payload,
|
||||
)
|
||||
from .exceptions import ConfigurationError
|
||||
|
||||
blueprint = Blueprint("blueprint", __name__)
|
||||
|
||||
# Pretty tracebacks for easier debugging
|
||||
install_rich_traceback(show_locals=False)
|
||||
|
||||
|
||||
# Configure Loguru to print colorful logs to stdout
|
||||
logger.remove()
|
||||
logger.add(
|
||||
sys.stdout,
|
||||
colorize=True,
|
||||
enqueue=False,
|
||||
backtrace=False,
|
||||
diagnose=False,
|
||||
format=(
|
||||
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> "
|
||||
"| <level>{level: <8}</level> "
|
||||
"| <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> "
|
||||
"- <level>{message}</level>"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
ALL_METHODS = [
|
||||
"GET",
|
||||
"POST",
|
||||
@@ -103,3 +79,9 @@ def models():
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@blueprint.errorhandler(ConfigurationError)
|
||||
def configuration_error(e: ConfigurationError):
|
||||
"""Return a 400 JSON error payload for ValueError."""
|
||||
return e.get_response_content(), 400
|
||||
|
||||
+2
-10
@@ -41,13 +41,6 @@ def test(coverage, filter):
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option(
|
||||
"-f",
|
||||
"--fix-imports",
|
||||
default=True,
|
||||
is_flag=True,
|
||||
help="Fix imports using isort, before linting",
|
||||
)
|
||||
@click.option(
|
||||
"-c",
|
||||
"--check",
|
||||
@@ -55,7 +48,7 @@ def test(coverage, filter):
|
||||
is_flag=True,
|
||||
help="Don't make any changes to files, just confirm they are formatted correctly",
|
||||
)
|
||||
def lint(fix_imports, check):
|
||||
def lint(check):
|
||||
"""Lint and check code style with black, flake8 and isort."""
|
||||
skip = [
|
||||
"requirements",
|
||||
@@ -85,7 +78,6 @@ def lint(fix_imports, check):
|
||||
if check:
|
||||
isort_args.append("--check")
|
||||
black_args.append("--check")
|
||||
if fix_imports:
|
||||
execute_tool("Fixing import order", "isort", *isort_args)
|
||||
execute_tool("Fixing import order", "isort", *isort_args)
|
||||
execute_tool("Formatting style", "black", *black_args)
|
||||
execute_tool("Checking code style", "flake8")
|
||||
|
||||
+147
-121
@@ -2,22 +2,28 @@
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import uuid
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from flask import Request
|
||||
from rich.console import Console
|
||||
from rich import box
|
||||
from rich.console import Console, Group
|
||||
from rich.json import JSON
|
||||
from rich.markdown import Markdown
|
||||
from rich.padding import Padding
|
||||
from rich.panel import Panel
|
||||
from rich.table import Table
|
||||
|
||||
# Global console instance for consistent logging across modules
|
||||
console = Console()
|
||||
|
||||
|
||||
# --- Request logging helpers ---
|
||||
ROLE_COLORS = {
|
||||
"tool": "magenta",
|
||||
"system": "yellow",
|
||||
"user": "cyan",
|
||||
"assistant": "light_green",
|
||||
}
|
||||
|
||||
|
||||
def should_redact() -> bool:
|
||||
@@ -92,6 +98,13 @@ def _capture_request_details(req: Request, request_id: str) -> Dict[str, Any]:
|
||||
return details
|
||||
|
||||
|
||||
def escape_tags(text: str) -> str:
|
||||
"""Escapes xml-like tags in text so that they are visible when rendered as Markdown."""
|
||||
return re.sub(
|
||||
"(<[^<\n]+?)(>)", "\\1>`\n", re.sub("(<)([^>\n]+?>)", "\n`<\\2", text)
|
||||
).replace(">`\n\n\n`<", ">`\n\n`<")
|
||||
|
||||
|
||||
def log_request(req: Request) -> str:
|
||||
"""Pretty-print a Flask request using Rich and return the request id."""
|
||||
request_id = uuid.uuid4().hex[:8]
|
||||
@@ -103,132 +116,145 @@ def log_request(req: Request) -> str:
|
||||
|
||||
# Rich pretty print of the full request details
|
||||
console.rule(f"[bold]Request #{rid}[/bold] — {method} {path}")
|
||||
console.print(Panel.fit("Headers"))
|
||||
console.print(details.get("headers"))
|
||||
console.print(Panel.fit("Args / Form / JSON"))
|
||||
json_payload = details.get("json")
|
||||
cleaned_json = json_payload
|
||||
if isinstance(json_payload, dict):
|
||||
# Remove verbose fields to log them separately
|
||||
cleaned_json = {
|
||||
k: v
|
||||
for k, v in json_payload.items()
|
||||
if k
|
||||
not in {
|
||||
"tools",
|
||||
}
|
||||
}
|
||||
|
||||
# Remove verbose fields to log them separately
|
||||
cleaned_json = {
|
||||
k: v if k not in {"tools", "messages"} else "Pretty-printed below ↓"
|
||||
for k, v in json_payload.items()
|
||||
}
|
||||
console.print_json(
|
||||
data={
|
||||
"query_args": details.get("query_args"),
|
||||
"form": details.get("form"),
|
||||
"json": cleaned_json,
|
||||
}
|
||||
data=cleaned_json,
|
||||
indent=None,
|
||||
)
|
||||
|
||||
# Separate section for chat messages (role + content)
|
||||
messages = []
|
||||
if isinstance(json_payload, dict):
|
||||
maybe_messages = json_payload.get("messages")
|
||||
if isinstance(maybe_messages, list):
|
||||
messages = maybe_messages
|
||||
messages = json_payload.get("messages", [])
|
||||
tools = json_payload.get("tools", [])
|
||||
|
||||
if messages:
|
||||
console.rule(f"Messages ({len(messages)})")
|
||||
for idx, tool in enumerate(tools, start=1):
|
||||
table = Table(
|
||||
caption="[italic]Required fields are marked with *[/italic]",
|
||||
pad_edge=False,
|
||||
box=box.SIMPLE,
|
||||
leading=2,
|
||||
)
|
||||
|
||||
for idx, msg in enumerate(messages, start=1):
|
||||
role = ""
|
||||
content_val: Any = ""
|
||||
name = None
|
||||
if isinstance(msg, dict):
|
||||
role = str(msg.get("role", ""))
|
||||
content_val = msg.get("content", "")
|
||||
name = msg.get("name")
|
||||
tool_call_id = msg.get("tool_call_id")
|
||||
title = (
|
||||
f"Message {idx}: {role}"
|
||||
if not name
|
||||
else f"Message {idx}: {role} ({name}) - {tool_call_id}"
|
||||
table.add_column("Name", justify="right", style="cyan", no_wrap=True)
|
||||
table.add_column("Description", style="white")
|
||||
|
||||
for tool in tools:
|
||||
function = tool.get("function")
|
||||
required = function.get("parameters").get("required")
|
||||
|
||||
params_table = Table(
|
||||
show_header=False,
|
||||
show_edge=True,
|
||||
show_lines=True,
|
||||
title="Parameters",
|
||||
title_justify="left",
|
||||
expand=True,
|
||||
leading=3,
|
||||
)
|
||||
console.rule(title)
|
||||
console.print(
|
||||
Padding(
|
||||
Markdown(
|
||||
content_val.replace("<", "\n`<")
|
||||
.replace(">", ">`\n")
|
||||
.replace(">`\n\n\n`<", ">`\n\n`<")
|
||||
),
|
||||
(1, 0),
|
||||
params_table.add_column("Name", justify="right", no_wrap=True, style="cyan")
|
||||
params_table.add_column("Description", style="white")
|
||||
for param_name, param_value in (
|
||||
function.get("parameters").get("properties").items()
|
||||
):
|
||||
param_type = param_value.get("type")
|
||||
if param_type == "array":
|
||||
param_type += f"({param_value.get('items').get('type')})"
|
||||
if param_name in required:
|
||||
param_type = f"[bold]*{param_type}[/bold]"
|
||||
param_type = f"[magenta]{param_type}[/magenta]"
|
||||
param_name = f"{param_name}"
|
||||
params_table.add_row(
|
||||
Group(param_name, param_type),
|
||||
f"{param_value.get("description")}",
|
||||
)
|
||||
table.add_row(
|
||||
f"{function.get('name')}",
|
||||
Group(
|
||||
Markdown(escape_tags(function.get("description"))),
|
||||
params_table,
|
||||
),
|
||||
)
|
||||
|
||||
console.print(
|
||||
Panel(
|
||||
table,
|
||||
title=f"[italic]{0}/{len(messages)}[/italic] [bold]<tools>[/bold]",
|
||||
title_align="left",
|
||||
subtitle="[bold]</tools>[/bold]",
|
||||
subtitle_align="right",
|
||||
border_style=ROLE_COLORS["tool"],
|
||||
)
|
||||
)
|
||||
|
||||
for idx, msg in enumerate(messages, start=1):
|
||||
role = str(msg.get("role", ""))
|
||||
content_val = msg.get("content", "")
|
||||
name = msg.get("name")
|
||||
tool_call_id = msg.get("tool_call_id")
|
||||
message_title = (
|
||||
f"[italic]{idx}/{len(messages)}[/italic] [bold]<{role}>[/bold]"
|
||||
if not name
|
||||
else f"[italic]{idx}/{len(messages)}[/italic] [bold]<{role} name={name} id={tool_call_id}>[/bold]"
|
||||
)
|
||||
message_elements = []
|
||||
message_elements.append(
|
||||
Padding(
|
||||
Markdown(
|
||||
escape_tags(content_val),
|
||||
),
|
||||
(1, 0),
|
||||
)
|
||||
)
|
||||
tool_calls = msg.get("tool_calls", [])
|
||||
for tool_call in tool_calls:
|
||||
function = tool_call.get("function", {})
|
||||
arguments = function.get("arguments")
|
||||
tool_elements = []
|
||||
tool_title = f"[bold]<tool_call id={tool_call.get('id')}>[/bold]"
|
||||
tool_elements.append(
|
||||
f"[bold][magenta]{function.get('name')}[/magenta] [blue]([/blue][/bold]",
|
||||
)
|
||||
try:
|
||||
tool_elements.append(Padding(JSON(arguments), (0, 4)))
|
||||
except json.JSONDecodeError:
|
||||
tool_elements.append(
|
||||
Padding("[red]Invalid JSON generated by the model:[/red]", (0, 4))
|
||||
)
|
||||
tool_elements.append(arguments)
|
||||
tool_elements.append("[bold][blue])[/blue][/bold]")
|
||||
message_elements.append(
|
||||
Panel(
|
||||
Group(*tool_elements),
|
||||
title=tool_title,
|
||||
border_style=ROLE_COLORS["tool"],
|
||||
)
|
||||
)
|
||||
tool_calls = msg.get("tool_calls", [])
|
||||
for tool_call in tool_calls:
|
||||
function = tool_call.get("function", {})
|
||||
arguments = function.get("arguments")
|
||||
console.print(
|
||||
Padding(
|
||||
Panel.fit(f"Tool call [italic]{tool_call.get('id')}[italic]"),
|
||||
(0, 4),
|
||||
)
|
||||
message_style = ROLE_COLORS.get(role, "red")
|
||||
message_subtitle = message_title[message_title.find("<") :].replace("<", "</")
|
||||
message_subtitle = (
|
||||
"[bold]"
|
||||
+ message_subtitle[
|
||||
: (
|
||||
message_subtitle.find(" ")
|
||||
if " " in message_subtitle
|
||||
else message_subtitle.find(">")
|
||||
)
|
||||
console.print(
|
||||
Padding(
|
||||
f"[bold][magenta]{function.get('name')}[/magenta] ([/bold]",
|
||||
(0, 4),
|
||||
)
|
||||
)
|
||||
if arguments:
|
||||
try:
|
||||
console.print(Padding(JSON(arguments), (0, 8)))
|
||||
except json.JSONDecodeError:
|
||||
console.print("[red]Invalid JSON generated by the model:[/red]")
|
||||
print(arguments)
|
||||
console.print(Padding("[bold])[/bold]", (0, 4)))
|
||||
if tool_calls:
|
||||
console.print()
|
||||
]
|
||||
+ ">[/bold]"
|
||||
)
|
||||
console.print(
|
||||
Panel(
|
||||
Group(*message_elements),
|
||||
title=message_title,
|
||||
title_align="left",
|
||||
subtitle=message_subtitle,
|
||||
subtitle_align="right",
|
||||
border_style=message_style,
|
||||
)
|
||||
)
|
||||
|
||||
return request_id
|
||||
|
||||
|
||||
# --- SSE logging helpers, keeping for future use if we enable SSE logging ---
|
||||
# from .sse import SSEEvent
|
||||
# def _clean_payload(obj: Any) -> Any:
|
||||
# """Default cleaning to reduce noisy fields in logs.
|
||||
|
||||
# - If obj is a dict, remove top-level 'tools'
|
||||
# - If it contains a nested 'response' dict, also remove its 'tools'
|
||||
# Returns a shallow-cleaned copy when applicable; otherwise returns the input unchanged.
|
||||
# """
|
||||
# if not isinstance(obj, dict):
|
||||
# return obj
|
||||
# # Shallow copy top-level
|
||||
# cleaned = {k: v for k, v in obj.items()}
|
||||
# if "tools" in cleaned:
|
||||
# cleaned = {k: v for k, v in cleaned.items() if k != "tools"}
|
||||
# resp = cleaned.get("response")
|
||||
# if isinstance(resp, dict) and "tools" in resp:
|
||||
# # Shallow copy nested response to drop tools
|
||||
# new_resp = {k: v for k, v in resp.items() if k != "tools"}
|
||||
# cleaned = {**cleaned, "response": new_resp}
|
||||
# return cleaned
|
||||
|
||||
|
||||
# def log_event(ev: SSEEvent) -> None:
|
||||
# """Pretty-print one SSE event using Rich.
|
||||
|
||||
# - Title reflects whether the event had an 'event' name and its index
|
||||
# - If payload parses as JSON (ev.json), it is cleaned and printed as JSON; otherwise raw text is printed
|
||||
# """
|
||||
# obj = ev.json
|
||||
# if obj is not None:
|
||||
# title = (
|
||||
# f"SSE JSON #{ev.index}" if not ev.event else f"SSE {ev.event} #{ev.index}"
|
||||
# )
|
||||
# console.print(Panel.fit(title))
|
||||
# console.print_json(data=_clean_payload(obj))
|
||||
# else:
|
||||
# title = f"SSE data #{ev.index}"
|
||||
# if ev.event:
|
||||
# title = f"SSE {ev.event} #{ev.index}"
|
||||
# console.print(Panel.fit(title))
|
||||
# console.print(ev.data)
|
||||
|
||||
@@ -12,7 +12,7 @@ import re
|
||||
from functools import wraps
|
||||
from typing import Any, Dict
|
||||
|
||||
from flask import current_app, has_app_context
|
||||
from flask import current_app
|
||||
|
||||
RECORDINGS_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "recordings")
|
||||
|
||||
@@ -25,9 +25,7 @@ def config_bypass(func):
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
enabled = False
|
||||
if has_app_context():
|
||||
enabled = current_app.config["RECORD_TRAFFIC"]
|
||||
enabled = current_app.config["RECORD_TRAFFIC"]
|
||||
if not enabled:
|
||||
return None
|
||||
return func(*args, **kwargs)
|
||||
|
||||
+18
-86
@@ -7,7 +7,7 @@ This module provides helpers to decode and encode SSE streams, including:
|
||||
"""
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Iterable, Iterator, List, Optional
|
||||
|
||||
from .recording import record_sse
|
||||
@@ -31,17 +31,6 @@ class SSEEvent:
|
||||
retry: Optional[int] = None
|
||||
# Monotonic sequence number (1-based) within a stream, set by the decoder
|
||||
index: int = 0
|
||||
# Lazy JSON cache (computed on first access of .json)
|
||||
_json_cached: bool = field(default=False, init=False, repr=False)
|
||||
_json_value: Optional[Any] = field(default=None, init=False, repr=False)
|
||||
|
||||
@property
|
||||
def is_done(self) -> bool:
|
||||
"""Return True if this event marks the end of the stream.
|
||||
|
||||
The end-of-stream sentinel is the literal string "[DONE]".
|
||||
"""
|
||||
return self.data.strip() == "[DONE]"
|
||||
|
||||
@property
|
||||
def json(self) -> Optional[Any]:
|
||||
@@ -49,19 +38,9 @@ class SSEEvent:
|
||||
|
||||
Returns None if the data is empty, invalid JSON, or the [DONE] sentinel.
|
||||
"""
|
||||
if not self._json_cached:
|
||||
val: Optional[Any]
|
||||
text = (self.data or "").strip()
|
||||
if self.is_done or not text:
|
||||
val = None
|
||||
else:
|
||||
try:
|
||||
val = json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
val = None
|
||||
self._json_value = val
|
||||
self._json_cached = True
|
||||
return self._json_value
|
||||
text = (self.data or "").strip()
|
||||
val: Optional[Any] = json.loads(text)
|
||||
return val
|
||||
|
||||
|
||||
class SSEDecoder:
|
||||
@@ -92,11 +71,8 @@ class SSEDecoder:
|
||||
.strip()
|
||||
.decode(self.encoding, errors="replace")
|
||||
)
|
||||
elif line.startswith(b"data:"):
|
||||
part = line[5:]
|
||||
if part.startswith(b" "):
|
||||
part = part[1:]
|
||||
data_parts.append(part)
|
||||
else:
|
||||
data_parts.append(line[5:].strip())
|
||||
|
||||
data_text = (
|
||||
b"\n".join(data_parts).decode(self.encoding, errors="replace")
|
||||
@@ -107,8 +83,6 @@ class SSEDecoder:
|
||||
|
||||
def feed(self, chunk: bytes) -> Iterator[SSEEvent]:
|
||||
"""Feed a new bytes chunk and yield any complete parsed events."""
|
||||
if not chunk:
|
||||
return
|
||||
self.buffer += chunk
|
||||
self.full_buffer += chunk
|
||||
while True:
|
||||
@@ -119,11 +93,10 @@ class SSEDecoder:
|
||||
self.buffer = self.buffer[idx + 1 :]
|
||||
stripped = line.rstrip(b"\r\n")
|
||||
if stripped == b"":
|
||||
if self._event_lines:
|
||||
ev = self._parse_event(self._event_lines)
|
||||
self._seq += 1
|
||||
ev.index = self._seq
|
||||
yield ev
|
||||
ev = self._parse_event(self._event_lines)
|
||||
self._seq += 1
|
||||
ev.index = self._seq
|
||||
yield ev
|
||||
self._event_lines = []
|
||||
else:
|
||||
self._event_lines.append(stripped)
|
||||
@@ -157,13 +130,10 @@ def encode_sse_data(data: str) -> bytes:
|
||||
as per the SSE spec. Optionally include event and id.
|
||||
"""
|
||||
out = bytearray()
|
||||
if data == "":
|
||||
out.extend(b"data:\n")
|
||||
else:
|
||||
for line in data.splitlines():
|
||||
out.extend(b"data: ")
|
||||
out.extend(line.encode("utf-8"))
|
||||
out.extend(b"\n")
|
||||
for line in data.splitlines():
|
||||
out.extend(b"data: ")
|
||||
out.extend(line.encode("utf-8"))
|
||||
out.extend(b"\n")
|
||||
out.extend(b"\n")
|
||||
return bytes(out)
|
||||
|
||||
@@ -174,9 +144,7 @@ def encode_sse_json(obj: Any) -> bytes:
|
||||
return encode_sse_data(payload)
|
||||
|
||||
|
||||
def chunks_to_sse(
|
||||
chunks: Iterable[Dict[str, Any]], *, add_done: bool = True
|
||||
) -> Iterator[bytes]:
|
||||
def chunks_to_sse(chunks: Iterable[Dict[str, Any]]) -> Iterator[bytes]:
|
||||
"""Encode an iterator of JSON-able dicts into SSE byte messages.
|
||||
|
||||
If add_done is True, a final [DONE] sentinel event is yielded.
|
||||
@@ -188,48 +156,12 @@ def chunks_to_sse(
|
||||
buffer += sse
|
||||
yield sse
|
||||
finally:
|
||||
if add_done:
|
||||
sse = done_event_bytes()
|
||||
buffer += sse
|
||||
yield sse
|
||||
sse = done_event_bytes()
|
||||
buffer += sse
|
||||
yield sse
|
||||
record_sse(buffer, "downstream_response")
|
||||
|
||||
|
||||
def done_event_bytes() -> bytes:
|
||||
"""Return the SSE-encoded [DONE] sentinel as bytes."""
|
||||
return encode_sse_data("[DONE]")
|
||||
|
||||
|
||||
# Keeping for future use if we enable OpenAI backend
|
||||
# def sse_to_chunks(
|
||||
# stream: Iterable[bytes], *, skip_done: bool = True, encoding: str = "utf-8"
|
||||
# ) -> Iterator[Dict[str, Any]]:
|
||||
# """Convert an SSE byte-stream to an iterator of JSON dicts.
|
||||
#
|
||||
# - Collects multi-line data fields per SSE spec
|
||||
# - Uses event.json to avoid repeated json.loads
|
||||
# - Skips the [DONE] sentinel by default
|
||||
# """
|
||||
# for ev in sse_to_events(stream, encoding=encoding):
|
||||
# if skip_done and ev.is_done:
|
||||
# continue
|
||||
# if ev.json is None:
|
||||
# continue
|
||||
# yield ev.json
|
||||
#
|
||||
#
|
||||
# def sse_to_json_events(
|
||||
# stream: Iterable[bytes], *, skip_done: bool = True, encoding: str = "utf-8"
|
||||
# ) -> Iterator[Tuple[Optional[str], Dict[str, Any]]]:
|
||||
# """Yield (event, json_obj) pairs for events whose data parses as JSON.
|
||||
#
|
||||
# Non-JSON events are skipped. The [DONE] sentinel is skipped if skip_done
|
||||
# is True.
|
||||
# """
|
||||
# for ev in sse_to_events(stream, encoding=encoding):
|
||||
# if skip_done and ev.is_done:
|
||||
# continue
|
||||
# obj = ev.json
|
||||
# if obj is None:
|
||||
# continue
|
||||
# yield (ev.event, obj)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"""Exceptions for the application."""
|
||||
|
||||
|
||||
class ConfigurationError(ValueError):
|
||||
"""Exception raised for configuration errors."""
|
||||
|
||||
preamble = None
|
||||
|
||||
def get_response_content(self):
|
||||
"""Returns a formated error message inclduing the preamble and the message."""
|
||||
message = self.args[0].replace("\n", "\n\t")
|
||||
|
||||
return f"{self.preamble}\n\n\t{message}"
|
||||
|
||||
|
||||
class ServiceConfigurationError(ConfigurationError):
|
||||
"""Exception raised for configuration errors in the service configuration."""
|
||||
|
||||
preamble = "Service configuration error, check your .env file."
|
||||
|
||||
|
||||
class CursorConfigurationError(ConfigurationError):
|
||||
"""Exception raised for configuration errors in Cursor configuration."""
|
||||
|
||||
preamble = "Cursor configuration error, check your Cursor settings."
|
||||
@@ -6,7 +6,6 @@ Flask==3.1.2
|
||||
|
||||
# Logging
|
||||
rich==14.1.0
|
||||
loguru==0.7.3
|
||||
|
||||
# Requests
|
||||
requests==2.32.5
|
||||
|
||||
@@ -0,0 +1,396 @@
|
||||
{
|
||||
"model": "gpt-minimal",
|
||||
"temperature": 0,
|
||||
"user": "REDACTED",
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "REDACTED"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "REDACTED"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "REDACTED"
|
||||
}
|
||||
],
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "REDACTED",
|
||||
"description": "REDACTED",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"explanation": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"target_directories": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"search_only_prs": {
|
||||
"type": "boolean",
|
||||
"description": "REDACTED"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"explanation",
|
||||
"query",
|
||||
"target_directories"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "REDACTED",
|
||||
"description": "REDACTED",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"is_background": {
|
||||
"type": "boolean",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"explanation": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"command",
|
||||
"is_background"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "REDACTED",
|
||||
"description": "REDACTED",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"glob": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"output_mode": {
|
||||
"type": "string",
|
||||
"description": "REDACTED",
|
||||
"enum": [
|
||||
"content",
|
||||
"files_with_matches",
|
||||
"count"
|
||||
]
|
||||
},
|
||||
"-B": {
|
||||
"type": "number",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"-A": {
|
||||
"type": "number",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"-C": {
|
||||
"type": "number",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"-i": {
|
||||
"type": "boolean",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"head_limit": {
|
||||
"type": "number",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"multiline": {
|
||||
"type": "boolean",
|
||||
"description": "REDACTED"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"pattern"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "REDACTED",
|
||||
"description": "REDACTED",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target_file": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"explanation": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"target_file"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "REDACTED",
|
||||
"description": "REDACTED",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"paths": {
|
||||
"description": "REDACTED",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "REDACTED",
|
||||
"description": "REDACTED",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target_notebook": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"cell_idx": {
|
||||
"type": "number",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"is_new_cell": {
|
||||
"type": "boolean",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"cell_language": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"old_string": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"new_string": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"target_notebook",
|
||||
"cell_idx",
|
||||
"is_new_cell",
|
||||
"cell_language",
|
||||
"old_string",
|
||||
"new_string"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "REDACTED",
|
||||
"description": "REDACTED",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"merge": {
|
||||
"type": "boolean",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"todos": {
|
||||
"type": "array",
|
||||
"description": "REDACTED",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"pending",
|
||||
"in_progress",
|
||||
"completed",
|
||||
"cancelled"
|
||||
],
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"content",
|
||||
"status",
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"minItems": 2
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"merge",
|
||||
"todos"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "REDACTED",
|
||||
"description": "REDACTED",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target_file": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"instructions": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"code_edit": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"target_file",
|
||||
"instructions",
|
||||
"code_edit"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "REDACTED",
|
||||
"description": "REDACTED",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target_file": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"offset": {
|
||||
"type": "integer",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "REDACTED"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"target_file"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "REDACTED",
|
||||
"description": "REDACTED",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target_directory": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"ignore_globs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "REDACTED"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"target_directory"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "REDACTED",
|
||||
"description": "REDACTED",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target_directory": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"glob_pattern": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"glob_pattern"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"tool_choice": "auto",
|
||||
"stream": true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
data: {"id":"chatcmpl-ERO7qkefORSgjuOmbThzZoG8","object":"chat.completion.chunk","created":1758093803,"model":"gpt-minimal","choices":[{"index":0,"delta":{"role":"assistant","content":"<think>\n\n"},"finish_reason":null}]}
|
||||
|
||||
data: {"id":"chatcmpl-ERO7qkefORSgjuOmbThzZoG8","object":"chat.completion.chunk","created":1758093804,"model":"gpt-minimal","choices":[{"index":0,"delta":{"role":"assistant","content":"</think>\n\n"},"finish_reason":null}]}
|
||||
|
||||
data: {"id":"chatcmpl-ERO7qkefORSgjuOmbThzZoG8","object":"chat.completion.chunk","created":1758093804,"model":"gpt-minimal","choices":[{"index":0,"delta":{"role":"assistant","content":"pong"},"finish_reason":null}]}
|
||||
|
||||
data: {"id":"chatcmpl-ERO7qkefORSgjuOmbThzZoG8","object":"chat.completion.chunk","created":1758093806,"model":"gpt-minimal","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
|
||||
|
||||
data: [DONE]
|
||||
|
||||
@@ -0,0 +1,400 @@
|
||||
{
|
||||
"instructions": "REDACTED",
|
||||
"input": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "REDACTED"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "REDACTED"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"model": "gpt-5",
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"name": "REDACTED",
|
||||
"description": "REDACTED",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"explanation": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"target_directories": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"search_only_prs": {
|
||||
"type": "boolean",
|
||||
"description": "REDACTED"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"explanation",
|
||||
"query",
|
||||
"target_directories"
|
||||
]
|
||||
},
|
||||
"strict": false
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "REDACTED",
|
||||
"description": "REDACTED",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"is_background": {
|
||||
"type": "boolean",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"explanation": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"command",
|
||||
"is_background"
|
||||
]
|
||||
},
|
||||
"strict": false
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "REDACTED",
|
||||
"description": "REDACTED",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"glob": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"output_mode": {
|
||||
"type": "string",
|
||||
"description": "REDACTED",
|
||||
"enum": [
|
||||
"content",
|
||||
"files_with_matches",
|
||||
"count"
|
||||
]
|
||||
},
|
||||
"-B": {
|
||||
"type": "number",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"-A": {
|
||||
"type": "number",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"-C": {
|
||||
"type": "number",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"-i": {
|
||||
"type": "boolean",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"head_limit": {
|
||||
"type": "number",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"multiline": {
|
||||
"type": "boolean",
|
||||
"description": "REDACTED"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"pattern"
|
||||
]
|
||||
},
|
||||
"strict": false
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "REDACTED",
|
||||
"description": "REDACTED",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target_file": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"explanation": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"target_file"
|
||||
]
|
||||
},
|
||||
"strict": false
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "REDACTED",
|
||||
"description": "REDACTED",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"paths": {
|
||||
"description": "REDACTED",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"strict": false
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "REDACTED",
|
||||
"description": "REDACTED",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target_notebook": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"cell_idx": {
|
||||
"type": "number",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"is_new_cell": {
|
||||
"type": "boolean",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"cell_language": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"old_string": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"new_string": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"target_notebook",
|
||||
"cell_idx",
|
||||
"is_new_cell",
|
||||
"cell_language",
|
||||
"old_string",
|
||||
"new_string"
|
||||
]
|
||||
},
|
||||
"strict": false
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "REDACTED",
|
||||
"description": "REDACTED",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"merge": {
|
||||
"type": "boolean",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"todos": {
|
||||
"type": "array",
|
||||
"description": "REDACTED",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"pending",
|
||||
"in_progress",
|
||||
"completed",
|
||||
"cancelled"
|
||||
],
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"content",
|
||||
"status",
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"minItems": 2
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"merge",
|
||||
"todos"
|
||||
]
|
||||
},
|
||||
"strict": false
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "REDACTED",
|
||||
"description": "REDACTED",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target_file": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"instructions": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"code_edit": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"target_file",
|
||||
"instructions",
|
||||
"code_edit"
|
||||
]
|
||||
},
|
||||
"strict": false
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "REDACTED",
|
||||
"description": "REDACTED",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target_file": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"offset": {
|
||||
"type": "integer",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "REDACTED"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"target_file"
|
||||
]
|
||||
},
|
||||
"strict": false
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "REDACTED",
|
||||
"description": "REDACTED",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target_directory": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"ignore_globs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "REDACTED"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"target_directory"
|
||||
]
|
||||
},
|
||||
"strict": false
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "REDACTED",
|
||||
"description": "REDACTED",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target_directory": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
},
|
||||
"glob_pattern": {
|
||||
"type": "string",
|
||||
"description": "REDACTED"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"glob_pattern"
|
||||
]
|
||||
},
|
||||
"strict": false
|
||||
}
|
||||
],
|
||||
"tool_choice": "auto",
|
||||
"prompt_cache_key": "REDACTED",
|
||||
"stream": true,
|
||||
"reasoning": {
|
||||
"effort": "minimal",
|
||||
"summary": "detailed"
|
||||
},
|
||||
"store": false,
|
||||
"stream_options": {
|
||||
"include_obfuscation": false
|
||||
},
|
||||
"truncation": "auto"
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -101,8 +101,13 @@ class ReplyBase:
|
||||
|
||||
def assert_upstream_request(self, mock: Any) -> None:
|
||||
"""Assert upstream request matches the recorded upstream request."""
|
||||
expected_upstream_request_json = json.loads(self.expected_upstream_request_body)
|
||||
assert mock.last_request.json() == expected_upstream_request_json
|
||||
if self.expected_upstream_request_body:
|
||||
expected_upstream_request_json = json.loads(
|
||||
self.expected_upstream_request_body
|
||||
)
|
||||
assert mock.last_request.json() == expected_upstream_request_json
|
||||
else:
|
||||
assert mock.last_request is None
|
||||
|
||||
def assert_downstream_response(self, response) -> None:
|
||||
"""Assert downstream response matches the recorded downstream response."""
|
||||
|
||||
@@ -21,7 +21,7 @@ Check "azure_response" for the error details:
|
||||
\t "foo": "bar"
|
||||
\t },
|
||||
\t "request_body": {
|
||||
\t "instructions": "REDACTE...",
|
||||
\t "instructions": "REDACTED...",
|
||||
\t "input": "...redacted 2 input items...",
|
||||
\t "model": "gpt-5",
|
||||
\t "tools": "...redacted 11 tools...",
|
||||
@@ -59,7 +59,7 @@ Check "azure_response" for the error details:
|
||||
\t "error": "Bad API Key or whatever"
|
||||
\t },
|
||||
\t "request_body": {
|
||||
\t "instructions": "REDACTE...",
|
||||
\t "instructions": "REDACTED...",
|
||||
\t "input": "...redacted 2 input items...",
|
||||
\t "model": "gpt-5",
|
||||
\t "tools": "...redacted 11 tools...",
|
||||
@@ -95,7 +95,7 @@ Check "azure_response" for the error details:
|
||||
\t "azure_status_code": 500,
|
||||
\t "azure_response": "Internal Server Error",
|
||||
\t "request_body": {
|
||||
\t "instructions": "REDACTE...",
|
||||
\t "instructions": "REDACTED...",
|
||||
\t "input": "...redacted 2 input items...",
|
||||
\t "model": "gpt-5",
|
||||
\t "tools": "...redacted 11 tools...",
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
"""Functional tests using WebTest.
|
||||
|
||||
See: http://webtest.readthedocs.org/
|
||||
"""
|
||||
|
||||
from .replay_base import ReplyBase
|
||||
|
||||
|
||||
class TestBadSummaryLevel(ReplyBase):
|
||||
"""Test a single ping-pong interaction, no tool calls."""
|
||||
|
||||
expected_upstream_request_body = None
|
||||
expected_downstream_status_code = 400
|
||||
expected_downstream_response_body = b"""Service configuration error, check your .env file.
|
||||
|
||||
\tAZURE_SUMMARY_LEVEL must be either auto, detailed, or concise.
|
||||
\t
|
||||
\tGot: foo"""
|
||||
|
||||
def modify_settings(self, app) -> None:
|
||||
"""Set invalid summary level in settings."""
|
||||
app.config["AZURE_SUMMARY_LEVEL"] = "foo"
|
||||
|
||||
|
||||
class TestBadModelName(ReplyBase):
|
||||
"""Test a single ping-pong interaction, no tool calls."""
|
||||
|
||||
expected_upstream_request_body = None
|
||||
expected_downstream_status_code = 400
|
||||
expected_downstream_response_body = b"""Cursor configuration error, check your Cursor settings.
|
||||
|
||||
\tModel name must be either gpt-high, gpt-medium, gpt-low, or gpt-minimal.
|
||||
\t
|
||||
\tGot: foo-minimal"""
|
||||
|
||||
@property
|
||||
def downstream_request_body(self) -> str:
|
||||
"""Set invalid model name in request body."""
|
||||
return super().downstream_request_body.replace("gpt-", "foo-")
|
||||
@@ -0,0 +1,12 @@
|
||||
"""Functional tests using WebTest.
|
||||
|
||||
See: http://webtest.readthedocs.org/
|
||||
"""
|
||||
|
||||
from .replay_base import ReplyBase
|
||||
|
||||
|
||||
class TestSSEWithoutClosingNewLines(ReplyBase):
|
||||
"""Test the replay of an SSE response without closing new lines."""
|
||||
|
||||
recording = "sse_without_closing_new_lines"
|
||||
@@ -15,11 +15,12 @@ class TestModelInvalidJson(ReplyBase):
|
||||
|
||||
def test(self, testapp, requests_mock, mocker):
|
||||
"""Test logging of context containing an invalid JSON tool call."""
|
||||
console_print = mocker.patch(
|
||||
"rich.console.Console.print", return_value=mocker.Mock()
|
||||
)
|
||||
mocker.patch("rich.console.Console.print")
|
||||
console_print = mocker.patch("rich.padding.Padding.__init__", return_value=None)
|
||||
super().test(testapp, requests_mock)
|
||||
console_print.assert_any_call("[red]Invalid JSON generated by the model:[/red]")
|
||||
console_print.assert_any_call(
|
||||
"[red]Invalid JSON generated by the model:[/red]", (0, 4)
|
||||
)
|
||||
|
||||
|
||||
def test_redact_headers():
|
||||
|
||||
@@ -8,11 +8,11 @@ class TestModels:
|
||||
"""Models."""
|
||||
|
||||
def test_models_endpoint_returns_400(self, testapp):
|
||||
"""Ensure /models endpoint returns HTTP 400."""
|
||||
"""Ensure /models endpoint returns HTTP 400 wihtout auth."""
|
||||
testapp.get("/models", status=400)
|
||||
|
||||
def test_models_endpoint_returns_200(self, testapp):
|
||||
"""Ensure /models endpoint returns HTTP 400."""
|
||||
"""Ensure /models endpoint returns HTTP 400 with auth."""
|
||||
response = testapp.get(
|
||||
"/models",
|
||||
status=200,
|
||||
@@ -25,5 +25,5 @@ class TestModels:
|
||||
assert '"gpt-minimal"' in content
|
||||
|
||||
def test_health_endpoint_returns_200(self, testapp):
|
||||
"""Ensure /health endpoint returns HTTP 200."""
|
||||
"""Ensure /health endpoint returns HTTP 200 without auth."""
|
||||
testapp.get("/health", status=200)
|
||||
|
||||
@@ -71,6 +71,9 @@ class TestRecording(ReplyBase):
|
||||
# Unrelated folder
|
||||
os.makedirs(os.path.join(tmp_path, "foo"))
|
||||
|
||||
# Smaller index than last recording index
|
||||
os.makedirs(os.path.join(tmp_path, "-10"))
|
||||
|
||||
super().test(testapp, requests_mock)
|
||||
|
||||
assert os.path.exists(os.path.join(tmp_path, "124"))
|
||||
|
||||
Reference in New Issue
Block a user