콘텐츠로 이동

Python 클라이언트

범위

Python은 client-side 편의 도구일 뿐 추론 runtime dependency가 아닙니다. 엔진은 네이티브 mlxcel / mlxcel-server 바이너리이고, 여기 나오는 모든 Python 경로는 그 HTTP API를 호출합니다.

방법은 둘입니다. python/에 함께 배포되는 mlxcel 패키지가 서버를 감싸 주며, 로컬 서버를 직접 띄우고 감독하거나(managed 모드) 이미 띄워 둔 서버에 접속합니다(connect 모드). 아니면 패키지를 쓰지 않고 OpenAI 공식 SDK나 일반 HTTP 클라이언트로 API를 직접 호출합니다. 이 페이지의 나머지가 후자입니다.

함께 배포되는 mlxcel 패키지

pip install ./python          # 저장소 체크아웃에서
pip install "./python[dev]"   # pytest, ruff, mypy 포함

Python 3.9 이상, openai>=1.40과 httpx>=0.27이 필요합니다. managed 모드는 mlxcel 바이너리도 필요하며, 클라이언트는 binary= 인자, MLXCEL_BIN, PATH 위의 mlxcel 순으로 찾습니다.

managed 모드. 모델을 넘기면 클라이언트가 mlxcel serve를 띄우고 서버가 준비를 보고할 때까지 기다렸다가 종료 시 정리합니다. 리스너가 바인딩되기 전에 모델을 로드하고 워밍업하므로, 가중치를 내려받는 첫 실행은 오래 걸릴 수 있습니다. 클라이언트는 기본 600초인 startup_timeout 동안 /health를 폴링하면서 자식 프로세스가 일찍 죽는지도 함께 봅니다.

import mlxcel

with mlxcel.LLM("mlx-community/Qwen3-4B-4bit") as llm:
    print(llm.generate("def fib(n):", max_tokens=128, temperature=0.7))
    print(llm.chat([{"role": "user", "content": "Hello"}], max_tokens=64))
    print(llm.model)     # /v1/models에서 자동 발견한 모델 id

종료를 보장하므로 컨텍스트 매니저 형태를 쓰십시오. 핸들이 새어 나가도 atexit 정리와 best-effort finalizer가 받아 줍니다.

생성자 인자는 둘로 갈립니다. binary=, host= / port=(TCP 강제), socket=(명시적 Unix 소켓 경로), api_key=, startup_timeout=은 클라이언트를 설정합니다. 나머지는 **server_kwargs로 전달되어 띄우는 mlxcel serve 프로세스 쪽을 설정합니다. ctx_size=, n_predict=, alias=, warmup=, 그리고 그대로 전달되는 extra_args=[...]가 여기 해당합니다.

with mlxcel.LLM(
    "mlx-community/Qwen3-4B-4bit",
    ctx_size=8192,
    warmup=False,
    extra_args=["--parallel", "1"],
) as llm:
    print(llm.generate("hello", max_tokens=32))

connect 모드. 모델 없이 base_url=이나 socket=을 넘기면 아무 프로세스도 띄우거나 감독하지 않습니다.

llm = mlxcel.LLM(base_url="http://localhost:8080/v1")   # TCP
llm = mlxcel.LLM(socket="/tmp/mlxcel.sock")             # Unix 도메인 소켓

모델과 접속 대상을 함께 넘기면 모드가 모호해지므로 MlxcelError를 냅니다. AsyncLLM은 같은 인자를 받고 awaitable을 돌려줍니다. 래퍼가 노출하지 않는 것은 llm.openai_client가 밑단 openai 클라이언트이므로 tools, response_format, logprobs, 멀티모달까지 그대로 닿습니다.

샘플링 파라미터. generate와 chat은 OpenAI 샘플링 필드(max_tokens, temperature, top_p, stop, seed, presence_penalty, frequency_penalty, logit_bias, n, logprobs, top_logprobs)를 요청 최상위 필드로 보냅니다. top_k, min_p, repetition_penalty, DRY 설정 같은 서버 전용 값은 SDK의 extra_body를 거쳐 요청 본문에 실리고, 알 수 없는 키워드 인자(예: p_less=True)도 같은 경로로 갑니다. 값이 겹치면 직접 넘긴 extra_body={...}가 우선합니다. top_k가 모델 어휘 크기 이상이면 llama-server와 마찬가지로 0(비활성)과 같아서 모든 토큰이 남습니다.

llm.generate("Once upon a time", max_tokens=200, top_p=0.9, top_k=40, min_p=0.05)

HTTP API 직접 호출

이 페이지의 나머지는 직접 띄운 서버를 OpenAI SDK와 httpx로 호출합니다.

서버 시작

mlxcel serve \
  --model mlx-community/Qwen3-4B-4bit \
  --host 127.0.0.1 \
  --port 8080 \
  --api-key local-secret

Standalone server binary를 배포할 때는 mlxcel-server를 사용합니다. 원격 접근을 의도적으로 보호하지 않았다면 service를 loopback에만 bind하십시오.

클라이언트 라이브러리 설치

python -m pip install openai httpx

openai는 OpenAI-compatible API를 호출합니다. httpx는 /tokenize 같은 mlxcel native helper와 SDK가 노출하지 않는 endpoint에 유용합니다.

동기 Chat

from openai import OpenAI

client = OpenAI(
    base_url="http://127.0.0.1:8080/v1",
    api_key="local-secret",
)

