FastAPI와 추론 서버 사이에
안정적인 번역 경계를 만든다
TurboFieldfare inference adapter는 모델 자체가 아니다. FastAPI 내부의 생성 요청을 TurboFieldfare HTTP JSON으로 번역하고, 응답과 실패를 서비스가 이해하는 일정한 형식으로 되돌리는 연결 계층이다.
왜 채팅 route보다 Adapter를 먼저 만드는가?
브라우저 요청을 받는 route가 TurboFieldfare의 URL, max_completion_tokens, OpenAI 응답의 choices[0], 오류 JSON까지 직접 알게 되면 추론 서버의 작은 변경이 route·RAG·MCP·test 전체로 퍼진다. 반대로 application이 generate(GenerationRequest)만 알면 TurboFieldfare 전용 지식은 한 파일에 머문다.
Adapter는 두 언어를 아는 통역사다
messages, max_output_tokens, temperature
model, max_completion_tokens, stream=false
OpenAI-compatible response
text, finish_reason, TokenUsage
여기서 “OpenAI-compatible”은 모든 OpenAI API를 지원한다는 뜻이 아니다. 현재 Swift 서버가 제공하는 정확한 세 endpoint와 필드만 사용한다. Adapter는 예상한 응답 구조를 Pydantic으로 다시 검증하므로 HTTP 200이라도 필수 필드가 틀리면 성공으로 위장하지 않는다.
실제 파일 구조와 책임
아래 파일들 사이를 요청이 어떤 순서로 옮겨 다니는지는 요청 흐름 연결도에서 흐름별로 볼 수 있다.
| 소스 | 답하는 질문 | 상세 문서 |
|---|---|---|
engines/base.py | 서비스가 추론 엔진에 요구하는 최소 능력은? | 내부 계약 |
engines/turbofieldfare.py | 내부 객체와 Swift JSON은 어떻게 변환되는가? | Adapter 구현 |
main.py | 생성·probe client를 왜 lifespan 동안 각각 공유하는가? | main.py 상세 |
core/config.py | host와 Docker 주소 및 timeout은 어떻게 나뉘는가? | config.py 상세 |
api/router.py | main.py는 어떻게 URL 경로를 모른 채 route를 붙이는가? | Route 모으기 |
api/dependencies.py | route는 구체 adapter를 어떻게 모른 채 사용하는가? | 의존성 주입 |
api/routes/system.py | Liveness와 readiness는 무엇이 다른가? | 상태 route |
schemas/system.py | 상태 응답의 JSON 형태는 어디에서 강제되는가? | 응답 schema |
schemas/chat.py | 브라우저가 보낼 것과 받을 것은 어디에서 정해지는가? | 브라우저 계약 |
api/routes/chat.py | 추론 실패 하나가 어떻게 HTTP status가 되는가? | 채팅 route |
engines/smoke.py | 웹 port 없이 실제 adapter를 어떻게 관찰하는가? | Smoke test |
이 단계의 자동 테스트는 무엇을 각각 붙잡는가
현재 47 passed는 여섯 파일에서 나온다. 아래 표는 각 파일이 지키는 서로 다른 질문을 정리한다. 테스트는 대부분 추론 서버 없이 실행되며, 실제 Gemma 생성 확인은 smoke.py가 담당한다.
| 테스트 파일 | 붙잡는 질문 | 상세 문서 |
|---|---|---|
tests/test_health.py | 추론 서버가 죽어도 /healthz는 200을 유지하는가? | 상태 endpoint 검증 |
tests/test_config.py | 잘못된 URL과 timeout이 시작 순간에 거부되는가? | 설정 검증 |
tests/test_chat.py | 채팅 API가 입력을 거르고 오류를 올바른 status로 바꾸는가? | 채팅 API 검증 |
tests/test_readiness_isolation.py | 생성 요청이 연결을 점유해도 readiness가 답하는가? | 연결 분리 검증 |
tests/test_inference_timeouts.py | 실제 TCP에서 deadline과 취소가 전파되는가? | 시간 초과·취소 검증 |
tests/test_inference_lifespan.py | 정상·예외·취소 종료에서 client가 모두 닫히는가? | 수명 관리 검증 |
tests/test_turbofieldfare_adapter.py | upstream 오류가 정해진 코드로 변환되는가? | 오류 변환 검증 |
이 backend를 실행하기 위한 주변 파일도 함께 문서화되어 있다. 설치할 package와 버전 고정은 requirements.txt, 환경변수 견본과 host.docker.internal 주소 문제는 .env.example, container image로 만드는 과정과 .dockerignore의 역할은 be/Dockerfile 상세에서 읽는다.
범용 계약과 TurboFieldfare wire format을 구분한다
| Application 내부 | TurboFieldfare로 보낼 때 | 분리 이유 |
|---|---|---|
max_output_tokens | max_completion_tokens | 특정 API의 명명법을 application에 퍼뜨리지 않는다. |
input_tokens | usage.prompt_tokens | 다른 엔진이 다른 응답 구조를 써도 usage 소비자는 유지된다. |
cached_input_tokens | prompt_tokens_details.cached_tokens | TurboFieldfare의 prefix KV 재사용 지표를 보존한다. |
InferenceError("busy") | HTTP 429 + queue_full | route가 Swift 오류 envelope를 직접 해석하지 않는다. |
이 구조가 Qwen을 바로 실행하게 해주는가?
아니다. Adapter 분리는 “호출하는 쪽”의 교체 비용을 줄일 뿐이다. Qwen을 실제로 실행하려면 tokenizer, chat template, MoE geometry, weight 변환, Metal kernel 등 추론 runtime 포팅이 별도로 필요하다. 다만 그 runtime이 같은 내부 계약을 구현하면 route와 RAG 코드는 덜 바뀐다.
왜 HTTP client를 요청마다 만들지 않는가?
lifespan이 HTTPX2 AsyncClient 두 개를 만든다. 생성용은 최대 연결 4개, probe용은 최대 2개로 서로 다른 풀을 사용한다. 이 시점에는 네트워크 연결이나 모델 로딩이 발생하지 않는다.
두 client, model ID, probe·generation timeout을 Adapter에 전달하고 application.state에 둔다.
연결 pool이 keep-alive TCP 연결을 재사용한다. 요청마다 client를 만들 때 생기는 handshake와 socket 낭비를 줄인다.
finally로 참조를 제거하고 async context가 두 client와 열린 연결을 닫는다. TurboFieldfare는 FastAPI가 시작한 process가 아니므로 종료하지 않는다.
오류를 숨기지 않고, 외부 문구에도 종속되지 않는다
| 관찰된 조건 | 내부 error code | 의미 |
|---|---|---|
| 연결 거절·DNS·network 오류 | unavailable | 연결 수립 또는 통신에 실패했다. 응답 도중 단절도 포함될 수 있다. |
| 정해진 시간을 초과 | timeout | 연결 또는 작업이 deadline을 넘었다. |
| connection pool 대기 초과 또는 HTTP 429 | busy | 현재 수용 여력이 없다. |
| 400 + context_length_exceeded | context_too_long | 모델의 문맥 길이 제한을 넘었다. |
| 400 + invalid_message | invalid_message | 메시지 내용이나 순서가 잘못됐다. |
| 404 + model_not_found | model_mismatch | 설정 모델을 서버가 제공하지 않는다. |
| 그 밖의 HTTP 4xx (429 제외) | request_rejected | 미지의 오류·깨진 오류 JSON도 안전한 공통 범주로 처리한다. |
| 5xx 또는 예상하지 않은 비-200 status (4xx 제외) | upstream_error | 서버 오류뿐 아니라 따라가지 않는 3xx redirect·예상하지 않은 2xx도 이 범주다. |
| HTTP 200이지만 JSON 계약 불일치 | invalid_response | 응답을 신뢰하고 전달할 수 없다. |
| 설정한 model ID가 목록/응답과 다름 | model_mismatch | 잘못된 server나 model에 연결했을 수 있다. |
원래 upstream 오류 문구와 생성 텍스트는 exception 문자열에 복사하지 않는다. 이후 로그가 추가되어도 prompt나 답변이 우연히 노출될 가능성을 줄이기 위해서다.
/healthz와 /readyz는 서로 다른 질문이다
TurboFieldfare가 꺼져 있어도 200이다. process와 기본 HTTP stack의 생존 신호다.
/health와 /v1/models를 호출한다. 연결 실패나 model 불일치면 503이다.
검토에서 발견한 네 문제를 어떻게 보완했나?
| 보완 항목 | 변경과 증거 | 코드 해설 |
|---|---|---|
| Readiness 연결 분리 | 생성 4개 점유 중에도 상태 전용 연결로 검사. 정상 모델 200, 다른 모델 503/model_mismatch. | 연결 분리 테스트 |
| 오류 분류 보강 | 문맥 초과·메시지 오류·모델 불일치를 구분. 알려진 status·code만 매핑하고 원문은 비노출. | 오류 변환 테스트 |
| 실제 회귀 검증 | 실제 deadline·pool timeout·취소 후 socket 반환 및 후속 요청, 정상·예외·취소 lifespan 정리 확인. | 시간 초과·취소 수명 관리 |
| 학습 문서 정정 | 서비스용 용어 사전을 별도로 적용. FastAPI Router·app.state와 MoE·React 의미를 구분하고 코드·결과 갱신. | 용도별 연결 구조 |
2단계 보완 시점의 자동 테스트는 47 passed였고, 3단계 채팅 API까지 더해 현재 76 passed다. 수정된 local-moe-be:step2 이미지도 빌드했다. 외부 네트워크 없는 임시 container에서 non-root 실행, /healthz=200, 연결 불가 시 /readyz=503/unavailable, 새 오류 schema와 lifespan 종료를 확인했다. 이 Docker 검증은 ASGI TestClient 경로이며 실제 Gemma 생성이나 공개 port 연결을 재측정한 것은 아니다.
자동 검증과 초기 live 검증을 구분해서 읽기
자동 검증
health·models·generate
user=app
host.docker.internal
위 생성 기록은 연결 분리 전 초기 구현의 실제 Gemma 결과이며, 이번 문서 검토의 재측정값은 아니다. 요청 문자열 자체가 Reply with exactly READY.였고 결과도 READY.였으므로, 마침표가 있다는 이유만으로 지시 불이행이라고 해석할 수 없다. Adapter는 응답 문자열을 그대로 전달했다. 당시 host와 Docker의 생성은 각각 약 13초였고 cached token 관찰값은 0이었다. Smoke 명령은 이전 assistant 응답과 후속 user 메시지를 이어 보내지 않으므로, 이 관찰만으로 대화 continuation의 캐시 재사용을 검증한 것은 아니다.
용어 설명은 어떻게 서비스 문맥을 유지하는가?
각 HTML은 data-study-context="backend" 또는 frontend 값을 선언한다. service-terms.js가 먼저 서비스용 설명을 준비하고, 공통 terms.js가 이를 반영해 굵은 밑줄과 클릭 팝업을 만든다. 폴더 경로를 추측하지 않고 HTML의 명시적 문맥을 사용하므로 문서 폴더를 옮겨도 의미가 바뀌지 않는다.
Backend의 Router는 HTTP 요청을 함수로 연결하며, app.state는 server의 공유 객체 보관 공간이다. React의 state는 화면 렌더링에 쓰이고, 기존 MoE 학습 페이지의 Router는 계속 Expert 선택기로 설명된다. 코드 블록·SVG 내부에는 팝업용 HTML을 끼워 넣지 않아 코드 표시와 도형 글자가 변형되지 않게 했다.
문서 검증을 다시 실행하는 방법
첫 도구는 Python 표준 라이브러리만 사용해 17개 HTML의 상대 링크·asset 존재, 용어 사전 로딩 순서, 전체 소스 6개와 Adapter 코드 구간 5개의 일치를 확인한다. 둘째는 Node 22와 Mac의 Chrome을 이용해 1440·820·390px에서 페이지 전체 가로 넘침, JavaScript 오류, 외부 HTTP resource 요청 여부와 실제 용어 팝업을 검사한다. npm 설치나 문서용 HTTP server는 필요 없다.
두 검사를 통과했다. Chrome 임시 프로필만 사용하며 기존 브라우저 프로필에는 접근하지 않는다. 이는 실제 Safari·iPad에서 Apple Pencil까지 재검증했다는 뜻이 아니다.