vllm 서버에서 gpt-oss 120B 모델을 띄우고, 이 endpoint url에 VSCode Cline Extension을 연결해서 사용하고 있어.

네, 상황을 정리하면 이렇게 되네요.

  1. vLLM 서버 에서 gpt-oss 120B 모델 을 띄움

  2. 그 모델의 endpoint URLVSCode Cline Extension 에 연결해서 사용 중

이 구성이 동작하려면 크게 세 단계가 필요합니다.


1. vLLM 서버에서 gpt-oss 120B 모델 실행

  • vLLM 설치
    pip install vllm
    
  • 모델 실행 예시 (OpenAI API 호환 모드)
    python -m vllm.entrypoints.openai.api_server \
      --model gpt-oss-120b \
      --tensor-parallel-size 4 \
      --port 8000
    
    • --model : Hugging Face 모델 경로 또는 로컬 경로

    • --tensor-parallel-size : GPU 개수

    • --port : API 서버 포트

이렇게 하면 http://<서버IP>:8000/v1 엔드포인트가 생깁니다.


2. VSCode Cline Extension 설정

Cline은 OpenAI API 형식을 따르기 때문에, vLLM 서버가 OpenAI 호환 모드로 띄워져 있어야 합니다.

  • VSCode → Cline Extension Settings"API Provider"OpenAI로 선택

  • API Base URL 을 vLLM 서버 주소로 설정:
    arduino
        
    http://<서버IP>:8000/v1
    
  • API Key 는 vLLM 서버에서 인증을 사용하지 않으면 아무 값이나 넣어도 됩니다.
    (예: dummy-key)

3. 연결 확인

터미널에서 다음 명령으로 정상 응답을 확인:

curl http://<서버IP>:8000/v1/models

여기서 gpt-oss-120b가 뜨면 Cline에서도 바로 사용할 수 있습니다.


💡

  • 로컬 개발 환경에서 GPU 부족 시, vLLM 대신 Hugging Face Text Generation Inference(TGI)도 OpenAI API 호환 모드로 쓸 수 있습니다.

  • Cline이 gpt-oss 120B 같이 초대형 모델을 호출하면 응답이 느릴 수 있으니, 프롬프트를 최대한 압축하거나 서버 쪽에서 --max-num-batched-tokens를 조정하는 게 좋습니다.


원하시면 제가 vLLM gpt-oss 120B + Cline 연결 전체 예제 설정 파일 을 만들어서 드릴 수 있습니다.
이렇게 하면 서버 띄우고 바로 Cline에 붙일 수 있게 됩니다.

그런데 관련해서 디버깅을 좀 해보고 싶어. Cline이 서버로 보내는 쿼리와 받는 정보를 raw data에 가깝게 볼 수 있을까?

가능해요. “최대한 원본(raw)에 가깝게” 보려면 프록시를 사이에 둬서 요청/응답을 그대로 덤프 하는 게 가장 깔끔합니다. 아래를 순서대로 해보세요. (짧게 → 깊게)


0) 한줄 요약

  • 권장: Cline ⇄ mitmproxy(reverse proxy) ⇄ vLLM

  • 보너스: VSCode 로그(Extension Host/Window), 서버측(access log), 마지막 수단으로 ngrep/tcpdump


1) mitmproxy로 전체 HTTP 캡처(권장)

Cline이 호출하는 OpenAI 호환 REST 요청을 바이트 단위로 시/헤더/바디/스트리밍까지 볼 수 있습니다.

1-1. mitmproxy 설치

pip install mitmproxy
# 또는 macOS: brew install mitmproxy

1-2. vLLM 뒤에 리버스 프록시 세우기

vLLM이 http://<VLLM_HOST>:8000/v1 라고 가정:

mitmproxy -p 8080 --mode reverse:http://<VLLM_HOST>:8000 -w cline_flows.mitm
# GUI 원하면:
# mitmweb -p 8080 --mode reverse:http://<VLLM_HOST>:8000 -w cline_flows.mitm

