Skip to content

Commit

Permalink
Merge pull request #7 from chat-apropo/query-params-config
Browse files Browse the repository at this point in the history
reestructure project
  • Loading branch information
matheusfillipe authored Feb 18, 2024
2 parents fe3c63c + 17db014 commit d920178
Show file tree
Hide file tree
Showing 9 changed files with 191 additions and 114 deletions.
21 changes: 7 additions & 14 deletions .github/workflows/workflow.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
path: g4f-api
- uses: a7ul/tar-action@v1.1.0
with:
command: c
cwd: "./"
files: "g4f-api/"
outPath: deploy.tar

- name: Deploy App to CapRover
uses: caprover/deploy-from-github@v1.0.1
with:
server: '${{ secrets.CAPROVER_SERVER }}'
app: '${{ secrets.APP_NAME }}'
token: '${{ secrets.APP_TOKEN }}'
run: |
npm install -g caprover
caprover deploy \
--caproverUrl '${{ secrets.CAPROVER_SERVER }}' \
--appToken '${{ secrets.APP_TOKEN }}' \
--appName '${{ secrets.APP_NAME }}' \
-b '${{ github.head_ref || github.ref_name }} '
7 changes: 4 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
FROM python:3.12.2-slim-bullseye

WORKDIR /app
COPY . /app
WORKDIR /backend
COPY backend/ backend
COPY requirements.txt .

RUN pip install --no-cache-dir -r requirements.txt