response = client.chat.completions.create(
    model="default",
    messages=[{"role": "user", "content": "Explain unified memory briefly."}],
    temperature=0.0,
    max_tokens=128,
)
print(response.choices[0].message.content)

배포 환경이 default를 쓰지 않으면 client.models.list()로 광고된 model id를 확인하십시오.

스트리밍

stream = client.chat.completions.create(
    model="default",
    messages=[{"role": "user", "content": "Count from one to five."}],
    stream=True,
)

for chunk in stream:
    delta = chunk.choices[0].delta.content
    if delta:
        print(delta, end="", flush=True)
print()

Response를 닫거나 client task를 취소하면 request가 disconnect되고, 서버는 지원되는 단계에서 disconnect cancellation을 감지합니다.

비동기 Chat

import asyncio
from openai import AsyncOpenAI

async def main() -> None:
    client = AsyncOpenAI(
        base_url="http://127.0.0.1:8080/v1",
        api_key="local-secret",
    )
    response = await client.chat.completions.create(
        model="default",
        messages=[{"role": "user", "content": "Say hello in Korean."}],
        max_tokens=64,
    )
    print(response.choices[0].message.content)
    await client.close()

asyncio.run(main())

Responses API

최근 OpenAI SDK는 mlxcel의 /v1/responses endpoint를 호출할 수 있습니다.

response = client.responses.create(
    model="default",
    input="Summarize why KV caches matter in one sentence.",
    max_output_tokens=80,
)
print(response.output_text)

mlxcel은 Responses API에 명시된 subset을 구현합니다. Hosted OpenAI 전용 tool이나 모든 SDK field를 사용할 수 있다고 가정하지 마십시오.

멀티모달 Chat

REST API에 설명된 표준 chat content-part 형식을 사용합니다. 예를 들어 inline image는 data URI로 전달할 수 있습니다.

response = client.chat.completions.create(
    model="default",
    messages=[{
        "role": "user",
        "content": [
            {"type": "text", "text": "Describe this image."},
            {"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}},
        ],
    }],
    max_tokens=128,
)

로드된 모델이 요청 modality를 구현해야 합니다. Image/audio URL 및 local-path 처리는 REST manual의 보안 제한을 따르며 API key는 network 또는 file access를 sandbox하지 않습니다.

임베딩

임베딩 체크포인트를 -m에 지정하거나 --embedding-model로 대화 모델 옆에 추가하여 서버를 시작합니다. OpenAI SDK는 /v1/embeddings를 직접 호출합니다.

response = client.embeddings.create(
    model="Qwen3-Embedding-0.6B",
    input=[
        "벡터 검색이란 무엇인가?",
        "벡터 검색은 가까운 임베딩을 찾는다.",
    ],
)
for item in response.data:
    print(item.index, item.embedding[:8])

형식 지정 이미지 입력, instruction, 요청별 embd_normalize 같은 mlxcel 확장에는 일반 HTTP 클라이언트를 사용합니다. 후기 상호작용 모델의 응답은 평면 벡터 하나가 아니라 행렬이며 base64 응답에는 shape가 들어 있습니다.

httpx를 사용한 재순위화

OpenAI SDK에는 재순위화 자원이 정의되어 있지 않습니다. 시퀀스 분류기를 -m에 두거나 --reranker-model을 사용하여 서버를 시작한 뒤 httpx로 /v1/rerank를 호출합니다.

import httpx

headers = {"Authorization": "Bearer local-secret"}
with httpx.Client(base_url="http://127.0.0.1:8080", headers=headers) as http:
    result = http.post(
        "/v1/rerank",
        json={
            "query": "벡터 검색이란 무엇인가?",
            "documents": [
                "벡터 검색은 가까운 임베딩을 찾는다.",
                "컴파일러는 소스 코드를 번역한다.",
            ],
            "top_n": 1,
            "return_documents": True,
        },
    )
    result.raise_for_status()
    print(result.json()["results"])

documents 대신 texts를 사용하면 TEI 호환 배열 응답을 요청합니다. OpenAI, llama-server 고유, Jina/Cohere, TEI 형식을 같은 것으로 가정하기 전에 임베딩 및 재순위화 API를 확인하십시오.

httpx로 Native Tokenization 호출

import httpx

headers = {"Authorization": "Bearer local-secret"}
with httpx.Client(base_url="http://127.0.0.1:8080", headers=headers) as http:
    result = http.post("/tokenize", json={"content": "Hello, mlxcel!"})
    result.raise_for_status()
    print(result.json())

역변환은 /detokenize를 사용합니다. 이 native endpoint는 OpenAI SDK method가 아닙니다.

오류 처리

import openai

try:
    client.chat.completions.create(
        model="default",
        messages=[{"role": "user", "content": "Hello"}],
    )
except openai.AuthenticationError as exc:
    print("Bearer API key를 확인하십시오", exc)
except openai.BadRequestError as exc:
    print("지원하지 않거나 잘못된 요청입니다", exc)
except openai.APIStatusError as exc:
    print("server returned", exc.status_code, exc)

Prefill queue가 가득 차면 HTTP 503을 반환합니다. SDK retry는 workload에 맞게 의도적으로 설정하십시오. 무조건 재시도하면 overload를 키울 수 있습니다. mlxcel error object는 REST API 오류 처리를 참조하십시오.