1-3. Cline의 Base URL을 mitmproxy로 바꾸기

Cline의 API Base URL 을:

http://localhost:8080/v1

로 설정합니다. (API Key는 dummy 가능)

이제 mitm 화면에서 각 요청/응답을 클릭해 Headers / Content / Raw 탭으로 모두 확인 가능.
SSE(스트리밍)도 “Raw”에서 줄 단위로 보입니다.

1-4. 본문까지 파일로 떨구고 싶다면(옵션)

mitmdump -p 8080 --mode reverse:http://<VLLM_HOST>:8000 \
  -s '"""
from mitmproxy import http
def response(flow: http.HTTPFlow):
    with open("http_log.txt","ab") as f:
        f.write((f"\\n=== {flow.request.method} {flow.request.pretty_url} ===\\n").encode())
        f.write(flow.request.headers.bytes() + b"\\n\\n" + flow.request.raw_content + b"\\n")
        f.write(b"--- RESPONSE ---\\n")
        f.write(flow.response.headers.bytes() + b"\\n\\n" + (flow.response.raw_content or b"") + b"\\n")
"""'

주의: 본문에 토큰/프롬프트 등 민감정보가 포함됩니다. 로컬에서만 사용!


2) 초간단 “로깅 프록시” 직접 띄우기(대체안)

외부 도구 없이, 작은 Node 프록시 를 넣어도 됩니다.

# server.js
const http = require('http');
const { request: httpRequest } = require('http');

const TARGET = 'http://<VLLM_HOST>:8000';

http.createServer((clientReq, clientRes) => {
  let body = [];
  clientReq.on('data', chunk => body.push(chunk));
  clientReq.on('end', () => {
    body = Buffer.concat(body);
    console.log('\n=== REQUEST ===');
    console.log(clientReq.method, clientReq.url);
    console.log(clientReq.headers);
    console.log(body.toString());

    const opts = {
      method: clientReq.method,
      headers: clientReq.headers,
      path: clientReq.url,
      host: new URL(TARGET).hostname,
      port: new URL(TARGET).port,
    };

    const proxyReq = httpRequest(opts, proxyRes => {
      let resBody = [];
      proxyRes.on('data', chunk => {
        resBody.push(chunk);
        clientRes.write(chunk); // 스트리밍 패스스루
      });
      proxyRes.on('end', () => {
        resBody = Buffer.concat(resBody);
        console.log('\n--- RESPONSE ---');
        console.log(proxyRes.statusCode, proxyRes.headers);
        console.log(resBody.toString());
        clientRes.end();
      });
    });

    proxyReq.on('error', e => {
      console.error('proxy error', e);
      clientRes.statusCode = 502;
      clientRes.end('proxy error');
    });

    proxyReq.write(body);
    proxyReq.end();
  });
}).listen(8080, () => console.log('logging proxy on :8080'));
node server.js
# 그리고 Cline Base URL을 http://localhost:8080/v1 로

3) VSCode 쪽에서 볼 수 있는 로그

  1. Output 패널

    • VSCode: View → Output → 드롭다운에서

      • “Log (Extension Host)” , “Log (Window)” , 혹시 있다면 “Cline” 채널 확인
  2. 로그 레벨 올리기

    • Settings에서 “Log Level” → “Trace”로 설정
  3. Developer Tools

    • Help → Toggle Developer Tools → Console 탭

    • 일부 네트워크/콘솔 로그가 찍힙니다(확장 작동 방식에 따라 차이)

확장마다 남기는 로그 범위가 달라 실제 요청/응답 바디 전체 는 안 나올 수 있어요. 그래서 위의 프록시 접근이 가장 확실 합니다.


4) 서버(vLLM) 측 로그를 좀 더 보기

  • vLLM OpenAI 서버는 Uvicorn 위에서 돌아가므로 access log 는 기본적으로 나옵니다.

  • 더 자세히 보려면(바디는 기본 미포함):

    python -m vllm.entrypoints.openai.api_server \
      --model gpt-oss-120b \
      --port 8000 \
      --log-level debug
    

이건 라우트/상태 정도만 보이고, 요청/응답 바디는 노출되지 않습니다.
원본 바디가 필요하면 1번(프록시)로 가는 게 정석.


5) 네트워크 레벨에서 “그냥 까보기”(HTTP일 때만)

서버와 Cline 사이가 평문 HTTP 라면, 패킷 레벨로도 확인 가능합니다.

  • ngrep (가독성 좋음)
    sudo ngrep -d any -W byline port 8000
    
  • tcpdump (바이너리/헤더까지)
    sudo tcpdump -A -s0 'tcp port 8000'
    

HTTPS라면 내용은 해독이 안 되니 프록시가 필요합니다.


6) 재현용: curl로 같은 요청 만들어보기

Cline이 보내는 페이로드를 캡처했으면, 동일한 요청curl로 반복 재현하면서 비교 디버깅하기 좋습니다.

curl -v http://localhost:8080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer dummy" \
  -d '{
    "model": "gpt-oss-120b",
    "messages": [{"role":"user","content":"hello"}],
    "stream": true
  }'
  • -v/--trace-ascii dump.txtwire-level 로그 확보 가능

어떤 방법이 제일 나을까?

  • 가장 확실하고 빠른 해법은 1) mitmproxy reverse 입니다. UI로 한눈에 보이고, 파일로도 저장 가능.

  • VSCode 로그는 “보조”.

  • 서버 로그/패킷 스니핑은 상황 따라 도움.

원하시면, 현재 환경(IP/포트/HTTPS 여부) 알려주면 제가 딱 맞춘 명령어와 Cline 설정 스크린샷 가이드 까지 만들어드릴게요.

API 키 입력이 필요하면?

Cline이 API 키를 반드시 요구하는 경우라도, vLLM 서버가 인증을 쓰지 않는다면 사실상 아무 문자열이나 넣으면 됩니다.
왜냐면 vLLM의 OpenAI 호환 API는 기본적으로 API 키 검증을 하지 않기 때문이에요.


1. 인증 없는 vLLM 서버일 때

  • Cline의 API Key 입력칸에 dummy 또는 test처럼 아무 값 입력

  • mitmproxy/프록시/서버 모두 그대로 통과합니다.

  • 요청 헤더 예시:

    Authorization: Bearer dummy
    

2. vLLM에서 API 키 검증을 켠 경우

만약 --api-key 같은 인증 옵션을 활성화했다면(또는 API Gateway 뒤에 둔 경우):

  1. 서버에서 설정한 키 를 기억하거나 환경변수로 확인
    export VLLM_API_KEY="my-secret-key"
    
  2. Cline API Key 입력란에 해당 키를 입력

  3. 프록시나 mitmproxy로 캡처 시에도 이 키가 그대로 Authorization 헤더에 뜹니다.

3. 디버깅 프록시에서 키를 안전하게 처리하는 팁

  • 요청 로깅 시 Authorization 헤더를 마스킹 하면 좋습니다.
    # mitmproxy 스크립트 예시
    from mitmproxy import http
    def request(flow: http.HTTPFlow):
        if "authorization" in flow.request.headers:
            flow.request.headers["authorization"] = "Bearer ***MASKED***"
    
  • 내부 개발/로컬 환경이면 그냥 그대로 봐도 무방하지만, 키가 외부로 유출되면 안 됩니다.

📌 결론

  • 인증 없으면 Cline에 임의 문자열 넣으면 끝.

  • 인증 있으면 vLLM 서버에 설정한 키를 넣어야 함.

  • 디버깅 시엔 프록시에서 키를 가려주는 습관을 들이면 안전합니다.


필요하다면 제가 API 키 유무별 Cline–vLLM–mitmproxy 연결 전체 그림 을 그려서, 어떤 값이 어디로 흘러가는지 보여드릴 수 있습니다.
그렇게 하면 디버깅 흐름이 한눈에 보입니다.

했는데 API Streaming Failed Unexpected token 173781 while expecting start token 200006 에러가 떠 cline에서

이 증상, 거의 100% 스트리밍 파서/포맷 불일치 예요.
특히 메시지에 나온 start token 200006는 OpenAI o-series(=gpt-4o 계열) 토크나이저의 특수 토큰 ID라서, Cline이 o-series/Harmony 포맷을 기대 하는데 vLLM의 일반 Chat Completions 스트림 을 받는 경우에 잘 터집니다. docs.cline.botVLLM Docs

아래 순서대로 체크해보세요. (위에서부터 한 단계씩)


1) Cline가 Responses API 가 아니라 Chat Completions 를 때리게 만들기

  • Cline 설정 → API Provider: OpenAI Compatible

  • Base URL : http://<서버IP>:8000/v1

  • Model ID : vLLM에 띄운 정확한 이름(예: gpt-oss-120b)

  • 가능하면 Responses API(“/v1/responses”) 옵션/모드 꺼두기 → vLLM은 공식적으로 /v1/chat/completions 스트림을 지원합니다. VLLM Docs

왜? vLLM의 OpenAI 호환 서버는 Completions/Chat/Embeddings까지만 구현되어 있고, OpenAI의 최신 Responses/Harmony(o-series) 이벤트 스트림과는 필드/이벤트명이 달라서 Cline 파서가 못 알아듣습니다. VLLM Docs


2) Cline에서 “o-series/Reasoning/Computer Use/병렬 툴콜” 꺼두기

  • 모델을 o1/o3/gpt-4o 류가 아닌 일반 채팅형 으로 고정

  • Tool/Computer Use 기능을 일단 OFF (vLLM의 함수/툴콜 구현과 Cline의 기대치가 어긋날 수 있음)


3) 스트리밍 자체가 문제인지 분리

  • Cline 설정에서 Stream 비활성화 가 가능하면 잠시 꺼서 테스트

  • 혹은 동일 요청을 curl로 재현 해 보세요:

    # 스트림 OFF로 테스트(파서 우회)
    curl http://<서버IP>:8000/v1/chat/completions \
      -H "Content-Type: application/json" \
      -H "Authorization: Bearer dummy" \
      -d '{
        "model":"gpt-oss-120b",
        "messages":[{"role":"user","content":"ping"}],
        "stream": false
      }'
    

이건 잘 되는데 Cline에서만 터지면 Cline의 SSE 파서/옵션 불일치 로 확정.


4) mitmproxy로 실제 스트림 모양 확인

  • Cline ↔ vLLM 사이에 mitmproxy(reverse)를 두고 SSE 라인 을 봅니다.
    data: { ... }가 JSON 청크로 오다가 마지막에 data: [DONE]으로 끝나야 Cline이 행복합니다. 만약 Responses API 이벤트명(예: event: response.delta) 비슷한 게 보이면 라우팅이 잘못된 겁니다. VLLM Docs

5) vLLM 서버 쪽 권장 실행 플래그

python -m vllm.entrypoints.openai.api_server \
  --model gpt-oss-120b \
  --host 0.0.0.0 --port 8000 \
  --tensor-parallel-size <GPU수> \
  --enable-log-requests  #(문제시 요청 로깅 보조)
  • 반드시 Cline이 /v1/chat/completions 로 치는지 확인하세요. (mitmproxy/curl)

6) 흔한 함정 정리

  • Base URL 끝에 /v1 빠짐 → 404/파서오류 유발

  • Model ID 오타 → 일부 게이트웨이는 다른 엔드포인트로 우회 응답

  • JSON 모드/Response Format 강제(예: json_schema) → 일부 모델에서 스트림 형식이 달라져 파서 에러

  • HTTP 압축/프록시 변환 이 SSE 줄바꿈을 깨뜨리는 경우


왜 200006이 보였나?

o-series(Harmony) 포맷에서 쓰는 특수 토큰 ID들이 있고, 글 중에도 200006 같은 ID가 언급됩니다. Cline이 그 포맷(=특정 토큰 경계/이벤트)을 기대 했는데, vLLM이 일반 Chat 스트림 을 보내니 “기대하던 시작 토큰이 아니다” 류의 에러가 난 것으로 추정됩니다. Simon Willison’s Weblog


필요하면 지금 Cline 설정 스크린샷(모델/엔드포인트/토글 항목)과 mitmproxy 캡처 한두 건만 올려 주세요.
그대로 보고 “어느 지점에서 Responses↔Chat이 엇갈리는지” 정확히 짚어드릴게요.

근데 mitmproxy를 안쓰면 저 에러가 안나

오—그럼 원인 범위가 mitmproxy 쪽 으로 꽤 좁혀졌어요.
SSE(서버센트 이벤트) 스트림을 mitmproxy가 버퍼링/재인코딩/HTTP2 변환 하면서 Cline의 파서가 기대하는 포맷이 깨지는 전형적인 케이스 같습니다.

아래 순서대로 “한 가지씩” 바꿔 보세요. (위에서부터 효과 큰 것 ▶ 세부)


1) mitmproxy 실행 옵션을 스트리밍 친화적으로 바꾸기

SSE는 라인 경계(줄바꿈)와 chunking이 아주 민감해요. 다음 옵션이 핵심입니다.

mitmproxy \
  --mode reverse:http://<VLLM_HOST>:8000 \
  -p 8080 \
  --set http2=false \
  --set connection_strategy=lazy \
  --set stream_large_bodies=1
  • http2=false : 업스트림/다운스트림에서 HTTP/2로 변환되는 걸 막아서 SSE 라인 경계 깨짐 을 예방

  • connection_strategy=lazy : 불필요한 선연결/재사용 줄임

  • stream_large_bodies=1 : 응답을 버퍼링하지 말고 바로바로 흘려보냄 (SSE 필수)

Cline Base URL은 http://localhost:8080/v1 로 유지


2) “바디 찍는” mitm 스크립트는 일단 끄기

제가 전에 드린 예시처럼 flow.response.raw_content를 파일로 쓰면 mitm이 본문을 수집하려고 버퍼링 할 수 있어요.
스크립트 없이 UI에서 Raw 탭으로 먼저 확인해 보세요.
(로그를 꼭 남겨야 한다면, responseheaders 이벤트에서 헤더만 찍고 바디는 건드리지 않는 방식으로…)

# headers만 로깅(버퍼링 방지)
from mitmproxy import http
def responseheaders(flow: http.HTTPFlow):
    print("\n=== RESPONSE HEADERS ===")
    print(flow.response.status_code, dict(flow.response.headers))
    # 스트리밍 강제
    flow.response.stream = True

3) gzip/인코딩 간섭 줄이기

가능하면 Cline 쪽에서 Accept-Encoding: identity 를 강제하거나, vLLM이 text/event-stream에 대해 압축을 안 하도록(기본적으로는 안 함) 둡니다.
압축이 끼면 중간 프록시가 재압축/해제 하면서 줄 경계가 어긋날 수 있어요.


4) 프록시 경로/중복 prefix 확인

reverse 모드에서 Base URL이/v1 인데, upstream에도 /v1가 붙은 주소로 중복되면 라우팅 꼬입니다.

  • mitmproxy: --mode reverse:http://<VLLM_HOST>:8000

  • Cline Base URL: http://localhost:8080/v1

  • Upstream vLLM 라우트는/v1/chat/completions 로 딱 한 번만 들어가야 합니다.


5) 재현 테스트 (직접 curl로 Cline 대신 쏘기)

mitm를 끼웠을 때만 깨지는지, 그리고 옵션 수정 후 복구됐는지 빠르게 확인:

# Stream 켠 채로 mitm 경유
curl -N -v http://localhost:8080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer dummy" \
  -d '{ "model":"gpt-oss-120b",
        "messages":[{"role":"user","content":"ping"}],
        "stream": true }'

정상이라면 이런 패턴으로 흘러와야 합니다:

data: {"id":"...","object":"chat.completion.chunk", ...}
data: {"id":"...","object":"chat.completion.chunk", ...}
...
data: [DONE]

6) 그래도 안 되면: 대안 2가지

  • HTTP/1.1 고정 프록시(tinyproxy/nginx/HAProxy)로 단순 패스스루만 하게 하기

    • nginx 예: proxy_http_version 1.1; proxy_set_header Connection ""; proxy_buffering off;
  • ngrep/tcpdump 로 패킷 레벨 캡처(프록시 없이도 raw 확인 가능)


7) 체크리스트(한 번에 훑기)

  • mitm http2=false, stream_large_bodies=1 적용

  • 커스텀 mitm 스크립트에서 본문 접근 제거 (헤더만 로깅)

  • Base URL 경로 중복 없음(/v1 한 번만)

  • SSE 응답 헤더 확인: Content-Type: text/event-stream, Cache-Control: no-cache

  • gzip/deflate 비활성화(가능하다면)

  • Cline이 /v1/responses 가 아닌 /v1/chat/completions 로 호출 중


원하시면 지금 쓰신 mitm 실행 커맨드랑(옵션 포함), Cline Base URL/모델 설정을 알려주세요.
그대로 맞춰서 오류 재현 → 최소 수정 세트 를 딱 뽑아드릴게요.

http2=false 옵션 하나만 추가하니 잘 돼네

아, 그럼 원인이 거의 확정이네요 — mitmproxy가 기본으로 HTTP/2로 업스트림 연결 을 시도하면서 SSE(서버센트 이벤트) 스트림의 줄 경계나 전송 방식이 미묘하게 변형돼서, Cline 파서가 깨졌던 겁니다.


http2=false로 해결됐나

  • SSE는 HTTP/1.1의 chunked transfer 에 의존하는 경우가 많습니다.

  • mitmproxy가 HTTP/2로 업스트림을 붙으면, 내부에서 프레임 단위로 전송 → 중간에 라인 경계(\n\n)가 바뀌거나 버퍼링 타이밍이 달라짐.

  • 특히 Cline 같이 실시간 토큰 단위 파서를 쓰는 쪽은 이 변화에 민감해서, "Unexpected token" 같은 오류를 뿜습니다.

  • --set http2=false는 mitmproxy가 업/다운 스트림 모두 HTTP/1.1로 고정 하게 해서 원본 chunking/SSE 형식을 보존하게 만듭니다.


안전하게 쓰는 mitmproxy SSE 프록시 실행 예시

mitmproxy \
  --mode reverse:http://<VLLM_HOST>:8000 \
  -p 8080 \
  --set http2=false \
  --set connection_strategy=lazy \
  --set stream_large_bodies=1
  • connection_strategy=lazy → 불필요한 선연결 방지

  • stream_large_bodies=1 → 응답 버퍼링 없이 바로 흘려보냄


앞으로 비슷한 상황 방지 팁

  1. SSE 프록시에는 HTTP/2 강제 비활성

  2. 본문 로깅이 필요하면 stream 모드에서 헤더만 찍고 바디는 건드리지 않기

  3. gzip/deflate 같은 압축은 가급적 꺼두기 → 중간 해제 시 줄 경계 깨짐 방지

  4. 프록시로 테스트한 curl 요청이 정상 스트리밍이면, Cline에서도 99% 잘 동작


이제 http2=false로 고정한 상태에서 디버깅 로그 수집도 문제없이 가능할 거예요.
원하시면 제가 Cline↔mitmproxy↔vLLM 흐름도 그려서, 데이터가 어떻게 흘러가고 어디서 줄 경계가 깨지는지 시각적으로 보여드릴 수도 있습니다.

Categories:

Updated: