<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>leetusik_.log</title>
        <link>https://velog.io/</link>
        <description>problem solver</description>
        <lastBuildDate>Sun, 15 Feb 2026 05:54:21 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>leetusik_.log</title>
            <url>https://velog.velcdn.com/images/leetusik_/profile/c139e531-2f6e-47b8-8dfc-964e2c47a5c0/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. leetusik_.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/leetusik_" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Quant Trading with Claude Code (feat: OpenClaw)]]></title>
            <link>https://velog.io/@leetusik_/Quant-Trading-with-Claude-Code-feat-OpenClaw</link>
            <guid>https://velog.io/@leetusik_/Quant-Trading-with-Claude-Code-feat-OpenClaw</guid>
            <pubDate>Sun, 15 Feb 2026 05:54:21 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/leetusik_/post/8a086434-da32-4d0d-8106-1065abcded3f/image.png" alt="">
투자를 해야한다는 것은 알고 있지만, 실력과 시간이 부족해서 자동 투자를 시작하지 못하고 있었다.
오늘은 <strong>Claude Code + OpenClaw</strong>를 적극 활용하여 하루 만에 만든 자동 투자 시스템을 소개한다.</p>
<h2 id="목차">목차</h2>
<ol>
<li>AI 자동 투자에서의 중요한 점 — 전략과 백테스팅</li>
<li>시스템 구조와 현재 워크플로우</li>
<li>전략 소개</li>
<li>사용한 API와 데이터 소스</li>
<li>발전 방향 — OpenClaw 자동화</li>
</ol>
<hr>
<h2 id="1-ai-자동-투자에서의-중요한-점--전략과-백테스팅">1. AI 자동 투자에서의 중요한 점 — 전략과 백테스팅</h2>
<p>&quot;AI한테 투자해줘~&quot; 라고 하면 될 것 같지만, 현실은 다르다. 작동 안 할 확률이 높다.</p>
<p>핵심은 <strong>좋은 전략을 찾고, 백테스팅으로 검증하는 것</strong>이다. AI가 잘하는 건 코드를 빠르게 짜는 것이지, 마법처럼 수익을 내는 것이 아니다.</p>
<p>내가 한 과정:</p>
<ol>
<li>인터넷/논문에서 검증된 퀀트 전략들을 조사</li>
<li>Claude Code로 백테스팅 프레임워크를 구축</li>
<li>수십 개의 전략 변형을 배치로 돌려 비교</li>
<li>Sharpe ratio, CAGR, MaxDD 기준으로 최적 전략 선별</li>
<li>선별된 전략을 실전 투자 시스템에 배포</li>
</ol>
<p>요즘에는 인터넷에서 쉽게 접할 수 있는 전략이 정말 많다. 중요한 것은 그걸 <strong>자기 상황에 맞게 파라미터를 조정하고, 과거 데이터로 검증</strong>하는 과정이다.</p>
<hr>
<h2 id="2-시스템-구조와-현재-워크플로우">2. 시스템 구조와 현재 워크플로우</h2>
<p>프로젝트는 크게 두 모듈로 나뉜다:</p>
<pre><code>quanty/
├── backtest/          ← 백테스팅 프레임워크
│   ├── src/quanty/
│   │   ├── data/      ← 데이터 소스 (Yahoo, FRED, Upbit)
│   │   ├── strategy/  ← 전략 구현체들
│   │   ├── engine/    ← 백테스트 엔진, 포트폴리오, 메트릭스
│   │   └── report/    ← 리포트 생성
│   └── strategies/    ← TOML 설정 파일들
│
├── invest/            ← 실전 투자 시스템
│   ├── src/invest/
│   │   ├── broker/    ← KIS(한국투자증권), Upbit 브로커
│   │   ├── executor/  ← 매매 실행 엔진
│   │   ├── scheduler/ ← APScheduler 기반 스케줄러
│   │   ├── notify/    ← Telegram 알림
│   │   └── strategy/  ← 백테스트 전략을 실전에 연결하는 어댑터
│   └── docker-compose.yml</code></pre><h3 id="워크플로우">워크플로우</h3>
<pre><code>[백테스팅]
전략 TOML 작성 → 백테스트 엔진 실행 → 메트릭스 비교 → 최적 전략 선정
                                                              ↓
[실전 투자]
스케줄러 (매주 화요일 10AM ET) → 시그널 생성 → 리스크 체크 → 매매 실행 → Telegram 알림</code></pre><p><strong>실전 매매 흐름 (상세)</strong>:</p>
<ol>
<li><code>MultiStrategyRunner</code>가 각 전략의 시그널을 생성</li>
<li>전략별 가중치를 배분 비율에 따라 병합: <code>merged_weight[sym] = Σ(전략_weight × allocation)</code></li>
<li>리스크 체크 통과 여부 확인</li>
<li>마켓 시간 확인 (장 마감이면 다음 개장까지 대기)</li>
<li>브로커별 독립 포트폴리오로 매매 계산:<ul>
<li><strong>KIS</strong>: SPY/GLD/BND의 USD 주식 매매</li>
<li><strong>Upbit</strong>: BTC의 KRW 암호화폐 매매</li>
</ul>
</li>
<li>매도 먼저 실행 → 매수 실행 (시장가 주문)</li>
<li>Telegram으로 결과 알림 (포트폴리오 현황, 목표 비중, 체결 내역, 백테스트 벤치마크 비교)</li>
</ol>
<h3 id="멀티-전략-지원">멀티 전략 지원</h3>
<p><code>invest/strategies/live.toml</code>에서 여러 전략을 조합할 수 있다:</p>
<pre><code class="language-toml">[[strategies]]
config = &quot;strategies/macro_rotation_weekly.toml&quot;
initial_balance = 3000    # 비율: 3000/(3000+1000) = 75%
rebalance_weekday = 1     # 화요일

