CoolFace
Modelpublic

hymenjj/llama-cpp-python-prebuilt

sourceHugging Faceupdated 7mo agoView on Hugging Face
0likes
app.py598 linesDownload Raw Back to server
1from __future__ import annotations2 3import os4import json5import typing6import contextlib7 8from anyio import Lock9from functools import partial10from typing import List, Optional, Union, Dict11 12import llama_cpp13 14import anyio15from anyio.streams.memory import MemoryObjectSendStream16from starlette.concurrency import run_in_threadpool, iterate_in_threadpool17from fastapi import Depends, FastAPI, APIRouter, Request, HTTPException, status, Body18from fastapi.middleware import Middleware19from fastapi.middleware.cors import CORSMiddleware20from fastapi.security import HTTPBearer21from sse_starlette.sse import EventSourceResponse22from starlette_context.plugins import RequestIdPlugin  # type: ignore23from starlette_context.middleware import RawContextMiddleware24 25from llama_cpp.server.model import (26    LlamaProxy,27)28from llama_cpp.server.settings import (29    ConfigFileSettings,30    Settings,31    ModelSettings,32    ServerSettings,33)34from llama_cpp.server.types import (35    CreateCompletionRequest,36    CreateEmbeddingRequest,37    CreateChatCompletionRequest,38    ModelList,39    TokenizeInputRequest,40    TokenizeInputResponse,41    TokenizeInputCountResponse,42    DetokenizeInputRequest,43    DetokenizeInputResponse,44)45from llama_cpp.server.errors import RouteErrorHandler46 47 48router = APIRouter(route_class=RouteErrorHandler)49 50_server_settings: Optional[ServerSettings] = None51 52 53def set_server_settings(server_settings: ServerSettings):54    global _server_settings55    _server_settings = server_settings56 57 58def get_server_settings():59    yield _server_settings60 61 62_llama_proxy: Optional[LlamaProxy] = None63 64llama_outer_lock = Lock()65llama_inner_lock = Lock()66 67 68def set_llama_proxy(model_settings: List[ModelSettings]):69    global _llama_proxy70    _llama_proxy = LlamaProxy(models=model_settings)71 72 73async def get_llama_proxy():74    # NOTE: This double lock allows the currently streaming llama model to75    # check if any other requests are pending in the same thread and cancel76    # the stream if so.77    await llama_outer_lock.acquire()78    release_outer_lock = True79    try:80        await llama_inner_lock.acquire()81        try:82            llama_outer_lock.release()83            release_outer_lock = False84            yield _llama_proxy85        finally:86            llama_inner_lock.release()87    finally:88        if release_outer_lock:89            llama_outer_lock.release()90 91 92_ping_message_factory: typing.Optional[typing.Callable[[], bytes]] = None93 94 95def set_ping_message_factory(factory: typing.Callable[[], bytes]):96    global _ping_message_factory97    _ping_message_factory = factory98 99 100def create_app(101    settings: Settings | None = None,102    server_settings: ServerSettings | None = None,103    model_settings: List[ModelSettings] | None = None,104):105    config_file = os.environ.get("CONFIG_FILE", None)106    if config_file is not None:107        if not os.path.exists(config_file):108            raise ValueError(f"Config file {config_file} not found!")109        with open(config_file, "rb") as f:110            # Check if yaml file111            if config_file.endswith(".yaml") or config_file.endswith(".yml"):112                import yaml113 114                config_file_settings = ConfigFileSettings.model_validate_json(115                    json.dumps(yaml.safe_load(f))116                )117            else:118                config_file_settings = ConfigFileSettings.model_validate_json(f.read())119            server_settings = ServerSettings.model_validate(config_file_settings)120            model_settings = config_file_settings.models121 122    if server_settings is None and model_settings is None:123        if settings is None:124            settings = Settings()125        server_settings = ServerSettings.model_validate(settings)126        model_settings = [ModelSettings.model_validate(settings)]127 128    assert (129        server_settings is not None and model_settings is not None130    ), "server_settings and model_settings must be provided together"131 132    set_server_settings(server_settings)133    middleware = [Middleware(RawContextMiddleware, plugins=(RequestIdPlugin(),))]134    app = FastAPI(135        middleware=middleware,136        title="๐Ÿฆ™ llama.cpp Python API",137        version=llama_cpp.__version__,138        root_path=server_settings.root_path,139    )140    app.add_middleware(141        CORSMiddleware,142        allow_origins=["*"],143        allow_credentials=True,144        allow_methods=["*"],145        allow_headers=["*"],146    )147    app.include_router(router)148 149    assert model_settings is not None150    set_llama_proxy(model_settings=model_settings)151 152    if server_settings.disable_ping_events:153        set_ping_message_factory(lambda: bytes())154 155    return app156 157 158def prepare_request_resources(159    body: CreateCompletionRequest | CreateChatCompletionRequest,160    llama_proxy: LlamaProxy,161    body_model: str | None,162    kwargs,163) -> llama_cpp.Llama:164    if llama_proxy is None:165        raise HTTPException(166            status_code=status.HTTP_503_SERVICE_UNAVAILABLE,167            detail="Service is not available",168        )169    llama = llama_proxy(body_model)170    if body.logit_bias is not None:171        kwargs["logit_bias"] = (172            _logit_bias_tokens_to_input_ids(llama, body.logit_bias)173            if body.logit_bias_type == "tokens"174            else body.logit_bias175        )176 177    if body.grammar is not None:178        kwargs["grammar"] = llama_cpp.LlamaGrammar.from_string(body.grammar)179 180    if body.min_tokens > 0:181        _min_tokens_logits_processor = llama_cpp.LogitsProcessorList(182            [llama_cpp.MinTokensLogitsProcessor(body.min_tokens, llama.token_eos())]183        )184        if "logits_processor" not in kwargs:185            kwargs["logits_processor"] = _min_tokens_logits_processor186        else:187            kwargs["logits_processor"].extend(_min_tokens_logits_processor)188    return llama189 190 191async def get_event_publisher(192    request: Request,193    inner_send_chan: MemoryObjectSendStream[typing.Any],194    body: CreateCompletionRequest | CreateChatCompletionRequest,195    body_model: str | None,196    llama_call,197    kwargs,198):199    server_settings = next(get_server_settings())200    interrupt_requests = (201        server_settings.interrupt_requests if server_settings else False202    )203    async with contextlib.asynccontextmanager(get_llama_proxy)() as llama_proxy:204        llama = prepare_request_resources(body, llama_proxy, body_model, kwargs)205        async with inner_send_chan:206            try:207                iterator = await run_in_threadpool(llama_call, llama, **kwargs)208                async for chunk in iterate_in_threadpool(iterator):209                    await inner_send_chan.send(dict(data=json.dumps(chunk)))210                    if await request.is_disconnected():211                        raise anyio.get_cancelled_exc_class()()212                    if interrupt_requests and llama_outer_lock.locked():213                        await inner_send_chan.send(dict(data="[DONE]"))214                        raise anyio.get_cancelled_exc_class()()215                await inner_send_chan.send(dict(data="[DONE]"))216            except anyio.get_cancelled_exc_class() as e:217                print("disconnected")218                with anyio.move_on_after(1, shield=True):219                    print(220                        f"Disconnected from client (via refresh/close) {request.client}"221                    )222                    raise e223 224 225def _logit_bias_tokens_to_input_ids(226    llama: llama_cpp.Llama,227    logit_bias: Dict[str, float],228) -> Dict[str, float]:229    to_bias: Dict[str, float] = {}230    for token, score in logit_bias.items():231        token = token.encode("utf-8")232        for input_id in llama.tokenize(token, add_bos=False, special=True):233            to_bias[str(input_id)] = score234    return to_bias235 236 237# Setup Bearer authentication scheme238bearer_scheme = HTTPBearer(auto_error=False)239 240 241async def authenticate(242    settings: Settings = Depends(get_server_settings),243    authorization: Optional[str] = Depends(bearer_scheme),244):245    # Skip API key check if it's not set in settings246    if settings.api_key is None:247        return True248 249    # check bearer credentials against the api_key250    if authorization and authorization.credentials == settings.api_key:251        # api key is valid252        return authorization.credentials253 254    # raise http error 401255    raise HTTPException(256        status_code=status.HTTP_401_UNAUTHORIZED,257        detail="Invalid API key",258    )259 260 261openai_v1_tag = "OpenAI V1"262 263 264@router.post(265    "/v1/completions",266    summary="Completion",267    dependencies=[Depends(authenticate)],268    response_model=Union[269        llama_cpp.CreateCompletionResponse,270        str,271    ],272    responses={273        "200": {274            "description": "Successful Response",275            "content": {276                "application/json": {277                    "schema": {278                        "anyOf": [279                            {"$ref": "#/components/schemas/CreateCompletionResponse"}280                        ],281                        "title": "Completion response, when stream=False",282                    }283                },284                "text/event-stream": {285                    "schema": {286                        "type": "string",287                        "title": "Server Side Streaming response, when stream=True. "288                        + "See SSE format: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format",  # noqa: E501289                        "example": """data: {... see CreateCompletionResponse ...} \\n\\n data: ... \\n\\n ... data: [DONE]""",290                    }291                },292            },293        }294    },295    tags=[openai_v1_tag],296)297@router.post(298    "/v1/engines/copilot-codex/completions",299    include_in_schema=False,300    dependencies=[Depends(authenticate)],301    tags=[openai_v1_tag],302)303async def create_completion(304    request: Request,305    body: CreateCompletionRequest,306) -> llama_cpp.Completion:307    if isinstance(body.prompt, list):308        assert len(body.prompt) <= 1309        body.prompt = body.prompt[0] if len(body.prompt) > 0 else ""310 311    body_model = (312        body.model313        if request.url.path != "/v1/engines/copilot-codex/completions"314        else "copilot-codex"315    )316 317    exclude = {318        "n",319        "best_of",320        "logit_bias_type",321        "user",322        "min_tokens",323    }324    kwargs = body.model_dump(exclude=exclude)325 326    # handle streaming request327    if kwargs.get("stream", False):328        send_chan, recv_chan = anyio.create_memory_object_stream(10)329        return EventSourceResponse(330            recv_chan,331            data_sender_callable=partial(  # type: ignore332                get_event_publisher,333                request=request,334                inner_send_chan=send_chan,335                body=body,336                body_model=body_model,337                llama_call=llama_cpp.Llama.__call__,338                kwargs=kwargs,339            ),340            sep="\n",341            ping_message_factory=_ping_message_factory,342        )343 344    # handle regular request345    async with contextlib.asynccontextmanager(get_llama_proxy)() as llama_proxy:346        llama = prepare_request_resources(body, llama_proxy, body_model, kwargs)347 348        if await request.is_disconnected():349            print(350                f"Disconnected from client (via refresh/close) before llm invoked {request.client}"351            )352            raise HTTPException(353                status_code=status.HTTP_400_BAD_REQUEST,354                detail="Client closed request",355            )356 357        return await run_in_threadpool(llama, **kwargs)358 359 360@router.post(361    "/v1/embeddings",362    summary="Embedding",363    dependencies=[Depends(authenticate)],364    tags=[openai_v1_tag],365)366async def create_embedding(367    request: CreateEmbeddingRequest,368    llama_proxy: LlamaProxy = Depends(get_llama_proxy),369):370    return await run_in_threadpool(371        llama_proxy(request.model).create_embedding,372        **request.model_dump(exclude={"user"}),373    )374 375 376@router.post(377    "/v1/chat/completions",378    summary="Chat",379    dependencies=[Depends(authenticate)],380    response_model=Union[llama_cpp.ChatCompletion, str],381    responses={382        "200": {383            "description": "Successful Response",384            "content": {385                "application/json": {386                    "schema": {387                        "anyOf": [388                            {389                                "$ref": "#/components/schemas/CreateChatCompletionResponse"390                            }391                        ],392                        "title": "Completion response, when stream=False",393                    }394                },395                "text/event-stream": {396                    "schema": {397                        "type": "string",398                        "title": "Server Side Streaming response, when stream=True"399                        + "See SSE format: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format",  # noqa: E501400                        "example": """data: {... see CreateChatCompletionResponse ...} \\n\\n data: ... \\n\\n ... data: [DONE]""",401                    }402                },403            },404        }405    },406    tags=[openai_v1_tag],407)408async def create_chat_completion(409    request: Request,410    body: CreateChatCompletionRequest = Body(411        openapi_examples={412            "normal": {413                "summary": "Chat Completion",414                "value": {415                    "model": "gpt-3.5-turbo",416                    "messages": [417                        {"role": "system", "content": "You are a helpful assistant."},418                        {"role": "user", "content": "What is the capital of France?"},419                    ],420                },421            },422            "json_mode": {423                "summary": "JSON Mode",424                "value": {425                    "model": "gpt-3.5-turbo",426                    "messages": [427                        {"role": "system", "content": "You are a helpful assistant."},428                        {"role": "user", "content": "Who won the world series in 2020"},429                    ],430                    "response_format": {"type": "json_object"},431                },432            },433            "tool_calling": {434                "summary": "Tool Calling",435                "value": {436                    "model": "gpt-3.5-turbo",437                    "messages": [438                        {"role": "system", "content": "You are a helpful assistant."},439                        {"role": "user", "content": "Extract Jason is 30 years old."},440                    ],441                    "tools": [442                        {443                            "type": "function",444                            "function": {445                                "name": "User",446                                "description": "User record",447                                "parameters": {448                                    "type": "object",449                                    "properties": {450                                        "name": {"type": "string"},451                                        "age": {"type": "number"},452                                    },453                                    "required": ["name", "age"],454                                },455                            },456                        }457                    ],458                    "tool_choice": {459                        "type": "function",460                        "function": {461                            "name": "User",462                        },463                    },464                },465            },466            "logprobs": {467                "summary": "Logprobs",468                "value": {469                    "model": "gpt-3.5-turbo",470                    "messages": [471                        {"role": "system", "content": "You are a helpful assistant."},472                        {"role": "user", "content": "What is the capital of France?"},473                    ],474                    "logprobs": True,475                    "top_logprobs": 10,476                },477            },478        }479    ),480) -> llama_cpp.ChatCompletion:481    # This is a workaround for an issue in FastAPI dependencies482    # where the dependency is cleaned up before a StreamingResponse483    # is complete.484    # https://github.com/tiangolo/fastapi/issues/11143485 486    body_model = body.model487    exclude = {488        "n",489        "logit_bias_type",490        "user",491        "min_tokens",492    }493    kwargs = body.model_dump(exclude=exclude)494 495    # handle streaming request496    if kwargs.get("stream", False):497        send_chan, recv_chan = anyio.create_memory_object_stream(10)498        return EventSourceResponse(499            recv_chan,500            data_sender_callable=partial(  # type: ignore501                get_event_publisher,502                request=request,503                inner_send_chan=send_chan,504                body=body,505                body_model=body_model,506                llama_call=llama_cpp.Llama.create_chat_completion,507                kwargs=kwargs,508            ),509            sep="\n",510            ping_message_factory=_ping_message_factory,511        )512 513    # handle regular request514    async with contextlib.asynccontextmanager(get_llama_proxy)() as llama_proxy:515        llama = prepare_request_resources(body, llama_proxy, body_model, kwargs)516 517        if await request.is_disconnected():518            print(519                f"Disconnected from client (via refresh/close) before llm invoked {request.client}"520            )521            raise HTTPException(522                status_code=status.HTTP_400_BAD_REQUEST,523                detail="Client closed request",524            )525 526        return await run_in_threadpool(llama.create_chat_completion, **kwargs)527 528 529@router.get(530    "/v1/models",531    summary="Models",532    dependencies=[Depends(authenticate)],533    tags=[openai_v1_tag],534)535async def get_models(536    llama_proxy: LlamaProxy = Depends(get_llama_proxy),537) -> ModelList:538    return {539        "object": "list",540        "data": [541            {542                "id": model_alias,543                "object": "model",544                "owned_by": "me",545                "permissions": [],546            }547            for model_alias in llama_proxy548        ],549    }550 551 552extras_tag = "Extras"553 554 555@router.post(556    "/extras/tokenize",557    summary="Tokenize",558    dependencies=[Depends(authenticate)],559    tags=[extras_tag],560)561async def tokenize(562    body: TokenizeInputRequest,563    llama_proxy: LlamaProxy = Depends(get_llama_proxy),564) -> TokenizeInputResponse:565    tokens = llama_proxy(body.model).tokenize(body.input.encode("utf-8"), special=True)566 567    return TokenizeInputResponse(tokens=tokens)568 569 570@router.post(571    "/extras/tokenize/count",572    summary="Tokenize Count",573    dependencies=[Depends(authenticate)],574    tags=[extras_tag],575)576async def count_query_tokens(577    body: TokenizeInputRequest,578    llama_proxy: LlamaProxy = Depends(get_llama_proxy),579) -> TokenizeInputCountResponse:580    tokens = llama_proxy(body.model).tokenize(body.input.encode("utf-8"), special=True)581 582    return TokenizeInputCountResponse(count=len(tokens))583 584 585@router.post(586    "/extras/detokenize",587    summary="Detokenize",588    dependencies=[Depends(authenticate)],589    tags=[extras_tag],590)591async def detokenize(592    body: DetokenizeInputRequest,593    llama_proxy: LlamaProxy = Depends(get_llama_proxy),594) -> DetokenizeInputResponse:595    text = llama_proxy(body.model).detokenize(body.tokens).decode("utf-8")596 597    return DetokenizeInputResponse(text=text)598