Portway
언어 한국어

코딩 에이전트의 네트워크 전송량을 줄이세요.

Claude Code, Codex 같은 코딩 에이전트는 턴마다 그동안 쌓인 대화를 통째로 다시 보냅니다. Portway는 직전 요청을 기준으로 다음 요청을 압축해, 달라진 부분만 수신기까지 보냅니다. 수신기는 원래 요청을 복원해 모델에 전달합니다.

brew install rath/tap/portway

macOS Apple silicon · Linux x86_64 / ARM64

에이전트 세션 하나를 그대로 재생했습니다. 실제 바이너리로 40턴을 돌렸습니다. 컨텍스트는 256 KiB에서 시작해 턴마다 4 KiB씩 늘어납니다.
에이전트가 보낸 양
13,877,261 B
zstd만 썼을 때
3,114,150 B
실제로 전송된 양
101,551 B

40턴: 에이전트가 429,900바이트를 보냈고, 실제로 전송된 것은 1,029바이트입니다.

바이트가 줄어드는 걸 화면으로 확인하세요

Portway에는 터미널 대시보드(portway --tui)와 브라우저 콘솔(--web)이 들어 있습니다. 요청이 나갈 때마다 에이전트가 보낸 크기, 실제로 전송된 크기, 걸린 시간과 토큰 수를 보여 줍니다. 아래 화면은 실제 Claude Code와 Codex 세션에서 캡처한 것입니다.

브라우저 콘솔: 요청 41건, 61.8 MB(91%) 업로드 절약, 사용 중인 업스트림 표와 턴별 요청 크기 차트

설치하고 에이전트를 연결하세요

Homebrew 패키지에는 터미널 대시보드와 브라우저 콘솔까지 들어 있어 Rust 툴체인이 필요 없습니다.

설정을 도와주는 안내가 필요하다면 Codex·Claude Code용 Portway 플러그인을 사용할 수 있습니다. 클라이언트 설정과 연결 확인을 도와줍니다. Homebrew나 릴리스 다운로드로 Portway 0.2.5 이상을 설치하거나 업데이트하면 되며, 소스 빌드는 필요 없습니다. 플러그인은 setup과 doctor 스킬만 추가할 뿐, 트래픽 경로를 바꾸지 않습니다. 설정을 마친 뒤 Codex는 codex -p portway로 실행할 때만 Portway를 거치고, Claude Code는 setup이 사용자 설정에 기록하므로 새로 여는 모든 세션이 Portway를 거칩니다.

  1. Portway를 설치합니다. macOS Apple silicon과 Linux x86_64 / ARM64(glibc 2.28 이상)를 지원합니다.

    brew install rath/tap/portway

    Homebrew를 쓰지 않는다면 릴리스 바이너리를 받거나 소스에서 빌드할 수 있습니다. Rust와 C 툴체인은 소스 빌드에만 필요합니다.

  2. API 마운트 설정을 portway.toml로 저장합니다. 그런 다음 브라우저 콘솔과 함께 실행합니다:

    # portway.toml
    host = "127.0.0.1"
    port = 8787
    
    [upstreams]
    anthropic = "https://api.anthropic.com"
    codex = "https://chatgpt.com/backend-api/codex"
    portway --config ./portway.toml --web
  3. 두 번째 터미널에서 Claude Code를 연결합니다. 먼저 Claude Code를 설치하고 평소처럼 로그인합니다. Portway를 실행해 둔 채 다음 명령으로 이번 실행에 적용합니다:

    env ANTHROPIC_BASE_URL=http://127.0.0.1:8787/anthropic claude
  4. 별도 프로필로 Codex를 연결합니다. 먼저 Codex를 설치하고 codex login으로 ChatGPT에 로그인합니다. 다음 내용을 ~/.codex/portway.config.toml로 저장합니다. CODEX_HOME을 지정했다면 그 디렉터리에 저장합니다:

    model_provider = "portway"
    
    [model_providers.portway]
    name = "portway"
    base_url = "http://127.0.0.1:8787/codex"
    requires_openai_auth = true
    wire_api = "responses"

    그런 다음 프로젝트 디렉터리에서 실행합니다:

    codex -p portway

    프로필은 평소 모델과 환경설정을 상속합니다. 기존 프로바이더로 실행하려면 codex만 입력합니다. 두 CLI 모두 기존 인증 정보를 사용하며 Portway 전용 API 키는 필요 없습니다. 두 마운트 URL 뒤에 /v1을 붙이지 마세요.

  5. 첫 요청을 확인합니다. CLI에서 짧은 메시지를 보내 응답을 받고, Portway 로그나 브라우저 콘솔에 요청이 표시되는지 확인합니다. 공급자에 직접 연결하는 이 설정에는 델타 압축이 적용되지 않습니다. 이미 원격 Portway 서버를 사용한다면 클라이언트 설정의 127.0.0.1:8787을 접속 가능한 서버 주소로 바꾸세요.

    영구 설정·API 키 방식·문제 해결 안내

  6. 델타 압축을 사용하려면 수신기를 추가합니다. 서비스 앞의 게이트웨이 뒤에서 portway receive를 돌린 다음, 송신기가 그 게이트웨이를 바라보게 설정합니다. 수신기 설정에 과정이 나와 있습니다.