[[strategies]]
config = &quot;strategies/zscore_btc.toml&quot;
initial_balance = 1000    # 25%
rebalance_weekday = 1</code></pre>
<p><code>initial_balance</code>는 실제 금액이 아니라 <strong>비율 계산용</strong>이다. 실제 매매는 브로커의 실시간 잔고를 기준으로 한다.</p>
<hr>
<h2 id="3-전략-소개">3. 전략 소개</h2>
<h3 id="a-macro-rotation-매크로-로테이션">A. Macro Rotation (매크로 로테이션)</h3>
<p><strong>핵심 아이디어</strong>: 거시경제 지표를 feature로 사용해 자산별 수익률을 예측하고, 포트폴리오를 자동 배분한다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td><strong>투자 유니버스</strong></td>
<td>SPY(미국주식), GLD(금), BND(채권), BTC(비트코인)</td>
</tr>
<tr>
<td><strong>매크로 피처</strong></td>
<td>VIX(변동성), 10Y-3M 수익률 곡선, FFR(기준금리)</td>
</tr>
<tr>
<td><strong>모델</strong></td>
<td>RandomForest (n_estimators=100, max_depth=3)</td>
</tr>
<tr>
<td><strong>학습 윈도우</strong></td>
<td>롤링 4년</td>
</tr>
<tr>
<td><strong>리밸런싱</strong></td>
<td>월간 / 주간 변형</td>
</tr>
<tr>
<td><strong>BTC 상한</strong></td>
<td>10% (초과분은 ETF로 재분배)</td>
</tr>
</tbody></table>
<p>작동 방식:</p>
<ol>
<li>과거 4년 데이터로 각 자산의 N일 후 수익률을 학습</li>
<li>현재 매크로 지표를 입력하여 각 자산의 미래 수익률을 예측</li>
<li>양수 수익률이 예측된 자산에만 역변동성 가중으로 배분</li>
<li>예측 수익률이 모두 음수면 → 전액 현금</li>
</ol>
<h3 id="b-z-score-크립토-트렌드-팔로잉">B. Z-Score (크립토 트렌드 팔로잉)</h3>
<p><strong>핵심 아이디어</strong>: 가격의 Z-Score를 계산하여 추세를 추종한다. 상승 추세에 진입, 하락 전환 시 퇴장.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td><strong>투자 유니버스</strong></td>
<td>BTC, ETH, XRP, SOL (개별 또는 조합)</td>
</tr>
<tr>
<td><strong>데이터</strong></td>
<td>Upbit 1시간봉 캔들</td>
</tr>
<tr>
<td><strong>지표</strong></td>
<td>Z-Score = (가격 - EMA) / 표준편차</td>
</tr>
<tr>
<td><strong>진입 기준</strong></td>
<td>Z &gt; 0.5 (상승 추세)</td>
</tr>
<tr>
<td><strong>퇴장 기준</strong></td>
<td>Z &lt; -0.5 (하락 추세)</td>
</tr>
<tr>
<td><strong>EMA 기간</strong></td>
<td>65</td>
</tr>
</tbody></table>
<p>히스테리시스 설계로 빈번한 매매를 방지한다. 낮은 승률(~47%)이지만, 평균 수익 &gt;&gt; 평균 손실로 양의 기대값을 가진다.</p>
<h3 id="c-macro--z-score-하이브리드">C. Macro + Z-Score 하이브리드</h3>
<p><strong>핵심 아이디어</strong>: 매크로 로테이션(ETF)과 Z-Score(크립토)를 결합한다.</p>
<table>
<thead>
<tr>
<th>설정</th>
<th>매크로(ETF)</th>
<th>크립토</th>
</tr>
</thead>
<tbody><tr>
<td>95/5</td>
<td>95%</td>
<td>5%</td>
</tr>
<tr>
<td>90/10</td>
<td>90%</td>
<td>10%</td>
</tr>
<tr>
<td>85/15</td>
<td>85%</td>
<td>15%</td>
</tr>
<tr>
<td>80/20</td>
<td>80%</td>
<td>20%</td>
</tr>
</tbody></table>
<p>매크로 부분은 월간/주간 리밸런싱, 크립토 부분은 Z-Score 기반 일간 신호.</p>
<h3 id="d-dual-momentum-듀얼-모멘텀">D. Dual Momentum (듀얼 모멘텀)</h3>
<p><strong>핵심 아이디어</strong>: 절대 모멘텀 + 상대 모멘텀으로 크립토를 선별한다.</p>
<ol>
<li><strong>절대 모멘텀</strong>: 21일 수익률 &gt; 0인 코인만 통과 (추세 필터)</li>
<li><strong>상대 모멘텀</strong>: 통과한 코인 중 상위 2개 선택</li>
<li>선택된 코인에 균등 배분, 통과한 코인이 없으면 → 100% 현금</li>
</ol>
<h3 id="배치-백테스트-비교">배치 백테스트 비교</h3>
<p>모든 전략을 한 번에 비교할 수 있다:</p>
<pre><code class="language-bash">uv run python -m quanty --batch strategies/all.toml</code></pre>
<p>결과는 Sharpe ratio 기준 내림차순으로 정렬되어 출력되고, <code>results/comparison_&lt;timestamp&gt;.txt</code>에 자동 저장된다.</p>
<pre><code>================================================================
  STRATEGY COMPARISON
================================================================
          Strategy   CAGR  Sharpe  Sortino   MaxDD    Vol  Calmar  Trades  Final($)
----------------------------------------------------------------
     macro_zscore  24.10%    0.26     0.10  -23.78%  2.52%    1.01    2602  465,406
   macro_rotation  11.22%    0.85     1.34  -18.24%  9.01%    0.62      73  198,450
              ...
================================================================
  (Sorted by Sharpe ratio, descending)</code></pre><hr>
<h2 id="4-사용한-api와-데이터-소스">4. 사용한 API와 데이터 소스</h2>
<h3 id="데이터-수집">데이터 수집</h3>
<table>
<thead>
<tr>
<th>소스</th>
<th>용도</th>
<th>API</th>
<th>캐시</th>
</tr>
</thead>
<tbody><tr>
<td><strong>FRED</strong> (연방준비은행)</td>
<td>VIX, 수익률곡선, 기준금리</td>
<td>공식 REST API (<code>api.stlouisfed.org</code>)</td>
<td>PostgreSQL</td>
</tr>
<tr>
<td><strong>Upbit</strong></td>
<td>크립토 1시간봉 캔들</td>
<td>REST API (<code>api.upbit.com</code>)</td>
<td>PostgreSQL</td>
</tr>
<tr>
<td><strong>Yahoo Finance</strong></td>
<td>미국 ETF 일봉</td>
<td><code>yfinance</code> 라이브러리</td>
<td>Parquet 파일</td>
</tr>
</tbody></table>
<p><strong>FRED 공식 API</strong>:</p>
<ul>
<li>무료 API 키 발급: <a href="https://fred.stlouisfed.org/docs/api/api_key.html">https://fred.stlouisfed.org/docs/api/api_key.html</a></li>
<li>Rate limit: 120 req/min (충분)</li>
<li>처음에 CSV 스크래핑 엔드포인트를 썼다가 502/504 에러로 고생 → 공식 API로 전환하니 안정적</li>
<li>PostgreSQL에 캐시하여 한 번 받은 데이터는 다시 API 호출 불필요 (gap-fill 패턴)</li>
</ul>
<p><strong>심볼 라우팅</strong>:</p>
<pre><code>&quot;FRED:VIXCLS&quot;    → FRED 공식 API → PostgreSQL 캐시
&quot;UPBIT:BTC:60m&quot;  → Upbit REST API → PostgreSQL 캐시
&quot;SPY&quot;            → Yahoo Finance  → Parquet 파일 캐시</code></pre><h3 id="실전-매매-브로커">실전 매매 브로커</h3>
<table>
<thead>
<tr>
<th>브로커</th>
<th>대상</th>
<th>인증</th>
<th>매매 방식</th>
</tr>
</thead>
<tbody><tr>
<td><strong>한국투자증권 (KIS)</strong></td>
<td>SPY, GLD, BND</td>
<td>OAuth 2.0 (24시간 토큰)</td>
<td>해외주식 시장가 주문</td>
</tr>
<tr>
<td><strong>Upbit</strong></td>
<td>BTC</td>
<td>JWT (요청별 서명)</td>
<td>시장가 주문</td>
</tr>
</tbody></table>
<p><strong>KIS</strong>: 실전/모의투자 계정 모두 지원 (<code>KIS_ACCOUNT_TYPE=01</code> 실전, <code>03</code> 모의)</p>
<h3 id="telegram-알림">Telegram 알림</h3>
<p>리밸런싱 시 알림 내용:</p>
<ul>
<li>포트폴리오 총 가치 (KIS USD + Upbit KRW→USD 환산)</li>
<li>목표 비중과 예측 수익률</li>
<li>실행된 매매 내역</li>
<li>백테스트 벤치마크 비교 (CAGR, Sharpe, MaxDD, 월간 승률)</li>
</ul>
<p>매일 4:30 PM ET에 일간 스냅샷도 전송한다.</p>
<hr>
<h2 id="5-배포-구조">5. 배포 구조</h2>
<p>Docker Compose로 배포한다:</p>
<pre><code class="language-yaml">services:
  db:
    image: postgres:16-alpine     # 매매 기록, 시그널 저장
  app:
    build: invest/Dockerfile      # Python 3.13 + uv
    env_file: .env.deploy         # API 키, 브로커 설정
    restart: unless-stopped       # 자동 재시작</code></pre>