ENTRYPOINT ["python3", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
ENTRYPOINT ["python3", "-m", "backend.run"]
15 changes: 15 additions & 0 deletions backend/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from fastapi import FastAPI

from backend.errors import add_exception_handlers
from backend.routes import add_routers

app = FastAPI(
title="G4F API",
description="Get text completions from various models and providers using https://github.com/xtekky/gpt4free",
version="0.0.1",
)

add_exception_handlers(app)
add_routers(app)

__all__ = ["app"]
66 changes: 66 additions & 0 deletions backend/dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from typing import Literal, TypeVar

import g4f
from fastapi import Query
from fastapi.openapi.models import Example
from g4f import ModelUtils
from pydantic import BaseModel, ConfigDict, Field

all_models = list(ModelUtils.convert.keys())

all_working_providers = [provider.__name__ for provider in g4f.Provider.__providers__ if provider.working]

A = TypeVar("A")


def generate_examples_from_values(values: list) -> dict[str, Example]:
return {str(v or "--"): Example(value=v) for v in values}


def allowed_values_or_none(v: A | None, allowed: list[A]) -> A | None:
if v is None:
return v
if v not in allowed:
raise ValueError(f"Value {v} not in allowed values: {allowed}")
return v


class Message(BaseModel):
role: Literal["user", "assistant"] = Field(..., description="Who is sending the message")
content: str = Field(..., description="Content of the message")


class CompletionRequest(BaseModel):
messages: list[Message] = Field(..., description="List of messages to use for completion")
model_config = ConfigDict(extra="forbid")


class CompletionParams:
def __init__(
self,
model: str
| None = Query(
None,
description="LLM model to use for completion. Cannot be specified together with provider.",
openapi_examples=generate_examples_from_values([None] + all_models),
),
provider: str
| None = Query(
None,
description="Provider to use for completion. Cannot be specified together with model.",
openapi_examples=generate_examples_from_values([None] + all_working_providers),
),
):
allowed_values_or_none(model, all_models)
allowed_values_or_none(provider, all_working_providers)
if model and provider:
raise ValueError("model and provider cannot be provided together yet")
if not (model or provider):
raise ValueError("one of model or provider must be specified")

self.model = model
self.provider = provider


def chat_completion() -> type[g4f.ChatCompletion]:
return g4f.ChatCompletion
15 changes: 15 additions & 0 deletions backend/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import json

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from pydantic import ValidationError


def add_exception_handlers(app: FastAPI) -> None:
@app.exception_handler(ValueError)
def _(_: Request, exc: ValueError) -> JSONResponse:
return JSONResponse(status_code=422, content={"detail": str(exc)})

@app.exception_handler(ValidationError)
def _(_: Request, exc: ValidationError) -> JSONResponse:
return JSONResponse(status_code=422, content=json.loads(exc.json()))
48 changes: 48 additions & 0 deletions backend/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import g4f
from fastapi import APIRouter, Depends, FastAPI
from fastapi.responses import RedirectResponse

from backend.dependencies import CompletionParams, CompletionRequest, all_models, all_working_providers, chat_completion

router_root = APIRouter()
router_api = APIRouter(prefix="/api")


def add_routers(app: FastAPI) -> None:
app.include_router(router_root)
app.include_router(router_api)


@router_root.get("/")
def get_root():
return RedirectResponse(url="/docs")


@router_api.post("/completions")
def post_completion(
completion: CompletionRequest,
params: CompletionParams = Depends(),
chat: type[g4f.ChatCompletion] = Depends(chat_completion),
):
response = chat.create(
model=params.model,
provider=params.provider,
messages=[msg.model_dump() for msg in completion.messages],
stream=False,
)
return {"completion": response}


@router_api.get("/providers")
def get_list_providers():
return {"providers": all_working_providers}


@router_api.get("/models")
def get_list_models():
return {"models": all_models}


@router_api.get("/health")
def get_health_check():
return {"status": "ok"}
11 changes: 11 additions & 0 deletions backend/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import uvicorn

if __name__ == "__main__":
uvicorn.run(
"backend:app",
host="0.0.0.0",
port=8000,
reload=False,
workers=1,
timeout_keep_alive=30,
)
85 changes: 0 additions & 85 deletions main.py

This file was deleted.

37 changes: 25 additions & 12 deletions tests/test_api.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from unittest.mock import Mock

import pytest
from backend import app
from backend.dependencies import all_models, all_working_providers, chat_completion
from fastapi.testclient import TestClient

from main import all_models, all_working_providers, app, chat_completion
COMPLETION_PATH = "/api/completions"


def test_api_validation():
Expand All @@ -16,32 +18,41 @@ def test_api_validation():
assert response.status_code == 200

# Invalid model
response = client.post("/", json={"model": "gpt2", "messages": [{"role": "user", "content": "Hello"}]})
response = client.post(
COMPLETION_PATH, params={"model": "Kjf0ajL0gjlskb0K"}, json={"messages": [{"role": "user", "content": "Hello"}]}
)
assert response.status_code == 422

# Invalid provider
response = client.post("/", json={"provider": "gpt2", "messages": [{"role": "user", "content": "Hello"}]})
response = client.post(
COMPLETION_PATH,
params={"provider": "xkdjak3jal"},
json={"messages": [{"role": "user", "content": "Hello"}]},
)
assert response.status_code == 422

# Valid model and provider given together
response = client.post(
"/",
json={
COMPLETION_PATH,
params={
"model": all_models[0],
"provider": all_working_providers[0],
},
json={
"messages": [{"role": "user", "content": "Hello"}],
},
)
assert response.status_code == 422

# Both model and provider missing
response = client.post("/", json={"messages": [{"role": "user", "content": "Hello"}]})
response = client.post(COMPLETION_PATH, json={"messages": [{"role": "user", "content": "Hello"}]})
assert response.status_code == 422

# Valid request
response = client.post(
"/",
json={"model": all_models[0], "messages": [{"role": "user", "content": "Hello"}]},
COMPLETION_PATH,
params={"model": all_models[0]},
json={"messages": [{"role": "user", "content": "Hello"}]},
)
assert response.status_code == 200
assert response.json() == {"completion": "response"}
Expand All @@ -56,15 +67,17 @@ def test_all_provider_model_combination(model, provider):

with TestClient(app) as client:
response = client.post(
"/",
json={"model": model, "messages": [{"role": "user", "content": "Hello"}]},
COMPLETION_PATH,
params={"model": model},
json={"messages": [{"role": "user", "content": "Hello"}]},
)
assert response.status_code == 200
assert response.json() == {"completion": "response"}

response = client.post(
"/",
json={"provider": provider, "messages": [{"role": "user", "content": "Hello"}]},
COMPLETION_PATH,
params={"provider": provider},
json={"messages": [{"role": "user", "content": "Hello"}]},
)
assert response.status_code == 200
assert response.json() == {"completion": "response"}

0 comments on commit d920178

Please sign in to comment.