OpenAI Responses API¶
mlxcel은 mlxcel serve와 mlxcel-server 양쪽에서 OpenAI 호환 Responses API를 제공합니다. OpenAI Python 또는 TypeScript SDK의 client.responses.create(...) 표면을 이미 사용하는 클라이언트를 로컬 mlxcel 모델 서버로 연결할 때 유용합니다.
엔드포인트¶
| 메서드 | 경로 | 설명 |
|---|---|---|
POST | /v1/responses | 응답 생성(동기, SSE, 또는 background: true인 백그라운드 job). 백그라운드 모드 참조. |
GET | /v1/responses/{id} | 저장된 응답 조회 또는 백그라운드 응답 폴링. |
DELETE | /v1/responses/{id} | 저장된 응답 삭제. 끝나지 않은 백그라운드 응답은 400을 반환하므로 먼저 취소해야 합니다. |
POST | /v1/responses/{id}/cancel | 진행 중인 응답이나 백그라운드 job을 cancelled로 표시. |
POST | /v1/responses/compact | 이전 턴을 요약해 compaction 출력 item 하나로 만듭니다. 압축 참조. |
POST | /v1/responses/input_tokens | 요청 본문을 렌더링한 입력의 토큰 수를 {"input_tokens": N}으로 반환합니다. previous_response_id와 conversation 이력도 포함합니다. |
/v1이 없는 별칭(/responses, /responses/{id}, /responses/{id}/cancel, /responses/compact, /responses/input_tokens)도 함께 마운트됩니다.
빠른 예시¶
from openai import OpenAI
client = OpenAI(base_url="http://127.0.0.1:8080/v1", api_key="sk-no-auth")
resp = client.responses.create(
model="Qwen3.5-0.8B-4bit",
input="Reply with: hello",
max_output_tokens=64,
)
print(resp.output_text)
curl http://127.0.0.1:8080/v1/responses \
-H 'Content-Type: application/json' \
-d '{"model":"Qwen3.5-0.8B-4bit","input":"Reply with: hello","max_output_tokens":64}'
지원 요청 표면¶
| 필드 | 지원 범위 |
|---|---|
model | 로드된 모델 alias/path와 일치해야 합니다. |
input | 문자열 입력 또는 message/function-call item 배열. type 없이 role을 가진 item은 message로 읽고, assistant 메시지는 output_text와 refusal 콘텐츠 파트를 받으므로 클라이언트가 이전 response.output을 그대로 입력으로 다시 보낼 수 있습니다. |
instructions | system 성격의 프롬프트로 렌더링됩니다. previous_response_id로는 이어받지 않습니다. |
tools / tool_choice | function 도구만 지원합니다. hosted built-in 도구는 거부됩니다. |
parallel_tool_calls | 기존 tool-call parser로 전달됩니다. |
text.format | text 또는 json_schema 구조화 출력. |
reasoning | echo되며 모델이 reasoning trace를 방출하면 이를 표면화하는 데 사용됩니다. 같은 이름의 chat completions 필드와 달리 reasoning.effort는 reasoning_effort chat template kwarg로 매핑되지 않습니다. 여기서는 chat_template_kwargs로 직접 설정하십시오. |
previous_response_id | 저장된 응답을 대화 이력으로 복원합니다. conversation과 상호 배타적입니다. |
conversation | 인메모리 conversation별 transcript store를 사용합니다. |
context_management | [{"type": "compaction", "compact_threshold": N}]이면 이력을 자동으로 압축합니다. 압축 참조. |
store | 기본값은 true이며 false이면 응답을 저장하지 않고 반환합니다. |
stream | 단조 증가 sequence_number가 있는 typed SSE 이벤트를 방출합니다. |
max_output_tokens, temperature, top_p, top_logprobs | 가능한 경우 mlxcel 생성 설정으로 매핑됩니다. |
truncation | disabled만 허용하며 다른 값은 400을 반환합니다. |
background | true이면 queued 응답을 즉시 반환하고 생성을 job으로 실행합니다. 백그라운드 모드 참조. stream: true와 함께 쓰면 400을 반환합니다. |
web_search, file_search, computer_use_preview, code_interpreter, image_generation, mcp, custom, apply_patch, function_shell 같은 built-in tool 타입은 400 invalid_request_error를 반환합니다. mlxcel은 외부 hosted 도구 실행기를 포함하지 않기 때문입니다.
백그라운드 모드¶
POST /v1/responses에 "background": true를 주면 queued 응답을 HTTP 200으로 즉시 반환하고 생성을 분리된 job으로 실행합니다. GET /v1/responses/{id}로 상태가 종료될 때까지 폴링합니다.
curl -s http://127.0.0.1:8080/v1/responses -H 'Content-Type: application/json' \
-d '{"model":"local","input":"Write a haiku about rain.","background":true}'
# {"id":"resp_...","status":"queued","background":true,"output":[],...}
curl -s http://127.0.0.1:8080/v1/responses/resp_...
| 상태 | 의미 |
|---|---|
queued | 승인됐지만 생성은 시작하지 않았습니다. 미디어 디코딩, 자동 압축, 생성 슬롯 대기가 이 단계에서 일어납니다. |
in_progress | 모델이 prefill이나 생성을 시작했습니다. |
completed, incomplete | 종료됐고, 동기 응답과 같은 output, usage, incomplete_details를 가집니다. |
failed | error에 {code, message}가 들어 있습니다. code는 server_error, server_overloaded(job이 시작하려 할 때 큐가 가득 찼음), invalid_prompt(미디어 읽기, 프롬프트 렌더링, 자동 압축이 요청을 거부함) 중 하나입니다. |
cancelled | POST /v1/responses/{id}/cancel이 job을 멈췄습니다. |
백그라운드 응답의 모든 객체는 "background": true를 가지며, 동기 응답에는 이 키가 없습니다. job이 종료되기 전까지 output은 []이고 usage는 모두 0이라서 폴링으로 부분 출력을 볼 수 없습니다.
create가 답하는 경우는 다음과 같습니다.
- 생성 전에 동기 요청이 거부되는 모든 경우(형식이 잘못된 필드, 지원하지 않는 미디어, 잘못된 샘플링이나 도구 파라미터, 잘못된 스키마, 알 수 없는
previous_response_id)에400 invalid_request_error를 반환합니다.stream이true이거나 응답 저장소가 꺼져 있거나--responses-background-max-jobs, 응답 저장소 entry 한도,--responses-store-max-bytes중 하나가0일 때도 같습니다. job은 만들어지지 않습니다. --responses-background-max-jobs만큼 job이 이미 active이거나 active job이--responses-store-max-bytes를 채우면error.code: "rate_limit_exceeded"와 함께429를 반환합니다. job이 끝난 뒤 다시 시도하십시오.- 생성 큐가 가득 차면 동기 요청과 같이
503을 반환합니다.
create 연결이나 폴링을 끊어도 취소되지 않습니다. job을 멈추는 것은 cancel 엔드포인트뿐입니다. store: false job도 허용되며, 최종 객체는 --responses-background-ephemeral-ttl-secs 동안 폴링으로 읽을 수 있지만 previous_response_id로는 쓸 수 없습니다. 끝나지 않은 job을 previous_response_id로 지정하면 previous response '<id>' has not finished (status: <status>)와 함께 400을 반환하고, 종료된 뒤에는 다른 저장 응답처럼 이어집니다. conversation 이력은 job을 만들 때 읽고 출력과 함께 끝날 때 턴을 추가하므로 failed job은 아무것도 추가하지 않습니다. context_management는 job 안에서 실행되므로 압축 실패는 create 호출을 실패시키지 않고 job을 failed로 끝냅니다.
구조화 출력 제약은 create 시점에 만들므로 잘못된 스키마는 job 실패가 아니라 400입니다. job은 메모리에만 있으므로 프로세스를 멈추거나 다시 시작하면 큐에 있던 job과 실행 중이던 job이 사라지고 그 id는 이후 404를 반환합니다. 프로세스는 살아 있는데 생성 워커가 종료되면 해당 job은 failed로 끝납니다.
압축¶
POST /v1/responses/compact는 input에 담긴 전체 이력을 받아 response.compaction 타입 객체(id, created_at, model, output, usage)를 반환합니다. usage는 요약 생성의 토큰 수입니다. output에는 유지한 system·developer 메시지, 보존한 item, 그리고 compaction item 하나가 들어 있습니다. 아무것도 저장하지 않습니다.
- 보존 대상은 마지막 user 메시지와
keep_tokens안에 들어가는 최신 완결 턴입니다(기본값은 컨텍스트 창의 1/16, 최대 8192). 분할 지점은 user 턴 앞이나 완료된 도구 호출 묶음 뒤로 한정되므로 function call이 그 출력과 떨어지지 않습니다. - 요약은
/v1/responses워커 경로에서 비스트리밍 생성 한 번으로 만듭니다.max_output_tokens로 제한합니다(기본값 1024, 1에서 16384). stream,previous_response_id,conversation은 거부하며keep_tokens는 0보다 커야 합니다.- 이력이 컨텍스트 창 하나보다 길거나, 요약이 들어가지 않거나, 요약해도 컨텍스트가 짧아지지 않으면 코드
context_length_exceeded로400을 반환하고 아무것도 바꾸지 않습니다. compactionitem의encrypted_content는 요약과 보존 item을 담은 base64url JSON입니다. 암호화되어 있지 않으며 다른input배열과 같이 클라이언트 데이터로 취급합니다.
/v1/responses와 /v1/responses/input_tokens는 compaction 입력 item을 받아 재생합니다. 요약이 이전 턴을 대신하며 previous_response_id 체인에서도 마찬가지입니다.
context_management: [{"type": "compaction", "compact_threshold": N}](N은 1에서 100000000)을 지정하면 /v1/responses가 렌더링된 입력을 /v1/responses/input_tokens와 같은 방식으로 셉니다. 토큰 수가 N에 도달하거나 출력 공간이 남지 않으면 생성 전에 위와 같이 이력을 압축하고 output은 compaction item으로 시작합니다. 저장된 응답은 다음 previous_response_id 단계에서 요약을 재생하며, 임계값 아래에서는 다시 압축하지 않습니다. 스트리밍 요청은 response.in_progress 뒤, output_index 0의 compaction item 앞에 확장 이벤트 두 개를 보냅니다.
| 이벤트 | 필드 |
|---|---|
mlx.compaction.started | sequence_number, response_id, input_tokens(압축 전 렌더링된 입력) |
mlx.compaction.completed | sequence_number, response_id, input_tokens_before, input_tokens_after |
context_management가 있는 요청은 생성 길이가 프롬프트 뒤에 남은 컨텍스트로 제한되며, length로 멈추면 status: "incomplete"와 이유 max_output_tokens를 반환합니다. 압축은 스트림이 열리기 전에 끝나므로 압축 실패는 HTTP 오류입니다.
저장소 플래그¶
두 서버 진입점은 같은 store 제어 플래그를 받습니다.
| 플래그 | 기본값 | 환경 변수 | 비고 |
|---|---|---|---|
--responses-store-max-entries | 1024 | LLAMA_ARG_RESPONSES_STORE_MAX_ENTRIES | 0이면 응답 저장을 비활성화. |
--responses-store-max-bytes | 268435456 | MLXCEL_RESPONSES_STORE_MAX_BYTES | 보유 JSON byte의 대략적 예산입니다. 0이면 라우트는 살아 있지만 저장된 응답을 즉시 축출합니다. |
--responses-store-ttl-secs | 3600 | LLAMA_ARG_RESPONSES_STORE_TTL_SECS | 0이면 TTL 비활성화. |
--responses-background-max-jobs | 64 | MLXCEL_RESPONSES_BACKGROUND_MAX_JOBS | 동시에 active인 background: true 응답 수의 상한이며, 각 job은 생성 스레드 하나를 차지합니다. 0이면 background 모드를 비활성화합니다. |
--responses-background-ephemeral-ttl-secs | 600 | MLXCEL_RESPONSES_BACKGROUND_EPHEMERAL_TTL_SECS | background: true와 store: false를 함께 쓴 요청의 최종 객체를 읽을 수 있는 기간입니다. 최소 1이며 0이면 기동에 실패합니다. |
--conversation-store-max-entries | 256 | LLAMA_ARG_CONVERSATION_STORE_MAX_ENTRIES | 0이면 conversation transcript 비활성화. |
--conversation-store-max-bytes | 67108864 | MLXCEL_CONVERSATION_STORE_MAX_BYTES | 보유 JSON byte의 대략적 예산입니다. 0이면 라우트는 살아 있지만 transcript를 즉시 축출합니다. |
--conversation-store-ttl-secs | 3600 | LLAMA_ARG_CONVERSATION_STORE_TTL_SECS | 0이면 TTL 비활성화. |
active background job은 TTL과 LRU 한도 밖에 있어 실행 중에 축출되지 않습니다. 대략적인 byte 수는 job을 받아들일 때 --responses-store-max-bytes에 대해 계산하며, active 합계를 예산 밖으로 밀어내는 job은 거절합니다. 저장된 응답을 축출하는 일은 없습니다. 끝난 job의 최종 객체는 저장된 응답이 되는데, 그 객체 하나가 예산보다 크면 error.code: "server_error"와 빈 output을 가진 failed 응답을 대신 남겨 폴링이 답을 받게 합니다. store: false job의 최종 객체는 --responses-background-ephemeral-ttl-secs 동안만 남고 previous_response_id로 쓸 수 없습니다. background job은 메모리에만 있으므로 재시작하면 사라집니다. job 상한, entry 상한, byte 예산 중 하나라도 0이면 background 모드가 꺼집니다.
응답 저장이 비활성화되면 GET, DELETE, cancel, previous_response_id 요청은 400을 반환합니다. conversation store가 비활성화되면 conversation을 사용하는 요청이 400을 반환합니다.
스트리밍 이벤트¶
스트리밍 응답은 typed SSE frame으로 전송됩니다. 일반적인 이벤트 순서는 다음과 같습니다.
response.createdresponse.in_progressresponse.output_item.addedresponse.content_part.addedresponse.output_text.delta(0회 이상)response.output_text.doneresponse.content_part.doneresponse.output_item.doneresponse.completed
Function-call argument와 reasoning trace는 각각 response.function_call_arguments.*, response.reasoning_text.* 이벤트를 사용합니다. Hermes/Qwen JSON, Mistral([ARGS]와 JSON 목록 형식), Llama 3 JSON, Qwen3-Coder XML 형식에서는 argument가 생성되는 동안 response.function_call_arguments.delta 조각으로 스트리밍되고, 다른 도구 호출 형식은 생성이 끝날 때 호출 전체를 한 번에 보냅니다. 생성 종료 시점의 파싱 결과가 기준이므로 스트리밍된 호출은 id를 유지하고 아직 보내지 않은 나머지 argument를 받습니다. 생성 프롬프트가 이미 추론 블록을 연 상태로 끝나는 템플릿(<think>\n으로 끝남)에서는 첫 토큰부터 추론이 메시지보다 앞선 reasoning item 안에서 response.reasoning_text.delta로 스트리밍됩니다.
프롬프트 캐시가 켜져 있으면 서버는 일반 응답마다 반환한 reasoning item을 기억합니다. 이후 요청이 그 assistant 턴을 reasoning item 없이 다시 보내면 서버가 추론을 다시 넣어 주므로, 턴이 생성 당시와 같은 형태로 렌더링되고 프롬프트 캐시도 계속 일치합니다. 저장소 크기는 MLXCEL_REASONING_ECHO_MAX_BYTES로 제한합니다(기본값 16 MiB, 0이면 비활성화).