같은 저장소에는 임베딩할 수 있는 Rust 라이브러리 portway-core와, 데몬 모드·리포트·터미널 대시보드·브라우저 콘솔을 갖춘 CLI가 함께 들어 있습니다. 요청 메타데이터만 SQLite에 기록하고, 본문과 자격 증명, 사전 내용은 기록하지 않습니다.

40턴은 39턴 더하기 4킬로바이트

코딩 에이전트는 서버에 아무 상태도 남기지 않습니다. 한 단계 더 나아가려면 시스템 프롬프트와 앞서 오간 모든 메시지, 모든 도구 실행 결과를 다시 보내고, 그 뒤에 새로 생긴 몇 킬로바이트를 덧붙입니다. 그러니 각 요청은 직전 요청을 거의 그대로 복사한 것입니다.

요청이 실제로 나르는 양을 비율 그대로 그렸습니다. 밝은 끝이 앞선 어느 요청에도 없던 부분입니다.
  1. 1턴263,966 B
  2. 2턴268,145 B
  3. 3턴272,556 B
  4. 20턴344,723 B
  5. 39턴425,707 B
  6. 40턴429,900 B

그래서 세션의 업로드량은 길이의 제곱에 비례해 늘어납니다. 업로드가 곧 대기 시간이고 비용인 환경에서는 금세 체감됩니다:

짧은 단발 요청에서는 얻을 게 거의 없습니다. 기준 삼을 앞선 요청이 없기 때문입니다.

직전 턴이 사전이 됩니다

Portway는 두 곳에서 실행됩니다. 에이전트 옆의 송신기와, 에이전트가 호출하는 서비스 앞의 수신기입니다. 양쪽 모두 마지막으로 합의한 요청 본문을 보관합니다. 송신기는 그 본문을 기준으로 다음 요청을 압축하고, 수신기는 서비스가 바이트 하나라도 보기 전에 원본을 그대로 복원합니다.

  1. 에이전트Claude Code, Codex, base URL을 바꿀 수 있는 클라이언트라면 무엇이든
  2. portway송신기, 127.0.0.1:8787
  3. portway receive직접 관리하는 호스트
  4. 서비스모델 서버, 게이트웨이 또는 프로바이더 API
위 세션의 2턴입니다. 모델이 받는 바이트는 에이전트가 보낸 것과 똑같습니다. 델타를 나르는 것은 가운데 구간뿐입니다.

양 끝이 기준을 맞추는 방법

  1. 1턴이 사전의 바탕이 됩니다. 이 요청은 zstd로 압축해 X-Dict-Store: 1과 함께 보냅니다. 수신기는 압축을 풀어 넘겨주고, 본문은 보관합니다.

  2. 수신기는 무엇을 보관했는지 알려 줍니다. 응답에는 실제로 저장한 바이트의 SHA-256을 담은 X-Dict-Stored가 붙습니다. 송신기는 그 해시가 자기 사본과 일치할 때만 그 본문을 기준으로 삼습니다.

  3. 2턴은 해시로 1턴을 가리킵니다. 요청은 DCZ 형식으로 나갑니다. 사전의 SHA-256을 담은 40바이트 헤더 뒤에, 그 사전을 기준으로 압축되고 체크섬으로 끝나는 zstd 프레임이 이어집니다. 송신기는 라우트마다 확인된 본문을 최대 여덟 개까지 보관하고, 새 요청과 접두사를 가장 길게 공유하는 본문을 고릅니다. 뒤에 덧붙이기만 하는 대화라면 그것이 직전 턴입니다.

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     0x5E      |     0x2A      |     0x4D      |     0x18      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     0x20      |     0x00      |     0x00      |     0x00      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
+             SHA-256 of the dictionary, 32 bytes               +
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
+         zstd frame compressed against the dictionary,         +
|         ending in a content checksum                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
DCZ 요청 본문이며 RFC 9842 5절을 따릅니다. 처음 8바이트는 zstd의 스킵 가능 프레임 시그니처입니다. 기능 지원 통보, 저장 확인, 재시도 표식은 Portway가 자체적으로 정의한 규약이며 프로토콜 문서에 정리해 두었습니다.

