PT ▾
Obter chave de API

Gateway LLM em Python: Chamando um Endpoint de Backend

Um serviço de gateway LLM em Python pode ter cerca de 80 linhas: FastAPI na frente, httpx atrás, com retentativas, tempos limite e passagem de streaming para um backend compatível com OpenAI.

Atualizado

Raios de luz passando por um único portão em uma ponte de vidro, ilustrando uma camada de gateway na frente de um backend

Pontos principais

Verifique se o endpoint segue estritamente o esquema de chat completions da OpenAI para compatibilidade pronta para usar.

Garanta que o tamanho da janela de contexto corresponda aos requisitos de memória do seu aplicativo para evitar erros de truncamento.

Implemente suporte a streaming para lidar com a latência de gerações longas e melhorar a experiência do usuário.

Confirme se as políticas de privacidade estão alinhadas com suas necessidades de retenção de dados, especialmente em relação ao armazenamento de prompts.

O que um gateway fino em Python faz

Uma configuração útil de gateway LLM em Python é menor do que as pessoas esperam. Você não precisa de uma plataforma completa. Um gateway fino é um pequeno serviço web que fica entre seus aplicativos e um endpoint de backend e adiciona quatro coisas: um token de autenticação compartilhado para seus próprios clientes, uma política de tempo limite e retentativa, um limite de chamadas a servidores upstream simultâneas e passagem de streaming limpa. Seus aplicativos falam com ele como se fosse OpenAI, e ele encaminha para o backend real.

Este artigo constrói isso com FastAPI e httpx, usando https://api.openllmgateway.com/v1 como backend e o ID do modelo uncensored. Se você ainda está decidindo se precisa de um gateway, leia o que é um gateway LLM primeiro. O foco aqui é o código.

Configuração e a rota sem streaming

Instale as dependências e defina duas variáveis de ambiente: a chave do backend e uma lista separada por vírgulas de tokens que você distribui para seus próprios aplicativos. Seus aplicativos nunca veem a chave do backend.

pip install fastapi uvicorn httpx openai
export BACKEND_KEY="your-backend-key"
export GATEWAY_TOKENS="app-one-token,app-two-token"

A primeira metade do serviço cobre configuração, autenticação e a rota JSON simples com retries:

import asyncio
import os

import httpx
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse, Response, StreamingResponse

BACKEND = "https://api.openllmgateway.com/v1"
BACKEND_KEY = os.environ["BACKEND_KEY"]
TOKENS = set(os.environ["GATEWAY_TOKENS"].split(","))

app = FastAPI()
http = httpx.AsyncClient(
    timeout=httpx.Timeout(connect=5, read=120, write=30, pool=10)
)
slots = asyncio.Semaphore(8)  # matches the full-key concurrency limit
UP = {"Authorization": "Bearer " + BACKEND_KEY, "Content-Type": "application/json"}


def check_auth(req: Request):
    token = req.headers.get("authorization", "").removeprefix("Bearer ").strip()
    if token not in TOKENS:
        raise HTTPException(401, "invalid gateway token")


@app.post("/v1/chat/completions")
async def chat(req: Request):
    check_auth(req)
    body = await req.json()
    body["model"] = "uncensored"
    if body.get("stream"):
        return await relay_stream(body)
    async with slots:
        for attempt in range(4):
            try:
                r = await http.post(BACKEND + "/chat/completions", json=body, headers=UP)
            except (httpx.ConnectError, httpx.ReadTimeout):
                await asyncio.sleep(2 ** attempt)
                continue
            if r.status_code == 429:
                await asyncio.sleep(float(r.headers.get("Retry-After", "2")))
                continue
            if r.status_code >= 500:
                await asyncio.sleep(2 ** attempt)
                continue
            return JSONResponse(r.json(), status_code=r.status_code)
    raise HTTPException(502, "backend unavailable after retries")

Observe que o gateway força model para uncensored. Clientes não podem solicitar nada além disso, o que é uma maneira simples de manter uma política em um só lugar.

Streaming passthrough que não vaza slots

O streaming é onde gateways simples falham. Se você ler o corpo inteiro do upstream e só então enviá-lo, o usuário não vê nada até o fim. Se você esquecer de liberar o slot de concorrência quando um cliente se desconecta, você eventualmente se bloqueia. A API trata uma requisição em streaming como ocupando seu slot de concorrência até o fim do streaming, então seu semáforo local deve fazer o mesmo.

async def relay_stream(body):
    await slots.acquire()
    try:
        req = http.build_request("POST", BACKEND + "/chat/completions", json=body, headers=UP)
        upstream = await http.send(req, stream=True)
    except httpx.HTTPError:
        slots.release()
        raise HTTPException(502, "backend unreachable")

    if upstream.status_code >= 300:
        data = await upstream.aread()
        await upstream.aclose()
        slots.release()
        return Response(data, status_code=upstream.status_code, media_type="application/json")

    async def pump():
        try:
            async for chunk in upstream.aiter_raw():
                yield chunk
        finally:
            await upstream.aclose()
            slots.release()

    return StreamingResponse(pump(), media_type="text/event-stream")

