mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-25 21:36:52 +08:00
docs: update OpenAPI specification to include all current endpoints (#10412)
Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
parent
4a3f5dd9b4
commit
5ce5469bfa
4744
docs/openapi.json
4744
docs/openapi.json
File diff suppressed because it is too large
Load Diff
226
scripts/update_openapi.py
Executable file
226
scripts/update_openapi.py
Executable file
@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Update OpenHands OpenAPI documentation.
|
||||
|
||||
Generates the OpenAPI specification from the FastAPI application and writes it
|
||||
to docs/openapi.json.
|
||||
|
||||
Usage:
|
||||
python scripts/update_openapi.py
|
||||
|
||||
Behavior:
|
||||
- Uses openhands.server.app.app.openapi() to build the spec.
|
||||
- Preserves existing "servers" from docs/openapi.json if present; otherwise
|
||||
writes sensible defaults.
|
||||
- Sets info.version to openhands.__version__.
|
||||
- Sanitizes endpoint descriptions to remove code blocks and internal-only sections.
|
||||
- Excludes operational/UI-only convenience endpoints:
|
||||
- /server_info
|
||||
- /api/conversations/{conversation_id}/vscode-url
|
||||
- /api/conversations/{conversation_id}/web-hosts
|
||||
- Creates a backup docs/openapi.json.backup before overwriting.
|
||||
|
||||
Output:
|
||||
- Prints OpenAPI and API versions, endpoint count, servers count, and sample endpoints.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
|
||||
# Suppress warnings and logs during import
|
||||
logging.getLogger().setLevel(logging.CRITICAL)
|
||||
warnings.filterwarnings('ignore')
|
||||
os.environ['OPENHANDS_LOG_LEVEL'] = 'CRITICAL'
|
||||
|
||||
# Add the project root to the Python path
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
try:
|
||||
from openhands import __version__
|
||||
from openhands.server.app import app
|
||||
except ImportError as e:
|
||||
print(f'Error importing OpenHands modules: {e}')
|
||||
print(
|
||||
"Make sure you're running this script from the project root and dependencies are installed."
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _sanitize_description(text: str) -> str:
|
||||
"""Remove internal, code-centric, or redundant sections from endpoint descriptions.
|
||||
|
||||
- Strip fenced code blocks
|
||||
- Remove Args/Returns/Raises/Example/Examples/Notes sections
|
||||
- Remove inline curl examples
|
||||
- Avoid provider-implementation specifics like LiteLLM/Bedrock
|
||||
"""
|
||||
import re
|
||||
|
||||
if not text:
|
||||
return text
|
||||
|
||||
# Remove fenced code blocks
|
||||
text = re.sub(r'```[\s\S]*?```', '', text, flags=re.MULTILINE)
|
||||
|
||||
# Remove common docstring sections (until next blank line or end)
|
||||
for header in [
|
||||
r'Args?:',
|
||||
r'Returns?:',
|
||||
r'Raises?:',
|
||||
r'Example[s]?:',
|
||||
r'Notes?:',
|
||||
]:
|
||||
text = re.sub(rf'(?ms)^\s*{header}.*?(?:\n\s*\n|\Z)', '', text)
|
||||
|
||||
# Remove lines that contain curl examples
|
||||
text = re.sub(r'(?im)^.*\bcurl\b.*$', '', text)
|
||||
|
||||
# Generalize provider-implementation specifics
|
||||
text = re.sub(r'\bLiteLLM\b', 'configured model providers', text)
|
||||
text = re.sub(r'\blitellm\b', 'configured providers', text)
|
||||
text = re.sub(r'\bBedrock\b', '', text)
|
||||
|
||||
# Collapse excessive blank lines and trim
|
||||
text = re.sub(r'\n{3,}', '\n\n', text).strip()
|
||||
return text
|
||||
|
||||
|
||||
def _sanitize_spec(spec: dict) -> dict:
|
||||
"""Sanitize descriptions and summaries to be public-API friendly."""
|
||||
path_summary_overrides = {
|
||||
'/api/options/models': 'List Supported Models',
|
||||
'/api/options/agents': 'List Agents',
|
||||
'/api/options/security-analyzers': 'List Security Analyzers',
|
||||
'/api/conversations/{conversation_id}/list-files': 'List Workspace Files',
|
||||
'/api/conversations/{conversation_id}/select-file': 'Get File Content',
|
||||
'/api/conversations/{conversation_id}/zip-directory': 'Download Workspace Archive',
|
||||
}
|
||||
path_description_overrides = {
|
||||
'/api/options/models': 'List model identifiers available on this server based on configured providers.',
|
||||
'/api/options/agents': 'List available agent types supported by this server.',
|
||||
'/api/options/security-analyzers': 'List supported security analyzers.',
|
||||
'/api/conversations/{conversation_id}/list-files': 'List workspace files visible to the conversation runtime. Applies .gitignore and internal ignore rules.',
|
||||
'/api/conversations/{conversation_id}/select-file': 'Return the content of the given file from the conversation workspace.',
|
||||
'/api/conversations/{conversation_id}/zip-directory': 'Return a ZIP archive of the current conversation workspace.',
|
||||
}
|
||||
|
||||
for path, methods in list(spec.get('paths', {}).items()):
|
||||
for method, meta in list(methods.items()):
|
||||
if not isinstance(meta, dict):
|
||||
continue
|
||||
# Override overly specific summaries where helpful
|
||||
if path in path_summary_overrides:
|
||||
meta['summary'] = path_summary_overrides[path]
|
||||
# Override description if provided; otherwise sanitize
|
||||
if path in path_description_overrides:
|
||||
meta['description'] = path_description_overrides[path]
|
||||
elif 'description' in meta and isinstance(meta['description'], str):
|
||||
meta['description'] = _sanitize_description(meta['description'])
|
||||
|
||||
return spec
|
||||
|
||||
|
||||
def generate_openapi_spec():
|
||||
"""Generate the OpenAPI specification from the FastAPI app."""
|
||||
spec = app.openapi()
|
||||
|
||||
# Explicitly exclude certain endpoints that are operational, experimental, or UI-only convenience
|
||||
excluded_endpoints = [
|
||||
'/api/conversations/{conversation_id}/exp-config', # Internal experimentation endpoint
|
||||
'/server_info', # Operational/system diagnostics
|
||||
'/api/conversations/{conversation_id}/vscode-url', # UI/runtime convenience
|
||||
'/api/conversations/{conversation_id}/web-hosts', # UI/runtime convenience
|
||||
]
|
||||
|
||||
if 'paths' in spec:
|
||||
for endpoint in excluded_endpoints:
|
||||
if endpoint in spec['paths']:
|
||||
del spec['paths'][endpoint]
|
||||
print(f'Excluded endpoint: {endpoint}')
|
||||
|
||||
# Sanitize descriptions and summaries
|
||||
spec = _sanitize_spec(spec)
|
||||
|
||||
return spec
|
||||
|
||||
|
||||
def load_current_spec(spec_path):
|
||||
"""Load the current OpenAPI specification if it exists."""
|
||||
if spec_path.exists():
|
||||
with open(spec_path, 'r') as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
|
||||
|
||||
def update_openapi_spec(spec_path, backup=True):
|
||||
"""Update the OpenAPI specification file."""
|
||||
# Generate new spec
|
||||
new_spec = generate_openapi_spec()
|
||||
|
||||
# Load current spec for server information
|
||||
current_spec = load_current_spec(spec_path)
|
||||
|
||||
# Preserve server information from current spec if it exists
|
||||
if 'servers' in current_spec:
|
||||
new_spec['servers'] = current_spec['servers']
|
||||
else:
|
||||
# Default servers if none exist
|
||||
new_spec['servers'] = [
|
||||
{'url': 'https://app.all-hands.dev', 'description': 'Production server'},
|
||||
{'url': 'http://localhost:3000', 'description': 'Local server'},
|
||||
]
|
||||
|
||||
# Update version to match the package version
|
||||
new_spec['info']['version'] = __version__
|
||||
|
||||
# Backup current file if requested
|
||||
if backup and spec_path.exists():
|
||||
backup_path = spec_path.with_suffix('.json.backup')
|
||||
spec_path.rename(backup_path)
|
||||
print(f'Backed up current spec to {backup_path}')
|
||||
|
||||
# Write new spec
|
||||
with open(spec_path, 'w') as f:
|
||||
json.dump(new_spec, f, indent=2)
|
||||
|
||||
return new_spec
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function."""
|
||||
spec_path = project_root / 'docs' / 'openapi.json'
|
||||
|
||||
print('Updating OpenAPI specification...')
|
||||
print(f'Target file: {spec_path}')
|
||||
|
||||
try:
|
||||
new_spec = update_openapi_spec(spec_path)
|
||||
|
||||
print('✅ Successfully updated OpenAPI specification!')
|
||||
print(f' OpenAPI version: {new_spec.get("openapi", "N/A")}')
|
||||
print(f' API version: {new_spec.get("info", {}).get("version", "N/A")}')
|
||||
print(f' Total endpoints: {len(new_spec.get("paths", {}))}')
|
||||
print(f' Servers: {len(new_spec.get("servers", []))}')
|
||||
|
||||
# List some key endpoints
|
||||
paths = list(new_spec.get('paths', {}).keys())
|
||||
if paths:
|
||||
print(' Sample endpoints:')
|
||||
for path in sorted(paths)[:5]:
|
||||
methods = list(new_spec['paths'][path].keys())
|
||||
print(f' {path}: {methods}')
|
||||
if len(paths) > 5:
|
||||
print(f' ... and {len(paths) - 5} more')
|
||||
|
||||
except Exception as e:
|
||||
print(f'❌ Error updating OpenAPI specification: {e}')
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Loading…
x
Reference in New Issue
Block a user