docker-compose/generate-config/app.py (236 lines of code) (raw):
import os
import re
import sys
from typing import Any, Dict, List, Literal, Optional
from urllib.parse import urlparse, urlunparse
import httpx
import typer
from config import CoreConfig
from dotenv import load_dotenv
from pydantic import BaseModel as PydanticBaseModel
class BaseModel(PydanticBaseModel):
class Config:
extra = "allow"
class Pricing(BaseModel):
unit: str
prompt: Optional[str] = None
completion: Optional[str] = None
class Limits(BaseModel):
max_total_tokens: Optional[int] = None
max_prompt_tokens: Optional[int] = None
max_completion_tokens: Optional[int] = None
class Capabilities(BaseModel):
scale_types: List[str]
completion: bool
chat_completion: bool
embeddings: bool
fine_tune: bool
inference: bool
class Features(BaseModel):
rate: bool
tokenize: bool
truncate_prompt: bool
configuration: bool
system_prompt: bool
tools: bool
seed: bool
url_attachments: bool
folder_attachments: bool
class Data(BaseModel):
id: str
model: Optional[str] = None
display_name: Optional[str] = None
icon_url: Optional[str] = None
owner: str
object: str
status: str
created_at: int
updated_at: int
features: Features
defaults: Optional[Dict[str, Any]] = None
lifecycle_status: Optional[str] = None
capabilities: Optional[Capabilities] = None
limits: Optional[Limits] = None
pricing: Optional[Pricing] = None
tokenizer_model: Optional[str] = None
display_version: Optional[str] = None
description: Optional[str] = None
input_attachment_types: Optional[List[str]] = None
max_input_attachments: Optional[int] = None
class RootObject(BaseModel):
data: List[Data]
object: str
def get_env(name: str) -> str:
value = os.getenv(name)
if value is None:
raise ValueError(f"Environment variable {name!r} is not set")
return value
def print_info(*args, **kwargs):
print(*args, **kwargs, file=sys.stderr)
def modify_url(
original_url: str, new_scheme: str, new_hostname: str, new_port: int
) -> str:
parsed_url = urlparse(original_url)
new_netloc = f"{new_hostname}:{new_port}" if new_port else new_hostname
modified_url = parsed_url._replace(scheme=new_scheme, netloc=new_netloc)
return urlunparse(modified_url)
load_dotenv()
REMOTE_DIAL_URL = get_env("REMOTE_DIAL_URL")
REMOTE_DIAL_API_KEY = get_env("REMOTE_DIAL_API_KEY")
app = typer.Typer()
@app.command()
def main(
local_app_port: Optional[int] = None,
deployment_id_regex: Optional[str] = None,
):
regex: re.Pattern | None = None
if deployment_id_regex is not None:
regex = re.compile(deployment_id_regex, re.IGNORECASE)
headers = {"api-key": REMOTE_DIAL_API_KEY}
models_url = f"{REMOTE_DIAL_URL}/openai/models"
applications_url = f"{REMOTE_DIAL_URL}/openai/applications"
config = CoreConfig(
{
"routes": {},
"keys": {
"dial_api_key": {
"project": "TEST-PROJECT",
"role": "default",
}
},
}
)
with httpx.Client() as client:
response = client.get(models_url, headers=headers)
models = RootObject.parse_obj(response.json())
process_data(
REMOTE_DIAL_URL,
REMOTE_DIAL_API_KEY,
models.data,
config,
"models",
regex,
)
response = client.get(applications_url, headers=headers)
applications = RootObject.parse_obj(response.json())
process_data(
REMOTE_DIAL_URL,
REMOTE_DIAL_API_KEY,
applications.data,
config,
"applications",
regex,
)
if local_app_port is not None:
config.add_application(
"local-application",
{
"displayName": "Locally hosted application",
"endpoint": f"http://host.docker.internal:{local_app_port}/openai/deployments/app/chat/completions",
"forwardAuthToken": True,
# Enable all kinds of attachments by default.
# The user will remove the ones that are not applicable.
"inputAttachmentTypes": ["*/*"],
"features": {
"urlAttachmentsSupported": True,
"folderAttachmentsSupported": True,
},
},
)
config.print()
def process_data(
upstream_base: str,
upstream_key: str,
data: List[Data],
config: CoreConfig,
key: Literal["models", "applications"],
regex: Optional[re.Pattern] = None,
):
data = [
item
for item in data
if item.capabilities is None
or item.capabilities.chat_completion
or item.capabilities.embeddings
]
n = len(data)
print_info(f"\n{key} [{n}]:")
for idx, item in enumerate(data, start=1):
is_chat_model = (
item.capabilities is None or item.capabilities.chat_completion
)
model_type = "chat" if is_chat_model else "embedding"
endpoint = "chat/completions" if is_chat_model else "embeddings"
if regex is not None and not regex.search(item.id):
continue
name = item.display_name or "NONAME"
if item.display_version:
name += f" ({item.display_version})"
print_info(f" {idx:>3}. {model_type:<10}. {item.id:<30}: {name}")
endpoint_base = f"http://adapter-dial:5000/openai/deployments/{item.id}"
icon_url = item.icon_url
if icon_url is not None:
icon_url = modify_url(icon_url, "http", "localhost", 3001)
model = {
"type": model_type,
"displayName": f"{item.display_name} (Adapter)",
"displayVersion": item.display_version,
"description": item.description,
"tokenizerModel": item.tokenizer_model,
"iconUrl": icon_url,
"endpoint": f"{endpoint_base}/{endpoint}",
"forwardAuthToken": True,
"upstreams": [
{
"endpoint": f"{upstream_base}/openai/deployments/{item.id}/{endpoint}",
"key": upstream_key,
}
],
"features": {
"rateEndpoint": (
f"{endpoint_base}/rate" if item.features.rate else None
),
"tokenizeEndpoint": (
f"{endpoint_base}/tokenize"
if item.features.tokenize
else None
),
"truncatePromptEndpoint": (
f"{endpoint_base}/truncate_prompt"
if item.features.truncate_prompt
else None
),
"configurationEndpoint": (
f"{endpoint_base}/configuration"
if item.features.configuration
else None
),
"systemPromptSupported": item.features.system_prompt,
"toolsSupported": item.features.tools,
"seedSupported": item.features.seed,
"urlAttachmentsSupported": item.features.url_attachments,
"folderAttachmentsSupported": item.features.folder_attachments,
},
"maxInputAttachments": item.max_input_attachments,
"inputAttachmentTypes": item.input_attachment_types,
"defaults": item.defaults,
"limits": {
"maxPromptTokens": (
item.limits.max_prompt_tokens if item.limits else None
),
"maxTotalTokens": (
item.limits.max_total_tokens if item.limits else None
),
"maxCompletionTokens": (
item.limits.max_completion_tokens if item.limits else None
),
},
"pricing": {
"unit": item.pricing.unit if item.pricing else None,
"prompt": item.pricing.prompt if item.pricing else None,
"completion": (
item.pricing.completion if item.pricing else None
),
},
}
# Note that
# * even applications are declared as models,
# because only models have "upstreams" property.
# * the local deployment id is different from the remote deployment id
# to highlight that the two are not required to be the same.
config.add_model(f"{item.id}-adapter", model)
if __name__ == "__main__":
typer.run(main)