O bloco finally é executado quando o streaming termina, quando o cliente se desconecta ou quando ocorre um erro, então o slot sempre é liberado. Retentativas estão deliberadamente ausentes aqui. Uma vez que bytes foram enviados ao cliente, você não pode reiniciar de forma limpa, então a retentativa só deve ocorrer antes do primeiro chunk, e nesta versão a verificação de status acima é a única barreira.

Chamando seu gateway de um cliente

Execute-o com uvicorn gateway:app --port 8000. Seus aplicativos agora usam o cliente OpenAI padrão com o gateway como base URL e um dos seus próprios tokens como chave:

from openai import OpenAI

# Talks to the local gateway, which forwards to https://api.openllmgateway.com/v1
client = OpenAI(base_url="http://localhost:8000/v1", api_key="app-one-token")

for chunk in client.chat.completions.create(
    model="uncensored",
    messages=[{"role": "user", "content": "Give me three names for a hiking app."}],
    stream=True,
):
    if chunk.choices and chunk.choices[0].delta.content:
        print(chunk.choices[0].delta.content, end="", flush=True)

Você pode adicionar as outras rotas da mesma forma. GET /v1/models e POST /v1/responses estão disponíveis no backend, e chamada de funções e modo JSON não precisam de tratamento especial porque o gateway encaminha o corpo intacto. O corpo máximo da requisição é 8 MB, então defina um limite correspondente na frente do gateway se ele estiver exposto à internet.

Tratando 429 corretamente

O backend responde 429 em três situações, e seu gateway deve diferenciá-las. Um código de erro rate_limited significa que você excedeu requisições por minuto: 300 em uma chave completa, 60 em uma chave de teste. Um código concurrency significa muitas requisições ao mesmo tempo: 8 em uma chave completa, 2 em uma chave de teste. Sobrecarga temporária também retorna 429 com Retry-After: 5.

O semáforo acima evita o caso de concorrência desde o início, desde que o gateway seja o único cliente usando essa chave. Para requisições por minuto, adicione um bucket de tokens simples ou um contador por minuto. O código acima respeita o cabeçalho Retry-After em cada 429, o que cobre sobrecarga. Se você ver erros de concurrency mesmo assim, outro processo está usando a mesma chave, ou um streaming antigo ainda está aberto.

Registre o cabeçalho X-Request-Id para cada chamada ao upstream para rastrear uma resposta ruim, e registre X-Balance-USD para acompanhar seu crédito. Um gateway é um bom lugar para alertar quando o saldo cai abaixo de um limite. A lista de verificação de produção de gateways AI cobre o resto: segredos, timeouts, verificações de saúde e monitoramento.

Rastreamento de custos dentro do gateway

Como toda resposta traz o uso de tokens, o gateway pode registrar o gasto por token de cliente. O preço é $0,15 por 1M de tokens de entrada e $0,50 por 1M de tokens de saída. Para uma chamada que usou 1.200 tokens de prompt e 350 tokens de conclusão, o custo é 1.200 x $0,15 / 1M mais 350 x $0,50 / 1M, que é $0,00018 mais $0,000175, ou $0,000355.

def cost_usd(usage):
    return usage["prompt_tokens"] * 0.15 / 1e6 + usage["completion_tokens"] * 0.50 / 1e6

Adicione-o após a análise da resposta JSON e armazene-o com o token de cliente. Para streams, peça ao cliente para incluir o uso no último chunk com stream_options. Requisições falhas ou recusadas não são cobradas, então conte apenas as respostas bem-sucedidas. As taxas atuais e bônus de recarga estão na página de preços.

Próximos passos

Você agora tem um gateway com autenticação, retentativas, limite de concorrência e streaming em cerca de 80 linhas. Extensões naturais são orçamentos por token, registro de requisições no seu próprio armazenamento e um segundo backend para failover. Mantenha o código pequeno; quando você precisar de muitos desses ao mesmo tempo, um projeto de gateway dedicado pode ser mais adequado.

Para executá-lo, você precisa de uma chave de backend. Cadastre-se e resgate o crédito de teste de $0,50 com um clique na página de chaves de API, depois verifique parâmetros e formatos de erro na documentação da API. Lembre-se de que uma chave de teste permite apenas 2 requisições simultâneas, então defina o semáforo para 2 durante os testes.

Perguntas e respostas

Sua chave está a um formulário de distância

Crie uma conta, copie a chave e altere a URL base. Essa é toda a configuração.

Obter chave de API
Preciso usar FastAPI especificamente?

Não. Qualquer framework assíncrono funciona. Os pontos-chave são encaminhar o corpo, manter um slot de concorrência para todo o stream e fazer retry apenas antes do streaming começar.

Por que forçar o nome do modelo no gateway?

Isso mantém uma política em um só lugar, impedindo que clientes solicitem um ID de modelo diferente. O backend serve um único modelo chamado uncensored.

Como saber se meu gateway não está atingindo o limite de requisições?

Verifique respostas 429 com o código de erro rate_limited ou concurrency, e leia X-RateLimit-Limit-Requests e X-RateLimit-Limit-Concurrency nas respostas.

O gateway pode executar chamadas de função?

Sim. As definições de ferramentas e os resultados das ferramentas fazem parte do corpo da requisição, então um gateway passthrough os encaminha sem alterações.