GPT-5.2 and matrix of features

This commit is contained in:
Gabriel Gavilan
2025-12-28 14:04:58 +01:00
parent 78f7c411ba
commit 1a866580b0
3 changed files with 740 additions and 36 deletions
+23 -35
View File
@@ -15,6 +15,9 @@ A service that allows Cursor to use Azure GPT-5 deployments by:
This project originates from Cursor's lack of support for Azure models that are only served through the **Responses API**. It will hopefully become obsolete as Cursor continues to improve its model support. This project originates from Cursor's lack of support for Azure models that are only served through the **Responses API**. It will hopefully become obsolete as Cursor continues to improve its model support.
> [!WARNING]
> You still need an active paid Cursor subscription to be able to use this project.
> [!IMPORTANT] > [!IMPORTANT]
> **Azure** now supports the **Completions API** for the models `gpt-5`, `gpt-5-mini`, and `gpt-5-nano`. > **Azure** now supports the **Completions API** for the models `gpt-5`, `gpt-5-mini`, and `gpt-5-nano`.
> >
@@ -22,26 +25,32 @@ This project originates from Cursor's lack of support for Azure models that are
> >
> The models `gpt-5-pro` and `gpt-5-codex` remain available only through the **Responses API**, but work great with this project (see list of specific model limitations in the next section). > The models `gpt-5-pro` and `gpt-5-codex` remain available only through the **Responses API**, but work great with this project (see list of specific model limitations in the next section).
## Supported Models ## Supported Models
The entire gpt-5 series is supported, although some models have some limitations on the reasoning effort / verbosity / truncation values they accept: The entire gpt-5 series is supported, although some models have some limitations on the reasoning effort / verbosity / truncation / summary values they accept:
| Model Name | Reasoning Effort | Verbosity | Truncation | | Variable | Value | 5.2 | 5.2-chat | 5.1 | 5.1-codex | 5.1-codex-mini | 5.1-codex-max | 5 | 5-nano | 5-mini | 5-pro | 5-codex |
| ------------------ | ----------------------------------------------- | ----------------------------------- | ------------------------- | | ---------- | ---------- | --- | -------- | --- | --------- | -------------- | ------------- | --- | ------ | ------ | ----- | ------- |
| gpt-5 | ✅ `minimal` `low` `medium` `high` | ✅ `low` `medium` `high` | ✅ `auto` `disabled` | | Reasoning | `minimal` | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
| gpt-5.1 | ✅ `minimal` `low` `medium` `high` | ✅ `low` `medium` `high` | ✅ `auto` `disabled` | | | `low` | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| gpt-5-mini | ✅ `minimal` `low` `medium` `high` | ✅ `low` `medium` `high` | ✅ `auto` `disabled` | | | `medium` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| gpt-5-nano | ✅ `minimal` `low` `medium` `high` | ✅ `low` `medium` `high` | ✅ `auto` `disabled` | | | `high` | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| gpt-5-pro | ⚠️ _~~`minimal`~~ ~~`low`~~ ~~`medium`~~_ `high` | ✅ `low` `medium` `high` | ✅ `auto` `disabled` | | Verbosity | `low` | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
| gpt-5-codex | ⚠️ _~~`minimal`~~_ `low` `medium` `high` | ⚠️ _~~`low`~~_ `medium` _~~`high`~~_ | ✅ `auto` `disabled` | | | `medium` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| gpt-5.1-codex | ⚠️ _~~`minimal`~~_ `low` `medium` `high` | ⚠️ _~~`low`~~_ `medium` _~~`high`~~_ | ⚠️ _~~`auto`~~_ `disabled` | | | `high` | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
| gpt-5.1-codex-mini | ⚠️ _~~`minimal`~~_ `low` `medium` `high` | ⚠️ _~~`low`~~_ `medium` _~~`high`~~_ | ⚠️ _~~`auto`~~_ `disabled` | | Truncation | `auto` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| | `disabled` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| Summary | `auto` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| | `detailed` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| | `concise` | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
_(This matrix is automatically generated, and updated after every new model release.)_
## Feature highlights ## Feature highlights
- Switching between `high`/`medium`/`low` reasoning effort levels by selecting different models in Cursor. - Switching between `high`/`medium`/`low`/`minimal` reasoning effort levels by selecting different models in Cursor.
- Configuring different _reasoning summary_ levels. - Configuring different _reasoning summary_ levels (`auto`, `detailed`, `concise`).
- Displaying _reasoning summaries_ in Cursor natively, like any other reasoning model. - Displaying _reasoning summaries_ in Cursor natively, like any other reasoning model.
- Production-ready, so you can share the service among different users in an organization. - Production-ready, so you can share the service among different users in an organization.
- When running from a terminal, [rich](https://github.com/Textualize/rich) logging of the model's context on every request, including Markdown rendering, syntax highlighting, tool calls/outputs, and more. - When running from a terminal, [rich](https://github.com/Textualize/rich) logging of the model's context on every request, including Markdown rendering, syntax highlighting, tool calls/outputs, and more.
@@ -118,28 +127,7 @@ In addition to updating the OpenAI Base URL, you need to:
2. Ensure the toggles for both options are **on**, as shown in the previous image. 2. Ensure the toggles for both options are **on**, as shown in the previous image.
3. Add the custom models called exactly `gpt-high`, `gpt-medium`, and `gpt-low`, as shown in the previous image. You can also create `gpt-minimal` for minimal reasoning effort. You don't need to remove other models. 3. Add the custom models called exactly `gpt-high`, `gpt-medium`, and `gpt-low`, as shown in the previous image. You can also create `gpt-minimal` for minimal reasoning effort for models that support it. You don't need to remove other models.
<details>
<summary>Additional steps if you face this error:
<img src="assets/cursor_invalid_model.jpg" alt="The model does not work with your current plan or api key" width="100%">
</summary>
> This is a bug on Cursor's side when custom models edit files in **∞ Agent** mode. Regardless of the model, and even if `edit_file` is working correctly, Cursor may show this pop-up and interrupt generation after the first `edit_file` function call.
>
> This only happens when using model names Cursor has not allowlisted or prepared for, such as `gpt-high`. However, we can't use the standard model names such as `gpt-5-high` because Cursor does not route those to custom OpenAI Base URLs.
>
> For now, this bug can be bypassed by using the Custom Modes beta
>
> In the near future, either the bug in Agent mode will be fixed or those two remaining functions will be added to Custom Modes—or, even better, Azure support will improve enough to render this project obsolete.
4. Enable Custom Modes Beta in _Cursor Settings > Chat_: ![Azure not working in Cursor](assets/cursor_chat_config.jpg)
5. Create a custom mode:
<img src="assets/cursor_custom_mode.gif" alt="Fix for cursor BYOK from azure" width="200">
</details>
### 4. Running the service ### 4. Running the service
+1 -1
View File
@@ -3,7 +3,7 @@ profile = "black"
[tool.coverage.run] [tool.coverage.run]
source = ["app"] source = ["app"]
omit = ["tests/*", "autoapp.py"] omit = ["tests/*", "autoapp.py", "scripts/*"]
[tool.flake8] [tool.flake8]
ignore = ["D401", "D202", "E226", "E302", "E41", "W503", "E203"] ignore = ["D401", "D202", "E226", "E302", "E41", "W503", "E203"]
+716
View File
@@ -0,0 +1,716 @@
#!/usr/bin/env python3
"""
Model Capabilities Testing Script.
Tests model configurations independently to discover which values are supported
for each parameter. Variables are tested independently (not permuted) since they
are independent of each other.
For each model, tests:
- All reasoning effort levels (minimal, low, medium, high)
- All verbosity levels (low, medium, high)
- All truncation modes (auto, disabled)
- All summary levels (auto, detailed, concise)
Usage:
python scripts/test_model_capabilities.py
Required environment variables (from parent environment or .env):
AZURE_BASE_URL: Your Azure OpenAI endpoint
AZURE_API_KEY: Your Azure API key
SERVICE_API_KEY: API key for authenticating to this service
Optional environment variables:
MODELS_TO_TEST: Comma-separated list of model deployment names to test
(default: all gpt-5 models)
"""
import json
import os
import signal
import socket
import subprocess
import sys
import time
from dataclasses import dataclass, field
import requests
from dotenv import load_dotenv
from rich.console import Console
from rich.table import Table
# Load .env file if it exists
load_dotenv()
console = Console()
# =============================================================================
# Configuration
# =============================================================================
# Models to test (can be overridden via MODELS_TO_TEST env var)
DEFAULT_MODELS = [
"gpt-5.2",
"gpt-5.2-chat",
"gpt-5.1",
"gpt-5.1-codex",
"gpt-5.1-codex-mini",
"gpt-5.1-codex-max",
"gpt-5",
# "gpt-5-chat-global",
"gpt-5-nano",
"gpt-5-mini-global",
"gpt-5-pro",
"gpt-5-codex",
]
# Base configuration (expected to work for all models)
BASE_CONFIG = {
"AZURE_VERBOSITY_LEVEL": "medium",
"AZURE_TRUNCATION": "disabled",
"AZURE_SUMMARY_LEVEL": "auto",
}
BASE_REASONING_EFFORT = "medium"
# Values to test for each parameter
REASONING_EFFORTS = ["minimal", "low", "medium", "high"]
VERBOSITY_LEVELS = ["low", "medium", "high"]
TRUNCATION_MODES = ["auto", "disabled"]
SUMMARY_LEVELS = ["auto", "detailed", "concise"]
# Service configuration
SERVICE_HOST = "127.0.0.1"
STARTUP_TIMEOUT = 15 # seconds to wait for service to start
REQUEST_TIMEOUT = 60 # seconds to wait for completions request
# =============================================================================
# Data Classes
# =============================================================================
@dataclass
class CapabilityTestResult:
"""Result of a single capability test case."""
model: str
category: str # 'reasoning', 'verbosity', 'truncation', 'summary'
tested_value: str
success: bool
error_message: str = ""
response_snippet: str = ""
@dataclass
class ModelCapabilities:
"""Discovered capabilities for a model."""
model: str
supported_reasoning_efforts: set = field(default_factory=set)
supported_verbosities: set = field(default_factory=set)
supported_truncations: set = field(default_factory=set)
supported_summary_levels: set = field(default_factory=set)
results: list = field(default_factory=list)
# =============================================================================
# Helpers
# =============================================================================
def find_free_port() -> int:
"""Find a free port on localhost."""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("", 0))
return s.getsockname()[1]
def wait_for_service(host: str, port: int, timeout: float = 10) -> bool:
"""Wait for the service to become available."""
start = time.time()
while time.time() - start < timeout:
try:
resp = requests.get(f"http://{host}:{port}/health", timeout=2)
if resp.status_code == 200:
return True
except requests.RequestException:
pass
time.sleep(0.5)
return False
def start_service(env_overrides: dict[str, str], port: int) -> subprocess.Popen:
"""Start the Flask service with specified environment variables."""
env = os.environ.copy()
env.update(env_overrides)
env["FLASK_ENV"] = "production"
env["FLASK_DEBUG"] = "0"
env["LOG_CONTEXT"] = "off"
env["LOG_COMPLETION"] = "off"
cmd = [
sys.executable,
"-m",
"flask",
"run",
"--host",
SERVICE_HOST,
"--port",
str(port),
]
proc = subprocess.Popen(
cmd,
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
)
return proc
def stop_service(proc: subprocess.Popen) -> None:
"""Gracefully stop the service."""
if proc.poll() is None:
proc.send_signal(signal.SIGTERM)
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()
proc.wait()
def send_test_request(
host: str, port: int, model_name: str, api_key: str
) -> tuple[bool, str, str]:
"""
Send a simple chat completion request.
Returns:
(success, error_message, response_snippet)
"""
url = f"http://{host}:{port}/v1/chat/completions"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
payload = {
"model": model_name,
"messages": [{"role": "user", "content": "Reply with exactly: pong"}],
"stream": True,
}
try:
with requests.post(
url, headers=headers, json=payload, stream=True, timeout=REQUEST_TIMEOUT
) as resp:
if resp.status_code != 200:
try:
error_data = resp.json()
error_msg = error_data.get("error", {}).get(
"message", resp.text[:2000]
)
except (ValueError, KeyError):
error_msg = resp.text[:2000]
return False, f"HTTP {resp.status_code}: {error_msg}", ""
content_parts = []
for line in resp.iter_lines():
if not line:
continue
line_str = line.decode("utf-8")
if line_str.startswith("data: "):
data_str = line_str[6:]
if data_str == "[DONE]":
break
try:
data = json.loads(data_str)
if "error" in data:
return (
False,
data["error"].get("message", str(data["error"])),
"",
)
choices = data.get("choices", [])
if choices:
delta = choices[0].get("delta", {})
if content := delta.get("content"):
content_parts.append(content)
except json.JSONDecodeError:
pass
full_content = "".join(content_parts)
if full_content:
return True, "", full_content[:100]
return True, "", "(empty response)"
except requests.Timeout:
return False, "Request timed out", ""
except requests.RequestException as e:
return False, str(e), ""
def parse_azure_error(error_msg: str) -> str:
"""Extract the relevant part of Azure error messages."""
if "BadRequestError" in error_msg:
return "BadRequestError - Invalid parameter"
if "InvalidParameterValue" in error_msg:
return "InvalidParameterValue"
if "model_not_found" in error_msg.lower():
return "Model not found"
return error_msg[:1000] if len(error_msg) > 100 else error_msg
# =============================================================================
# Main Testing Logic
# =============================================================================
def run_single_test(
host: str,
port: int,
api_key: str,
model: str,
env_config: dict[str, str],
reasoning_effort: str,
category: str,
tested_value: str,
caps: ModelCapabilities,
) -> CapabilityTestResult:
"""Run a single test and update capabilities."""
model_name = f"gpt-{reasoning_effort}"
success, error, snippet = send_test_request(host, port, model_name, api_key)
result = CapabilityTestResult(
model=model.replace("-global", ""),
category=category,
tested_value=tested_value,
success=success,
error_message=parse_azure_error(error) if error else "",
response_snippet=snippet,
)
caps.results.append(result)
if success:
if category == "reasoning":
caps.supported_reasoning_efforts.add(tested_value)
elif category == "verbosity":
caps.supported_verbosities.add(tested_value)
elif category == "truncation":
caps.supported_truncations.add(tested_value)
elif category == "summary":
caps.supported_summary_levels.add(tested_value)
return result
def run_tests(models: list[str]) -> dict[str, ModelCapabilities]:
"""Run all tests and return results by model."""
service_api_key = os.environ.get("SERVICE_API_KEY", "change-me")
# Validate required env vars
required_vars = ["AZURE_BASE_URL", "AZURE_API_KEY"]
missing = [v for v in required_vars if not os.environ.get(v)]
if missing:
console.print(
f"[red]Error: Missing required environment variables: {missing}[/red]"
)
sys.exit(1)
results: dict[str, ModelCapabilities] = {}
port = find_free_port()
# Calculate total tests:
# Per model: 4 reasoning + 3 verbosity + 2 truncation + 3 summary = 12 tests
# But we can batch: reasoning tests share same env, others need separate envs
# Env configs needed per model:
# 1 (base) for all reasoning efforts
# + 2 for verbosity (low, high - medium is base)
# + 1 for truncation (auto - disabled is base)
# + 2 for summary (detailed, concise - auto is base)
# = 6 service starts per model
tests_per_model = (
len(REASONING_EFFORTS)
+ len(VERBOSITY_LEVELS)
+ len(TRUNCATION_MODES)
+ len(SUMMARY_LEVELS)
)
total_tests = len(models) * tests_per_model
current_test = 0
console.print("\n[bold cyan]Model Capabilities Test Suite[/bold cyan]")
console.print(f"Models to test: {len(models)}")
console.print(f"Tests per model: {tests_per_model}")
console.print(f"Total tests: {total_tests}")
console.print(f"Base config: {BASE_CONFIG}\n")
for model in models:
caps = ModelCapabilities(model=model)
results[model] = caps
console.print(f"\n[bold yellow]{'═' * 50}[/bold yellow]")
console.print(f"[bold yellow]Testing model: {model}[/bold yellow]")
console.print(f"[bold yellow]{'═' * 50}[/bold yellow]")
# =====================================================================
# Test 1: All reasoning efforts with base env config
# =====================================================================
console.print("\n [cyan]Testing reasoning efforts...[/cyan]")
env_config = {**BASE_CONFIG, "AZURE_DEPLOYMENT": model}
proc = start_service(env_config, port)
try:
if not wait_for_service(SERVICE_HOST, port, STARTUP_TIMEOUT):
console.print(" [red]✗ Service failed to start[/red]")
for effort in REASONING_EFFORTS:
current_test += 1
result = CapabilityTestResult(
model=model,
category="reasoning",
tested_value=effort,
success=False,
error_message="Service failed to start",
)
caps.results.append(result)
else:
for effort in REASONING_EFFORTS:
current_test += 1
result = run_single_test(
SERVICE_HOST,
port,
service_api_key,
model,
env_config,
effort,
"reasoning",
effort,
caps,
)
status = "[green]✓[/green]" if result.success else "[red]✗[/red]"
error_info = (
f" {result.error_message}" if not result.success else ""
)
console.print(
f" [{current_test}/{total_tests}] "
f"{effort}: {status}{error_info}"
)
finally:
stop_service(proc)
time.sleep(0.3)
# =====================================================================
# Test 2: Verbosity levels (each needs separate service start)
# =====================================================================
console.print("\n [cyan]Testing verbosity levels...[/cyan]")
for verbosity in VERBOSITY_LEVELS:
current_test += 1
env_config = {
**BASE_CONFIG,
"AZURE_DEPLOYMENT": model,
"AZURE_VERBOSITY_LEVEL": verbosity,
}
proc = start_service(env_config, port)
try:
if not wait_for_service(SERVICE_HOST, port, STARTUP_TIMEOUT):
result = CapabilityTestResult(
model=model,
category="verbosity",
tested_value=verbosity,
success=False,
error_message="Service failed to start",
)
caps.results.append(result)
console.print(
f" [{current_test}/{total_tests}] "
f"{verbosity}: [red]✗[/red] Service failed"
)
else:
result = run_single_test(
SERVICE_HOST,
port,
service_api_key,
model,
env_config,
BASE_REASONING_EFFORT,
"verbosity",
verbosity,
caps,
)
status = "[green]✓[/green]" if result.success else "[red]✗[/red]"
error_info = (
f" {result.error_message}" if not result.success else ""
)
console.print(
f" [{current_test}/{total_tests}] "
f"{verbosity}: {status}{error_info}"
)
finally:
stop_service(proc)
time.sleep(0.3)
# =====================================================================
# Test 3: Truncation modes
# =====================================================================
console.print("\n [cyan]Testing truncation modes...[/cyan]")
for truncation in TRUNCATION_MODES:
current_test += 1
env_config = {
**BASE_CONFIG,
"AZURE_DEPLOYMENT": model,
"AZURE_TRUNCATION": truncation,
}
proc = start_service(env_config, port)
try:
if not wait_for_service(SERVICE_HOST, port, STARTUP_TIMEOUT):
result = CapabilityTestResult(
model=model,
category="truncation",
tested_value=truncation,
success=False,
error_message="Service failed to start",
)
caps.results.append(result)
console.print(
f" [{current_test}/{total_tests}] "
f"{truncation}: [red]✗[/red] Service failed"
)
else:
result = run_single_test(
SERVICE_HOST,
port,
service_api_key,
model,
env_config,
BASE_REASONING_EFFORT,
"truncation",
truncation,
caps,
)
status = "[green]✓[/green]" if result.success else "[red]✗[/red]"
error_info = (
f" {result.error_message}" if not result.success else ""
)
console.print(
f" [{current_test}/{total_tests}] "
f"{truncation}: {status}{error_info}"
)
finally:
stop_service(proc)
time.sleep(0.3)
# =====================================================================
# Test 4: Summary levels
# =====================================================================
console.print("\n [cyan]Testing summary levels...[/cyan]")
for summary in SUMMARY_LEVELS:
current_test += 1
env_config = {
**BASE_CONFIG,
"AZURE_DEPLOYMENT": model,
"AZURE_SUMMARY_LEVEL": summary,
}
proc = start_service(env_config, port)
try:
if not wait_for_service(SERVICE_HOST, port, STARTUP_TIMEOUT):
result = CapabilityTestResult(
model=model,
category="summary",
tested_value=summary,
success=False,
error_message="Service failed to start",
)
caps.results.append(result)
console.print(
f" [{current_test}/{total_tests}] "
f"{summary}: [red]✗[/red] Service failed"
)
else:
result = run_single_test(
SERVICE_HOST,
port,
service_api_key,
model,
env_config,
BASE_REASONING_EFFORT,
"summary",
summary,
caps,
)
status = "[green]✓[/green]" if result.success else "[red]✗[/red]"
error_info = (
f" {result.error_message}" if not result.success else ""
)
console.print(
f" [{current_test}/{total_tests}] "
f"{summary}: {status}{error_info}"
)
finally:
stop_service(proc)
time.sleep(0.3)
return results
def print_summary(results: dict[str, ModelCapabilities]) -> None:
"""Print a summary table of results."""
console.print("\n")
console.print("[bold cyan]═" * 60)
console.print("[bold cyan]CAPABILITIES SUMMARY[/bold cyan]")
console.print("[bold cyan]═" * 60)
for model, caps in results.items():
console.print(f"\n[bold yellow]{model}[/bold yellow]")
# Reasoning efforts
re_status = []
for effort in REASONING_EFFORTS:
if effort in caps.supported_reasoning_efforts:
re_status.append(f"[green]✓ {effort}[/green]")
else:
re_status.append(f"[red]✗ {effort}[/red]")
console.print(f" Reasoning: {' '.join(re_status)}")
# Verbosity
verb_status = []
for verb in VERBOSITY_LEVELS:
if verb in caps.supported_verbosities:
verb_status.append(f"[green]✓ {verb}[/green]")
else:
verb_status.append(f"[red]✗ {verb}[/red]")
console.print(f" Verbosity: {' '.join(verb_status)}")
# Truncation
trunc_status = []
for trunc in TRUNCATION_MODES:
if trunc in caps.supported_truncations:
trunc_status.append(f"[green]✓ {trunc}[/green]")
else:
trunc_status.append(f"[red]✗ {trunc}[/red]")
console.print(f" Truncation: {' '.join(trunc_status)}")
# Summary Level
summary_status = []
for summary in SUMMARY_LEVELS:
if summary in caps.supported_summary_levels:
summary_status.append(f"[green]✓ {summary}[/green]")
else:
summary_status.append(f"[red]✗ {summary}[/red]")
console.print(f" Summary: {' '.join(summary_status)}")
def print_detailed_table(results: dict[str, ModelCapabilities]) -> None:
"""Print a detailed table of all test results."""
console.print("\n")
console.print("[bold cyan]═" * 60)
console.print("[bold cyan]DETAILED RESULTS[/bold cyan]")
console.print("[bold cyan]═" * 60)
table = Table(show_header=True, header_style="bold magenta")
table.add_column("Model", style="cyan")
table.add_column("Category")
table.add_column("Value")
table.add_column("Status")
table.add_column("Error/Response", max_width=40)
for model, caps in results.items():
for result in caps.results:
status = "[green]✓ OK[/green]" if result.success else "[red]✗ FAIL[/red]"
detail = result.response_snippet if result.success else result.error_message
table.add_row(
result.model,
result.category,
result.tested_value,
status,
detail,
)
console.print(table)
def generate_markdown_table(results: dict[str, ModelCapabilities]) -> str:
"""Generate a transposed markdown table with models as columns."""
# Get model names without gpt- prefix and -global suffix for column headers
model_names = []
for model in results.keys():
short_name = model.replace("gpt-", "").replace("-global", "")
model_names.append(short_name)
# Build header row
header = "| Variable | Value | " + " | ".join(model_names) + " |"
separator = "| --- | --- |" + " --- |" * len(model_names)
lines = [header, separator]
# Define all variable/value combinations to test
test_rows = [
("Reasoning Effort", "minimal", "supported_reasoning_efforts"),
("Reasoning Effort", "low", "supported_reasoning_efforts"),
("Reasoning Effort", "medium", "supported_reasoning_efforts"),
("Reasoning Effort", "high", "supported_reasoning_efforts"),
("Verbosity", "low", "supported_verbosities"),
("Verbosity", "medium", "supported_verbosities"),
("Verbosity", "high", "supported_verbosities"),
("Truncation", "auto", "supported_truncations"),
("Truncation", "disabled", "supported_truncations"),
("Summary", "auto", "supported_summary_levels"),
("Summary", "detailed", "supported_summary_levels"),
("Summary", "concise", "supported_summary_levels"),
]
for variable, value, attr_name in test_rows:
cells = []
for model, caps in results.items():
supported_set = getattr(caps, attr_name)
if value in supported_set:
cells.append("✅")
else:
cells.append("❌")
row = f"| {variable} | `{value}` | " + " | ".join(cells) + " |"
lines.append(row)
return "\n".join(lines)
# =============================================================================
# Entry Point
# =============================================================================
def main() -> None:
"""Main entry point."""
models_env = os.environ.get("MODELS_TO_TEST", "")
if models_env:
models = [m.strip() for m in models_env.split(",") if m.strip()]
else:
models = DEFAULT_MODELS
console.print("[bold]Starting Model Capabilities Testing[/bold]")
console.print(f"Testing models: {models}")
try:
results = run_tests(models)
print_summary(results)
print_detailed_table(results)
console.print("\n")
console.print("[bold cyan]═" * 60)
console.print("[bold cyan]MARKDOWN TABLE (for README)[/bold cyan]")
console.print("[bold cyan]═" * 60)
console.print("\n```markdown")
console.print(generate_markdown_table(results))
console.print("```\n")
except KeyboardInterrupt:
console.print("\n[yellow]Testing interrupted by user[/yellow]")
sys.exit(1)
if __name__ == "__main__":
main()