기준이 사라지면 한 단계씩 물러섭니다

수신기가 재시작하거나, 오래된 본문을 밀어내거나, 처음 보는 송신기를 만날 수 있습니다. 어느 쪽이든 요청이 실패하지는 않습니다. 수신기는 복원할 수 없는 요청을 애플리케이션이 실행되기 전에 거부하고 거부했다는 표식을 남기며, 송신기는 한 단계 덜 압축해 다시 시도합니다.

  1. 보관된 본문을 기준으로 한 DCZ 412 사전이 사라졌거나 400·415로 프레임이 거부되면 물러섭니다
  2. 사전 없이 zstd만 415로 인코딩이 거부되면 물러섭니다
  3. 압축 없이, 에이전트가 보낸 그대로 항상 받아들여집니다

요청은 아무리 많아도 세 번까지만 나갑니다. Portway가 다시 보내는 것은 수신기의 표식이 붙은 거부뿐이고, 수신기는 애플리케이션 응답에서 그 표식을 지웁니다. 그래서 애플리케이션 때문에 재전송이 일어날 수 없고, 같은 요청이 두 번 실행되는 일도 없습니다. 애플리케이션이 낸 오류는 그대로 에이전트에게 돌아갑니다.

한 턴의 비용은 그 턴이 더한 만큼입니다

같은 벤치마크를 네 가지 컨텍스트 크기로, 각각 스무 턴씩 돌렸습니다. zstd만으로 압축하면 요청이 약 5분의 1로 줄고, 직전 턴 사전을 쓰면 컨텍스트가 아무리 커도 약 1킬로바이트만 나갑니다.

첫 턴을 빼고, 턴마다 회선으로 나간 바이트
1턴의 컨텍스트 비압축 zstd만 Portway 20턴 동안 절감
128 KiB175,40541,9621,10498.5%
256 KiB306,50369,6081,05898.7%
512 KiB570,438122,0891,03498.8%
1 MiB1,094,276222,9371,04898.9%

마지막 열에는 사전을 만들려고 컨텍스트 전체를 보내는 첫 요청까지 포함됩니다. 합성 텍스트는 zstd만 써도 Portway 자신의 소스에서와 비슷한 절감률이 나오도록 맞춰 두었습니다. 모든 실행 결과는 바이트 단위까지 똑같이 재현되고, 복원된 본문은 보낸 바이트의 SHA-256과 대조해 검증합니다.

cargo build --release --locked
python3 scripts/bench.py --turns 20 --context-kb 1024

하지 않는 일

줄이는 것은 바이트이지, 토큰이 아닙니다.
모델이 받는 요청은 똑같고, 청구되는 토큰도 똑같습니다. 프로바이더의 프롬프트 캐싱은 반복되는 접두사를 처리하는 비용을 줄이고, Portway는 그 접두사를 보내는 비용을 줄입니다. 둘을 함께 쓰면 됩니다.
수신기가 필요합니다.
프로토콜 지원을 알리지 않는 API에 바로 연결하면, 송신기는 요청을 압축하지 않고 그대로 넘깁니다. 수신기는 portway receive이거나, 이 프로토콜을 구현한 아무 서버나 됩니다.
재시작하면 잊습니다.
사전은 양쪽 메모리에만 존재하고, 자격 증명마다 따로 둡니다. 기본 설정에서는 32 KiB 미만의 본문이 사전이 되지 않습니다.
평범한 HTTP/1.1만 씁니다.
WebSocket도 CONNECT도 HTTP/2도 인바운드 TLS도 없습니다. TLS와 인증은 앞단의 게이트웨이에 맡기면 됩니다. 요청은 설정값에 따라 최대 256 MiB까지 버퍼링하고, 응답은 스트리밍합니다.