<p>스케줄러 (APScheduler):</p>
<ul>
<li><strong>리밸런싱</strong>: 매주 화요일 10:00 AM ET (뉴욕 시간)</li>
<li><strong>일간 스냅샷</strong>: 평일 4:30 PM ET</li>
<li><strong>KIS 토큰 갱신</strong>: 12시간마다</li>
<li><strong>헬스체크</strong>: 5분마다</li>
<li><strong>장 마감 시</strong>: 다음 개장(9:30 AM ET)까지 자동 대기</li>
</ul>
<hr>
<h2 id="6-발전-방향--openclaw-자동화">6. 발전 방향 — OpenClaw 자동화</h2>
<p>현재는 직접 전략을 찾아서 구현하고 백테스트하는 구조다. 이걸 <strong>OpenClaw로 자동화</strong>하면 어떨까?</p>
<h3 id="구상하는-파이프라인">구상하는 파이프라인</h3>
<pre><code>[Daily Quant Digest]         ← 매일 퀀트 리서치/뉴스레터 수집
        ↓
[전략 추출]                  ← OpenClaw가 논문/기사에서 전략 로직 추출
        ↓
[자동 코딩]                  ← 백테스팅 코드 자동 생성 (TOML + Strategy 클래스)
        ↓
[백테스트 실행]              ← 기존 프레임워크에서 자동 실행
        ↓
[결과 필터링]                ← Sharpe &gt; 0.8, MaxDD &lt; -25% 등 기준으로 선별
        ↓
