웹 port를 열지 않고
실제 Adapter를 끝까지 실행한다
Unit test와 실제 서비스 사이의 짧은 관찰 도구다. FastAPI의 진짜 lifespan에 진입해 readiness를 확인하고, 선택적으로 최대 16 token 생성까지 수행한다.
왜 이미 test가 있는데 smoke test도 필요한가?
MockTransport test는 변환 규칙을 빠르고 결정적으로 확인하지만 macOS network, 실제 Swift endpoint, model 파일, Metal 추론은 사용하지 않는다. 반대로 browser용 route를 만들기 전에도 Adapter 자체의 real path는 확인할 필요가 있다. Smoke command가 그 사이를 잇는다.
현재 전체 코드
"""Run the real application lifespan and adapter without opening a web port."""
import argparse
import asyncio
from app.engines.base import GenerationRequest, InferenceEngine, InferenceError, Message
from app.main import create_app
async def run(generate: bool) -> None:
application = create_app()
async with application.router.lifespan_context(application):
engine: InferenceEngine = application.state.inference_engine
model = await engine.check_ready()
print(f"ready model={model.id}")
if generate:
result = await engine.generate(GenerationRequest(
messages=(Message(role="user", content="Reply with exactly READY."),),
max_output_tokens=16,
temperature=0,
))
print(result.model_dump_json(indent=2))
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--generate", action="store_true", help="Also generate at most 16 tokens")
args = parser.parse_args()
try:
asyncio.run(run(args.generate))
except InferenceError as error:
parser.exit(1, f"inference error: {error.code} (upstream={error.upstream_status})\n")
if __name__ == "__main__":
main()실행 순서
asyncio.run일반 CLI의 main()에서 async event loop를 만들고 run()이 끝나면 닫는다.
실제 Settings와 같은 create_app()을 사용하지만 Uvicorn socket은 열지 않는다.
Production과 동일하게 두 HTTP client와 Adapter 하나가 생성된다. 종료 시 두 client가 닫히고 Adapter의 application 참조가 제거된다.
기본 실행은 readiness만, --generate를 주면 최대 16 token을 추가 생성한다.
실행 방법
Model process가 이미 있는지 먼저 확인하고 하나만 실행한다. Smoke command는 TurboFieldfare를 자동 시작하거나 종료하지 않는다.
2026-09-06 초기 구현의 실제 Gemma 결과
Host 실행은 약 13초, Docker 내부 실행도 약 13초가 걸렸다. 둘 다 HTTP 200이었다. 요청에도 마침표를 포함한 Reply with exactly READY.가 들어 있으므로 응답 READY.를 문장 부호 추가 오류로 해석하지 않는다. JSON의 text를 그대로 돌려주는 코드는 Adapter 소스와 변환 테스트로 확인한다.
cached_input_tokens=0은 cache가 고장났다는 뜻인가?
아니다. Smoke 코드는 실행할 때마다 user 메시지 하나만 보내며, 이전 assistant 응답과 다음 질문을 이어 붙이지 않는다. 같은 Swift 서버를 사용했다면 Python 프로세스나 HTTP client를 새로 만들었다는 이유만으로 서버 캐시가 초기화되지는 않는다.
현재 ServerPromptCache.match()는 먼저 모델·runtime·template 등 cache domain과 tools의 일치를 확인한다. 이후 기존 KV에 대응하는 token ID 전체가 새 입력의 앞부분과 정확히 맞고 새 입력이 더 긴지 검사하거나, 이전 user·assistant 기록과 후속 메시지를 이어 보낸 continuation 조건을 검사한다. 단순히 같은 질문을 다시 보낸다는 뜻도, message history만 보내면 항상 hit라는 뜻도 아니다.
따라서 여기서 확인한 것은 cached_input_tokens=0이라는 관찰값이다. 재사용 성공률·속도 향상·대화별 캐시 격리는 별도 검증이 필요하다. 이 설명의 근거는 Swift의 ServerPromptCache.swift와 ServerInference.swift다.
오류를 읽는 법
| 출력 | 뜻 | 우선 확인 |
|---|---|---|
unavailable | 연결 수립 또는 응답 전송 중 통신 실패 | TurboFieldfare 실행 여부, base URL |
model_mismatch | 모델 목록·생성 응답 ID 불일치 또는 404/model_not_found | --model-id, APP_INFERENCE_MODEL |
timeout | 정한 deadline 초과 | memory pressure, queue, timeout 설정 |
invalid_response | HTTP 200 JSON 계약 불일치 | server version과 Adapter schema |
실제 서버가 꺼진 상태에서도 command를 실행해 inference error: unavailable과 exit code 1이 나오는 것을 확인했다. 실패가 성공처럼 보이지 않는다는 검증이다.
보완 이후의 검증과 구분
위의 READY.와 소요 시간은 초기 구현에서 실제 Gemma로 측정한 기록이다. 연결 분리·오류 분류 보완에서는 모델을 다시 실행하지 않고 실제 TCP 회귀 테스트와 시간 초과·취소 테스트를 추가했다. 이전 live 결과를 새 버전의 재측정 결과로 해석하지 않는다.