[Telegram 알림]              ← &quot;유의미한 전략 발견!&quot; 사용자에게 통지</code></pre><h3 id="openclaw-cron-job-설계">OpenClaw Cron Job 설계</h3>
<pre><code>0 6 * * * /openclaw/daily_quant_digest.py     # 매일 새벽 6시
0 7 * * * /openclaw/extract_strategies.py      # 전략 추출
0 8 * * * /openclaw/auto_backtest.py           # 자동 백테스트
0 9 * * * /openclaw/notify_findings.py         # 결과 알림</code></pre><p><strong>기대 효과</strong>:</p>
<ul>
<li>매일 새로운 전략을 자동으로 발굴하고 검증</li>
<li>사람이 할 일: 알림을 보고 최종 판단만 하면 됨</li>
<li>점점 더 많은 전략이 축적되면서 포트폴리오 다각화 가능</li>
</ul>
<h3 id="추가-발전-방향">추가 발전 방향</h3>
<ol>
<li><strong>실시간 모니터링 대시보드</strong>: Grafana + PostgreSQL 메트릭스</li>
<li><strong>슬리피지/거래비용 정교화</strong>: 백테스트의 현실성 향상</li>
<li><strong>옵션/선물 헤징</strong>: 하락장 대비 자동 헤징 전략</li>
<li><strong>멀티 마켓 확장</strong>: 한국 주식, 일본 ETF 등</li>
</ol>
<hr>
<h2 id="마무리">마무리</h2>
<p>Claude Code로 하루 만에 백테스팅 프레임워크부터 실전 자동매매까지 구축했다. 핵심은:</p>
<ol>
<li><strong>전략이 먼저다</strong> — AI는 도구일 뿐, 검증된 전략 없이는 의미 없다</li>
<li><strong>백테스팅은 필수다</strong> — 과거에 안 되는 전략이 미래에 될 리 없다</li>
<li><strong>자동화가 핵심이다</strong> — 감정 개입 없이 룰 기반으로 매매</li>
</ol>
<p>다음 단계는 OpenClaw로 전략 발굴 자체를 자동화하는 것이다. AI가 매일 새로운 전략을 찾아서 검증하고, 유의미한 결과만 알려주는 시스템. 투자의 미래가 이런 방향이지 않을까.</p>
<hr>
<p><em>Built with Claude Code + OpenClaw</em>
<em>Tech Stack: Python 3.13, uv, PostgreSQL, Docker, APScheduler, KIS API, Upbit API, FRED API, Telegram Bot API</em></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[How to Deploy Langfuse (Self-Hosted) with a Custom Domain
]]></title>
            <link>https://velog.io/@leetusik_/How-to-Deploy-Langfuse-Self-Hosted-with-a-Custom-Domain</link>
            <guid>https://velog.io/@leetusik_/How-to-Deploy-Langfuse-Self-Hosted-with-a-Custom-Domain</guid>
            <pubDate>Mon, 02 Feb 2026 06:38:29 GMT</pubDate>
            <description><![CDATA[<p><strong>Method:</strong> Docker Compose + Cloudflare Tunnel (No Port Forwarding Required)<img src="https://velog.velcdn.com/images/leetusik_/post/f193aa3b-cd64-4971-bd4a-d0ce6b8f9e97/image.png" alt=""></p>
<h2 id="prerequisites">Prerequisites</h2>
<ol>
<li><strong>A Linux Server</strong> (Home server, VPS, or EC2) with Docker &amp; Docker Compose installed.</li>
<li><strong>A Domain Name</strong> (e.g., <code>yourdomain.com</code>).</li>
<li><strong>A Cloudflare Account</strong> (Free tier is sufficient).</li>
</ol>
<hr>
<h2 id="step-1-point-your-domain-to-cloudflare">Step 1: Point Your Domain to Cloudflare</h2>
<p><em>Skip this if your domain is already managed by Cloudflare.</em></p>
<ol>
<li>Log in to the <a href="https://dash.cloudflare.com">Cloudflare Dashboard</a> and click <strong>&quot;Domain registration&quot;</strong>.</li>
<li>Enter your root domain (e.g., <code>yourdomain.com</code>) and select the <strong>Free Plan</strong>.</li>
<li>Cloudflare will provide two <strong>Nameservers</strong> (e.g., <code>bob.ns.cloudflare.com</code>).</li>
<li>Log in to your domain registrar (Namecheap, GoDaddy, etc.).</li>
<li>Go to <strong>DNS Settings</strong> (or &quot;Nameservers&quot;) and change &quot;Default DNS&quot; to <strong>&quot;Custom DNS&quot;</strong>.</li>
<li>Paste the two Cloudflare nameservers and save.</li>
<li>Wait 15-30 minutes for Cloudflare to show the status as <strong>&quot;Active&quot;</strong>.</li>
</ol>
<hr>
<h2 id="step-2-set-up-cloudflare-tunnel-on-your-server">Step 2: Set Up Cloudflare Tunnel (On Your Server)</h2>
<p>This creates a secure link from your server to the internet without opening router ports.</p>
<h3 id="21-install-cloudflared">2.1 Install <code>cloudflared</code></h3>
<p>Run this on your Linux server:</p>
<pre><code class="language-bash"># Download and install the package
curl -L --output cloudflared.deb [https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb](https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb)
sudo dpkg -i cloudflared.deb</code></pre>
<h3 id="22-authenticate-and-create-tunnel">2.2 Authenticate and Create Tunnel</h3>
<pre><code class="language-bash"># Login (this gives you a URL to visit in your browser)
cloudflared tunnel login

# Create a new tunnel (Save the UUID output!)
cloudflared tunnel create langfuse-production

# Link your specific subdomain to the tunnel
# Replace &lt;UUID&gt; with your real ID from the previous command
cloudflared tunnel route dns &lt;UUID&gt; langfuse.yourdomain.com</code></pre>
<h3 id="23-move-credentials-to-system-folder">2.3 Move Credentials to System Folder</h3>
<p>The background service runs as root, so it needs the keys in <code>/etc/cloudflared</code>.</p>
<pre><code class="language-bash">sudo mkdir -p /etc/cloudflared
sudo cp ~/.cloudflared/cert.pem /etc/cloudflared/
sudo cp ~/.cloudflared/*.json /etc/cloudflared/</code></pre>
<h3 id="24-create-configuration-file">2.4 Create Configuration File</h3>
<p>Create the config file: <code>sudo nano /etc/cloudflared/config.yml</code>
<strong>Paste the following content (Update <code>&lt;UUID&gt;</code> and <code>hostname</code>):</strong></p>
<pre><code class="language-yaml">tunnel: &lt;UUID&gt;
credentials-file: /etc/cloudflared/&lt;UUID&gt;.json

ingress:
  - hostname: langfuse.yourdomain.com
    service: [http://127.0.0.1:3000](http://127.0.0.1:3000)
  - service: http_status:404</code></pre>
<h3 id="25-start-the-tunnel-service">2.5 Start the Tunnel Service</h3>
<pre><code class="language-bash"># Install the system service
sudo cloudflared service install

# Start and enable it
sudo systemctl start cloudflared
sudo systemctl enable cloudflared</code></pre>
<p><em>Verification:</em> Run <code>sudo systemctl status cloudflared</code>. It should be <code>active (running)</code>.</p>
<hr>
<h2 id="step-3-configure-langfuse-docker-compose">Step 3: Configure Langfuse (Docker Compose)</h2>
<p>You must update Langfuse to know its public URL, otherwise login will fail.</p>
<h3 id="31-edit-docker-composeyml">3.1 Edit <code>docker-compose.yml</code></h3>
<p>Open your file (<code>nano docker-compose.yml</code>) and update the <code>langfuse-web</code> service environment variables.</p>
<p><strong>Crucial Changes:</strong></p>
<ol>
<li><strong><code>NEXTAUTH_URL</code></strong>: Must match your https domain.</li>
<li><strong><code>NEXT_PUBLIC_LANGFUSE_BASEURL</code></strong>: Must match your https domain.</li>
<li><strong>Ports</strong>: Ensure it binds to <code>127.0.0.1</code> or <code>0.0.0.0</code>.</li>
</ol>
<p><strong>Example Snippet:</strong></p>
<pre><code class="language-yaml">  langfuse-web:
    image: docker.io/langfuse/langfuse:3
    restart: always
    depends_on: *langfuse-depends-on
    ports:
      - 3000:3000
    environment:
      &lt;&lt;: *langfuse-worker-env

      # --- NETWORK CONFIGURATION ---
      NEXTAUTH_URL: [https://langfuse.yourdomain.com](https://langfuse.yourdomain.com)
      NEXT_PUBLIC_LANGFUSE_BASEURL: [https://langfuse.yourdomain.com](https://langfuse.yourdomain.com)
      # -----------------------------

      NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:-mysecret}
      # ... (keep other existing variables) ...</code></pre>
<h3 id="32-apply-changes">3.2 Apply Changes</h3>
<pre><code class="language-bash">docker compose down
docker compose up -d</code></pre>
<hr>
<h2 id="step-4-final-cloudflare-settings">Step 4: Final Cloudflare Settings</h2>
<ol>
<li>Go to <strong>Cloudflare Dashboard &gt; SSL/TLS</strong>.</li>
<li>Set encryption mode to <strong>Full</strong>. (Do not use &quot;Flexible&quot;, it causes redirect loops).</li>
<li>Go to <strong>DNS</strong>. Ensure your <code>langfuse</code> CNAME record is <strong>Proxied</strong> (Orange Cloud icon).</li>
</ol>
<hr>
<h2 id="step-5-verify">Step 5: Verify</h2>
<p>Visit <strong><code>https://langfuse.yourdomain.com</code></strong>.</p>
<ul>
<li><strong>If it works on mobile but not desktop:</strong> It is a local DNS cache issue.<ul>
<li><strong>Fix:</strong> Change your computer&#39;s DNS to <code>1.1.1.1</code> or run <code>sudo killall -HUP mDNSResponder</code> (Mac) / <code>ipconfig /flushdns</code> (Windows).</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[폐쇄망에서 LLM 로드 및 호스팅하기]]></title>
            <link>https://velog.io/@leetusik_/%ED%8F%90%EC%87%84%EB%A7%9D%EC%97%90%EC%84%9C-LLM-%EB%A1%9C%EB%93%9C-%EB%B0%8F-%ED%98%B8%EC%8A%A4%ED%8C%85%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@leetusik_/%ED%8F%90%EC%87%84%EB%A7%9D%EC%97%90%EC%84%9C-LLM-%EB%A1%9C%EB%93%9C-%EB%B0%8F-%ED%98%B8%EC%8A%A4%ED%8C%85%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 26 Nov 2025 14:42:12 GMT</pubDate>
            <description><![CDATA[<p>openai, google이 제공하는 API 호출해서 써보기만 했던 LLM을, 폐쇄망 환경에서 <strong>VLLM</strong>과 <strong>llama.cpp</strong>을 각각 사용하여 직접 로드하고 호스팅해 보았다.</p>
<!--more-->

<h2 id="준비물">준비물</h2>
<p>폐쇄망에서는 단순히 LLM의 이름만으로 huggingface를 활용하여 모델을 불러오고, 관련 파이썬 패키지를 다운받는 것이 불가능하다. 그래서 아래의 준비물들이 필요하다.</p>
<ol>
<li>LLM 폴더(혹은 gguf 파일)</li>
<li>미리 다운받은 파이썬 패키지</li>
<li>실행스크립트</li>
</ol>
<h4 id="1-llm-폴더">1. LLM 폴더</h4>
<p>따라서 오프라인에서 LLM을 로드하기 위해서는 LLM 폴더 자체 혹은 LLM gguf 파일이 필요하다. LLM 폴더는 huggingface 아이디를 활용하여 git clone으로 받을 수 있고, gguf 파일도 마찬가지로 huggingface web 상에서 다운로드 받을 수 있다.</p>
<p><strong>모델 폴더 다운로드 예시</strong></p>
<pre><code class="language-bash">git clone https://huggingface.co/openai-community/gpt2 # root 폴더에 gpt2 모델 폴더 생성됨</code></pre>
<h4 id="2-파이썬-패키지">2. 파이썬 패키지</h4>
<p>파이썬 패키지도 폐쇄망이기 때문에 미리 다운로드를 받아야한다. <code>pip download</code> 를 활용하여 whl 파일로 저장해두고, LLM 로드 전 서버에 설치한다.
주의할 점은 아래의 패키지를 다운로드할 때 pip이 폐쇄망 내에서 사용할 파이썬 환경과 일치해야한다는 점이다.</p>
<p><strong>VLLM 활용 파이썬 패키지 다운로드 예시</strong></p>
<pre><code class="language-bash">mkdir python-packages # 다운로드 받을 폴더 생성

pip download vllm -d ./python-packages # vllm 의존성 패키지를 생성한 폴더에 다운로드. 휠파일 생성</code></pre>
<p><strong>llama.cpp 활용 파이썬 패키지 다운로드 예시</strong></p>
<pre><code class="language-bash">mkdir python-packages

# vllm과 다르게 별도로 설치해줘야하는 의존성들이 있다
pip download \
  &quot;llama-cpp-python[server]&quot; \
  scikit-build-core \
  cmake \
  ninja \
  pydantic-core \
  setuptools \
  wheel \
  -d ./python-packages</code></pre>
<h4 id="3-실행-스크립트">3. 실행 스크립트</h4>
<p>그리고 파이썬 패키지를 다운로드하고, <code>python -m vllm..</code>으로 시작하는 파이썬을 동작시킬 command line을 저장시킨 실행 스크립트가 있으면 좋다.</p>
<hr>
<hr>
<h2 id="vllm-활용-로드-gpu">VLLM 활용 로드 (GPU)</h2>
<p>VLLM은 GPU VRAM의 일정 부분을 할당하여 LLM을 로드하고, PagedAttention 알고리즘을 활용하여 메모리를 효율적으로 관리한다. 이를 통해 배치 처리를 최적화하고 더 많은 동시 요청을 처리할 수 있다.</p>
<h4 id="1-실행-스크립트-구성-vllm-ref">1. 실행 스크립트 구성 {#vllm-ref}</h4>
<p><a href="#vllm-sh">원본 코드는 아래에서 확인 가능합니다.</a></p>
<p>실행 스크립트는 다음의 항목을 포함해야한다.</p>
<ol>
<li>폐쇄망 환경 설정</li>
<li>파이썬 패키지 설치</li>
<li>python -m vllm.entrypoints.openai.api_server 실행</li>
</ol>
<h6 id="11-폐쇄망-환경-설정">1.1. 폐쇄망 환경 설정</h6>
<p>아래의 항목을 스크립트에 입력함으로써 huggingface가 인터넷에 접속하려는 시도를 막는다.
그리고 기타 환경변수를 설정해준다.</p>
<pre><code class="language-bash"># start.sh
# Get the directory where this script is located
SCRIPT_DIR=&quot;$(cd &quot;$(dirname &quot;${BASH_SOURCE[0]}&quot;)&quot; &amp;&amp; pwd)&quot;
export MODEL_PATH=&quot;${SCRIPT_DIR}/gemma-3-270m-it&quot;

export HF_DATASETS_OFFLINE=1
export TRANSFORMERS_OFFLINE=1
export HF_HUB_OFFLINE=1
export HF_DATASETS_CACHE=&quot;${SCRIPT_DIR}/.cache/huggingface/datasets&quot;
export HF_HOME=&quot;${SCRIPT_DIR}/.cache/huggingface&quot;
# ...</code></pre>
<h6 id="12-파이썬-패키지-설치">1.2. 파이썬 패키지 설치</h6>
<p>아래의 항목을 스크립트에 입력함으로써 vllm 구동을 위한 패키지를 폐쇄망에 설치한다.</p>
<pre><code class="language-bash"># start.sh
# ...
pip install --no-index --find-links &quot;${SCRIPT_DIR}/python-packages&quot; \
    vllm 

# ...</code></pre>
<h6 id="13-python-실행문">1.3. python... 실행문</h6>
<p>아래의 항목을 스크립트 마지막에 입력함으로써 llm로드와 호스팅을 진행한다.</p>
<pre><code class="language-bash"># start.sh
# ...
python -m vllm.entrypoints.openai.api_server \
    --host 0.0.0.0 \
    --port 8000 \
    --model &quot;${MODEL_PATH}&quot; \
    --gpu-memory-utilization 0.75 \
    --max-model-len 4096 \
    --trust-remote-code</code></pre>
<hr>
<hr>
<h2 id="llamacpp-활용-로드-cpu">llama.cpp 활용 로드 (CPU)</h2>
<p>llama.cpp는 GPU 없이 CPU만으로도 LLM을 실행할 수 있도록 최적화된 라이브러리이다. GGUF 포맷의 양자화된 모델을 사용하여 메모리 사용량을 줄이고 CPU에서도 합리적인 속도로 추론을 수행할 수 있다.</p>
<h4 id="1-실행-스크립트-구성-llama-cpp-ref">1. 실행 스크립트 구성 {#llama-cpp-ref}</h4>
<p><a href="#llama-cpp-sh">원본 코드는 아래에서 확인 가능합니다.</a></p>
<p>실행 스크립트는 다음의 항목을 포함해야한다.</p>
<ol>
<li>폐쇄망 환경 설정</li>
<li>파이썬 패키지 설치</li>
<li>python -m llama_cpp.server 실행</li>
</ol>
<h6 id="11-폐쇄망-환경-설정-1">1.1. 폐쇄망 환경 설정</h6>
<p>아래의 항목을 스크립트에 입력함으로써 모델 경로와 기타 환경변수를 설정해준다.</p>
<pre><code class="language-bash"># start.sh
# Get the directory where this script is located
SCRIPT_DIR=&quot;$(cd &quot;$(dirname &quot;${BASH_SOURCE[0]}&quot;)&quot; &amp;&amp; pwd)&quot;
export MODEL_PATH=&quot;${SCRIPT_DIR}/gemma-3-1b-it-q4_0.gguf&quot;
# ...</code></pre>
<h6 id="12-파이썬-패키지-설치-1">1.2. 파이썬 패키지 설치</h6>
<p>아래의 항목을 스크립트에 입력함으로써 llama-cpp-python 구동을 위한 패키지를 폐쇄망에 설치한다.</p>
<pre><code class="language-bash"># start.sh
# ...
pip install --no-index --find-links &quot;${SCRIPT_DIR}/python-packages&quot; \
    &quot;llama-cpp-python[server]&quot; 

# ...</code></pre>
<h6 id="13-python-실행문-1">1.3. python... 실행문</h6>
<p>아래의 항목을 스크립트 마지막에 입력함으로써 llm로드와 호스팅을 진행한다.</p>
<pre><code class="language-bash"># start.sh
# ...
python -m llama_cpp.server \
    --model &quot;${MODEL_PATH}&quot; \
    --host 0.0.0.0 \
    --port 8001 \
    --n_threads 4</code></pre>
<p><strong>주요 파라미터 설명:</strong></p>
<ul>
<li><code>--n_threads</code>: CPU 스레드 개수 (CPU 코어 수에 맞춰 조정)</li>
</ul>
<h2 id="테스트">테스트</h2>
<p>간단하게 curl로 테스트해볼 수 있다.</p>
<p><strong>VLLM 서버 테스트 (포트 8000)</strong></p>
<pre><code class="language-bash"># Health check
curl http://localhost:8000/health

# 모델 목록 조회
curl http://localhost:8000/v1/models

# Chat completion 테스트
curl -X POST http://localhost:8000/v1/chat/completions \
  -H &quot;Content-Type: application/json&quot; \
  -d &#39;{
    &quot;model&quot;: &quot;gemma-3-270m-it&quot;,
    &quot;messages&quot;: [{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;Hello!&quot;}],
    &quot;max_tokens&quot;: 100
  }&#39;</code></pre>
<p><strong>llama.cpp 서버 테스트 (포트 8001)</strong></p>
<pre><code class="language-bash"># Health check
curl http://localhost:8001/health

# Chat completion 테스트
curl -X POST http://localhost:8001/v1/chat/completions \
  -H &quot;Content-Type: application/json&quot; \
  -d &#39;{
    &quot;messages&quot;: [{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;Hello!&quot;}],
    &quot;max_tokens&quot;: 100
  }&#39;</code></pre>
<p>{{&lt; anchor id=&quot;test-ref&quot; &gt;}}혹은 <a href="#test-py">테스트를 위한 파이썬 파일</a>을 생성하여 아래와 같이 테스트해볼 수 있다.</p>
<pre><code class="language-bash"># VLLM 서버 테스트
python llm_test.py localhost 8000

# llama.cpp 서버 테스트
python llm_test.py localhost 8001</code></pre>
<h2 id="부록">부록</h2>
<p>uv가 설치되어있다는 전제로 uv를 활용해서 의존성 패키지를 설치했다.</p>
<pre><code class="language-bash">#!/bin/bash

# Exit on error
set -e

# Get the directory where this script is located
SCRIPT_DIR=&quot;$(cd &quot;$(dirname &quot;${BASH_SOURCE[0]}&quot;)&quot; &amp;&amp; pwd)&quot;

echo &quot;=========================================&quot;
echo &quot;LLM Load &amp; Host - vLLM (Closed Network)&quot;
echo &quot;=========================================&quot;
echo &quot;Script Directory: ${SCRIPT_DIR}&quot;
echo &quot;=========================================&quot;
echo &quot;&quot;

# Step 1: Configure environment for offline mode
echo &quot;[1/5] Configuring offline environment...&quot;
export HF_DATASETS_OFFLINE=1
export TRANSFORMERS_OFFLINE=1
export HF_HUB_OFFLINE=1
export HF_DATASETS_CACHE=&quot;${SCRIPT_DIR}/.cache/huggingface/datasets&quot;
export HF_HOME=&quot;${SCRIPT_DIR}/.cache/huggingface&quot;

# Configure UV for offline mode
export UV_NO_CACHE=1
export UV_OFFLINE=1
export UV_FIND_LINKS=&quot;${SCRIPT_DIR}/python-packages&quot;

# Point to local model directory
export MODEL_PATH=&quot;${SCRIPT_DIR}/gemma-3-270m-it&quot;

echo &quot;✓ Environment configured for offline mode&quot;
echo &quot;&quot;

# Step 2: Check if required directories exist
echo &quot;[2/5] Checking required directories...&quot;
if [ ! -d &quot;${MODEL_PATH}&quot; ]; then
    echo &quot;ERROR: Model directory not found at ${MODEL_PATH}&quot;
    exit 1
fi
echo &quot;✓ Model directory found: ${MODEL_PATH}&quot;

if [ ! -d &quot;${SCRIPT_DIR}/python-packages&quot; ]; then
    echo &quot;ERROR: Python packages directory not found at ${SCRIPT_DIR}/python-packages&quot;
    exit 1
fi
echo &quot;✓ Python packages directory found&quot;
echo &quot;&quot;

# Step 3: Check if UV is installed
echo &quot;[3/5] Checking UV installation...&quot;
if ! command -v uv &amp;&gt; /dev/null; then
    echo &quot;ERROR: UV is not installed. Please install UV first.&quot;
    exit 1
fi
echo &quot;✓ UV is installed: $(uv --version)&quot;
echo &quot;&quot;

# Step 4: Initialize UV project and install dependencies
echo &quot;[4/5] Setting up Python environment with UV...&quot;

# Check if .venv already exists
if [ -d &quot;${SCRIPT_DIR}/.venv&quot; ]; then
    echo &quot;Virtual environment already exists, skipping creation...&quot;
else
    echo &quot;Creating virtual environment...&quot;
    cd &quot;${SCRIPT_DIR}&quot;
    uv venv
fi

# Install dependencies from local packages (including vLLM)
echo &quot;Installing dependencies from local packages...&quot;
cd &quot;${SCRIPT_DIR}&quot;

# Install packages using uv with local packages directory
# Note: vLLM doesn&#39;t need accelerate, only torch, transformers, vllm and dependencies
uv pip install --no-index --find-links &quot;${SCRIPT_DIR}/python-packages&quot; \
    vllm 

echo &quot;✓ Dependencies installed from local packages&quot;
echo &quot;&quot;

# Step 5: Start the vLLM server
echo &quot;[5/5] Starting vLLM API server...&quot;
echo &quot;=========================================&quot;
echo &quot;Server Configuration:&quot;
echo &quot;  - Model: ${MODEL_PATH}&quot;
echo &quot;  - Host: 0.0.0.0&quot;
echo &quot;  - Port: 8000&quot;
echo &quot;  - GPU Memory Utilization: 0.75&quot;
echo &quot;  - Max Model Length: 4096&quot;
echo &quot;=========================================&quot;
echo &quot;Server will be accessible at:&quot;
echo &quot;  - Local: http://127.0.0.1:8000&quot;
echo &quot;  - Network: http://$(hostname -I | awk &#39;{print $1}&#39;):8000&quot;
echo &quot;  - API docs: http://$(hostname -I | awk &#39;{print $1}&#39;):8000/docs&quot;
echo &quot;  - OpenAI-compatible: http://$(hostname -I | awk &#39;{print $1}&#39;):8000/v1&quot;
echo &quot;=========================================&quot;
echo &quot;&quot;

# Run the vLLM server using UV
cd &quot;${SCRIPT_DIR}&quot;
uv run python -m vllm.entrypoints.openai.api_server \
    --host 0.0.0.0 \
    --port 8000 \
    --model &quot;${MODEL_PATH}&quot; \
    --gpu-memory-utilization 0.75 \
    --max-model-len 4096 \
    --trust-remote-code</code></pre>
<p>uv가 설치되어있다는 전제로 uv를 활용해서 의존성 패키지를 설치했다.</p>
<pre><code class="language-bash">#!/bin/bash

# Exit on error
set -e

# Get the directory where this script is located
SCRIPT_DIR=&quot;$(cd &quot;$(dirname &quot;${BASH_SOURCE[0]}&quot;)&quot; &amp;&amp; pwd)&quot;

echo &quot;=========================================&quot;
echo &quot;LLM Load &amp; Host - llama.cpp (Closed Network)&quot;
echo &quot;=========================================&quot;
echo &quot;Script Directory: ${SCRIPT_DIR}&quot;
echo &quot;=========================================&quot;
echo &quot;&quot;

# Step 1: Configure environment for offline mode
echo &quot;[1/5] Configuring offline environment...&quot;

# Configure UV for offline mode
export UV_NO_CACHE=1
export UV_OFFLINE=1
export UV_FIND_LINKS=&quot;${SCRIPT_DIR}/python-packages&quot;

# Point to local model file
export MODEL_PATH=&quot;${SCRIPT_DIR}/gemma-3-1b-it-q4_0.gguf&quot;

echo &quot;✓ Environment configured for offline mode&quot;
echo &quot;&quot;

# Step 2: Check if required files exist
echo &quot;[2/5] Checking required files...&quot;
if [ ! -f &quot;${MODEL_PATH}&quot; ]; then
    echo &quot;ERROR: Model file not found at ${MODEL_PATH}&quot;
    exit 1
fi
echo &quot;✓ Model file found: ${MODEL_PATH}&quot;

if [ ! -d &quot;${SCRIPT_DIR}/python-packages&quot; ]; then
    echo &quot;ERROR: Python packages directory not found at ${SCRIPT_DIR}/python-packages&quot;
    exit 1
fi
echo &quot;✓ Python packages directory found&quot;
echo &quot;&quot;

# Step 3: Check if UV is installed
echo &quot;[3/5] Checking UV installation...&quot;
if ! command -v uv &amp;&gt; /dev/null; then
    echo &quot;ERROR: UV is not installed. Please install UV first.&quot;
    exit 1
fi
echo &quot;✓ UV is installed: $(uv --version)&quot;
echo &quot;&quot;

# Step 4: Initialize UV project and install dependencies
echo &quot;[4/5] Setting up Python environment with UV...&quot;

# Check if .venv already exists
if [ -d &quot;${SCRIPT_DIR}/.venv&quot; ]; then
    echo &quot;Virtual environment already exists, skipping creation...&quot;
else
    echo &quot;Creating virtual environment...&quot;
    cd &quot;${SCRIPT_DIR}&quot;
    uv venv
fi

# Install dependencies from local packages
echo &quot;Installing dependencies from local packages...&quot;
cd &quot;${SCRIPT_DIR}&quot;

# Install llama-cpp-python with server support
uv pip install --no-index --find-links &quot;${SCRIPT_DIR}/python-packages&quot; \
    &quot;llama-cpp-python[server]&quot;

echo &quot;✓ Dependencies installed from local packages&quot;
echo &quot;&quot;

# Step 5: Start the llama.cpp server
echo &quot;[5/5] Starting llama.cpp API server...&quot;
echo &quot;=========================================&quot;
echo &quot;Server Configuration:&quot;
echo &quot;  - Model: ${MODEL_PATH}&quot;
echo &quot;  - Host: 0.0.0.0&quot;
echo &quot;  - Port: 8001&quot;
echo &quot;  - Threads: 4&quot;
echo &quot;=========================================&quot;
echo &quot;Server will be accessible at:&quot;
echo &quot;  - Local: http://127.0.0.1:8001&quot;
echo &quot;  - Network: http://$(hostname -I | awk &#39;{print $1}&#39;):8001&quot;
echo &quot;  - API docs: http://$(hostname -I | awk &#39;{print $1}&#39;):8001/docs&quot;
echo &quot;  - OpenAI-compatible: http://$(hostname -I | awk &#39;{print $1}&#39;):8001/v1&quot;
echo &quot;=========================================&quot;
echo &quot;&quot;

# Run the llama.cpp server using UV
cd &quot;${SCRIPT_DIR}&quot;
uv run python -m llama_cpp.server \
    --model &quot;${MODEL_PATH}&quot; \
    --host 0.0.0.0 \
    --port 8001 \
    --n_threads 4</code></pre>
<pre><code class="language-python">#!/usr/bin/env python3
&quot;&quot;&quot;
Test client for vLLM OpenAI-compatible API
Usage: python test_vllm_client.py &lt;server_ip&gt; [port]
Example: python test_vllm_client.py 192.168.1.100 8000
&quot;&quot;&quot;

import sys
import requests
import json


def test_vllm_api(server_ip: str, port: int = 8000):
    &quot;&quot;&quot;Test the vLLM OpenAI-compatible API endpoints.&quot;&quot;&quot;
    base_url = f&quot;http://{server_ip}:{port}&quot;
    api_url = f&quot;{base_url}/v1&quot;

    print(&quot;=&quot; * 60)
    print(f&quot;Testing vLLM API at {base_url}&quot;)
    print(&quot;=&quot; * 60)
    print()

    # Test 1: Health check
    print(&quot;[1/4] Testing health endpoint...&quot;)
    try:
        response = requests.get(f&quot;{base_url}/health&quot;, timeout=5)
        response.raise_for_status()
        print(&quot;✓ Health check passed&quot;)
        try:
            # Try to parse as JSON
            health_data = response.json()
            print(json.dumps(health_data, indent=2))
        except:
            # If not JSON, just print the text
            print(f&quot;Response: {response.text}&quot;)
    except Exception as e:
        print(f&quot;✗ Health check failed: {e}&quot;)
        return False
    print()

    # Test 2: List models
    print(&quot;[2/4] Testing models endpoint...&quot;)
    try:
        response = requests.get(f&quot;{api_url}/models&quot;, timeout=5)
        response.raise_for_status()
        print(&quot;✓ Models endpoint working&quot;)
        models = response.json()
        print(json.dumps(models, indent=2))

        # Get the model name for later use
        if models.get(&quot;data&quot;):
            model_name = models[&quot;data&quot;][0][&quot;id&quot;]
            print(f&quot;\nUsing model: {model_name}&quot;)
        else:
            print(&quot;Warning: No models found&quot;)
            model_name = None
    except Exception as e:
        print(f&quot;✗ Models endpoint failed: {e}&quot;)
        return False
    print()

    # Test 3: Chat completions (OpenAI-compatible)
    print(&quot;[3/4] Testing chat completions...&quot;)
    try:
        test_messages = [{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;Hello! How are you today?&quot;}]
        print(f&quot;Messages: {json.dumps(test_messages, indent=2)}&quot;)
        print(&quot;Generating... (this may take a moment)&quot;)

        response = requests.post(
            f&quot;{api_url}/chat/completions&quot;,
            json={
                &quot;model&quot;: model_name or &quot;gemma-3-270m-it&quot;,
                &quot;messages&quot;: test_messages,
                &quot;max_tokens&quot;: 100,
                &quot;temperature&quot;: 0.7,
            },
            timeout=60,
        )
        response.raise_for_status()

        result = response.json()
        print(&quot;✓ Chat completion successful&quot;)
        print()
        print(&quot;-&quot; * 60)
        print(&quot;Request:&quot;)
        print(json.dumps(test_messages, indent=2))
        print(&quot;-&quot; * 60)
        print(&quot;Response:&quot;)
        if &quot;choices&quot; in result and len(result[&quot;choices&quot;]) &gt; 0:
            message = result[&quot;choices&quot;][0][&quot;message&quot;][&quot;content&quot;]
            print(message)
            print(&quot;-&quot; * 60)
            print(&quot;Usage:&quot;, result.get(&quot;usage&quot;, {}))
        else:
            print(json.dumps(result, indent=2))
        print(&quot;-&quot; * 60)
    except Exception as e:
        print(f&quot;✗ Chat completion failed: {e}&quot;)
        return False
    print()

    # Test 4: Text completions (OpenAI-compatible)
    print(&quot;[4/4] Testing text completions...&quot;)
    try:
        test_prompt = &quot;Once upon a time&quot;
        print(f&quot;Prompt: {test_prompt}&quot;)
        print(&quot;Generating... (this may take a moment)&quot;)

        response = requests.post(
            f&quot;{api_url}/completions&quot;,
            json={
                &quot;model&quot;: model_name or &quot;gemma-3-270m-it&quot;,
                &quot;prompt&quot;: test_prompt,
                &quot;max_tokens&quot;: 50,
                &quot;temperature&quot;: 0.7,
            },
            timeout=60,
        )
        response.raise_for_status()

        result = response.json()
        print(&quot;✓ Text completion successful&quot;)
        print()
        print(&quot;-&quot; * 60)
        print(&quot;Prompt:&quot;, test_prompt)
        print(&quot;-&quot; * 60)
        print(&quot;Completion:&quot;)
        if &quot;choices&quot; in result and len(result[&quot;choices&quot;]) &gt; 0:
            text = result[&quot;choices&quot;][0][&quot;text&quot;]
            print(text)
            print(&quot;-&quot; * 60)
            print(&quot;Usage:&quot;, result.get(&quot;usage&quot;, {}))
        else:
            print(json.dumps(result, indent=2))
        print(&quot;-&quot; * 60)
    except Exception as e:
        print(f&quot;✗ Text completion failed: {e}&quot;)
        return False
    print()

    print(&quot;=&quot; * 60)
    print(&quot;✓ All tests passed!&quot;)
    print(&quot;=&quot; * 60)
    print()
    print(&quot;vLLM API is ready to use!&quot;)
    print()
    print(&quot;OpenAI-compatible endpoints:&quot;)
    print(f&quot;  - Chat: POST {api_url}/chat/completions&quot;)
    print(f&quot;  - Completions: POST {api_url}/completions&quot;)
    print(f&quot;  - Models: GET {api_url}/models&quot;)
    print()
    print(&quot;Interactive docs at:&quot;)
    print(f&quot;  {base_url}/docs&quot;)
    print()

    return True


def main():
    &quot;&quot;&quot;Main function.&quot;&quot;&quot;
    if len(sys.argv) &lt; 2:
        print(&quot;Usage: python test_vllm_client.py &lt;server_ip&gt; [port]&quot;)
        print(&quot;Example: python test_vllm_client.py 192.168.1.100 8000&quot;)
        sys.exit(1)

    server_ip = sys.argv[1]
    port = int(sys.argv[2]) if len(sys.argv) &gt; 2 else 8000

    try:
        success = test_vllm_api(server_ip, port)
        sys.exit(0 if success else 1)
    except KeyboardInterrupt:
        print(&quot;\nTest interrupted by user&quot;)
        sys.exit(1)


if __name__ == &quot;__main__&quot;:
    main()
</code></pre>
]]></description>
        </item>
    </channel>
</rss>