Portway
Language English

Cut your coding agent’s network traffic.

Every turn, Claude Code, Codex and other coding agents resend the whole growing conversation. Portway compresses each request against the previous one, so only the difference travels to a Portway receiver, which restores the original and forwards it to the model.

brew install rath/tap/portway

macOS Apple silicon · Linux x86_64 / ARM64

One agent session, replayed. Forty turns through the real binaries. The context starts at 256 KiB and grows by 4 KiB a turn.
Sent by the agent
13,877,261 B
zstd alone
3,114,150 B
Portway on the wire
101,551 B

Turn 40: the agent sent 429,900 bytes and 1,029 crossed the wire.

Watch the bytes shrink

Portway comes with a terminal dashboard (portway --tui) and a browser console (--web). Both show each request as it leaves: what the agent sent, what crossed the network, how long it took and how many tokens it carried. These screens were captured from a real Claude Code and Codex session.

The browser console: 41 requests, 61.8 MB (91 percent) of upload saved, the upstreams in use and a per-turn request size chart.

Install, then connect your agent

The Homebrew package includes the terminal dashboard and the browser console; no Rust toolchain is needed.

Prefer guided setup? The Portway plugin for Codex and Claude Code can configure your client and check its connection. Install or upgrade to Portway 0.2.5 or later with Homebrew or a release download; no source build is needed. The plugin only adds the setup and doctor skills; it does not route traffic. After setup, Codex goes through Portway only when started with codex -p portway, while Claude Code goes through it in every new session, because setup writes its user settings.

  1. Install Portway. Supports macOS Apple silicon and Linux x86_64 / ARM64 (glibc 2.28 or later).

    brew install rath/tap/portway

    Without Homebrew? Download a release or build from source. Only source builds need Rust and a C toolchain.

  2. Save your API mounts as portway.toml. Then start Portway with its browser console:

    # 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. Connect Claude Code in a second terminal. First install Claude Code and sign in normally. Leave Portway running, then use this command for one invocation:

    env ANTHROPIC_BASE_URL=http://127.0.0.1:8787/anthropic claude
  4. Connect Codex with a separate profile. First install Codex and sign in with ChatGPT using codex login. Save the following as ~/.codex/portway.config.toml (inside CODEX_HOME if you set it):

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

    Then run this from your project directory:

    codex -p portway

    The profile inherits your usual model and preferences. Run plain codex to use your usual provider. Both clients keep their credentials; Portway needs no API key of its own. Do not append /v1 to either mount URL.

  5. Check your first request. Send a short message in your CLI and confirm it answers and appears in Portway’s log or browser console. These direct provider connections do not enable delta compression. If you already use a remote Portway server, replace 127.0.0.1:8787 in the client settings with its reachable address.

    Persistent settings, API-key options, and troubleshooting

  6. Add a receiver to enable delta compression. Run portway receive behind the gateway in front of your service, then point the sender at the gateway. Receiver setup walks through it.

The same repository ships portway-core, an embeddable Rust library, and a CLI with daemon mode, reports, a terminal dashboard and a browser console. It records request metadata in SQLite, never bodies, credentials or dictionary contents.

Turn 40 is turn 39 plus four kilobytes

A coding agent keeps no state on the server. To take one more step, it sends the system prompt, every earlier message and every tool result again, followed by the few kilobytes that are new. Each request is almost entirely a copy of the one before it.

What each request carries, to scale. The bright end is the part no earlier request contained.
  1. Turn 1263,966 B
  2. Turn 2268,145 B
  3. Turn 3272,556 B
  4. Turn 20344,723 B
  5. Turn 39425,707 B
  6. Turn 40429,900 B

So the upload of a session grows with the square of its length. Where that upload is what you wait for or pay for, it adds up quickly:

Short, single-shot requests gain little. Nothing earlier can serve as a base.

The previous turn becomes the dictionary

Portway runs twice: a sender next to your agent, and a receiver in front of the service it calls. Both keep the last request body they agreed on. The sender compresses the next request against it, and the receiver rebuilds the original before your service sees a byte.

  1. AgentClaude Code, Codex, any client with a base URL
  2. portwaysender, 127.0.0.1:8787
  3. portway receiveon a host you control
  4. Your servicea model server, a gateway, or a provider API
Turn 2 of the session above. The model receives the same bytes the agent sent; only the middle hop carries the delta.

How the two ends agree on a base

  1. Turn 1 seeds the dictionary. It travels zstd-compressed with X-Dict-Store: 1. The receiver decodes it, passes it on and keeps the body.

  2. The receiver confirms what it kept. Its response carries X-Dict-Stored with the SHA-256 of the bytes it actually stored. The sender uses a body as a base only when that hash matches its own copy.

  3. Turn 2 names turn 1 by hash. It travels as DCZ: a 40-byte header carrying the dictionary's SHA-256, then a checksummed zstd frame compressed against it. The sender keeps up to eight confirmed bodies per route and picks the one that shares the longest prefix with the new request. In an append-only conversation, that is the previous turn.

 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                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
A DCZ request body, from RFC 9842, section 5. The first eight bytes are a zstd skippable-frame signature. The capability advertisement, the storage acknowledgement and the retry markers are Portway's own, written up in the protocol.

When a base goes missing, it falls back

A receiver restarts, evicts an old body, or meets a sender it has never seen. None of that fails a request. The receiver refuses what it cannot restore before your application runs, marks the refusal, and the sender tries again with less.

  1. DCZ against a stored body Falls back on 412 dictionary gone, or 400 or 415 frame refused
  2. Plain zstd, no dictionary Falls back on 415 encoding refused
  3. Uncompressed, exactly as the agent sent it Always accepted

A request is sent at most three times. Portway only replays a refusal carrying the receiver's marker, and the receiver strips that marker from application responses, so your application cannot trigger a replay and never runs a request twice. Errors from your application come back to the agent unchanged.

A turn costs what it added

The same benchmark at four context sizes, twenty turns each. Plain zstd shrinks each request by about four fifths; the previous-turn dictionary leaves about a kilobyte, whatever the size of the context.

Bytes on the wire per turn, after the first turn
Context at turn 1 Uncompressed zstd only Portway Saved over 20 turns
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%

The last column includes the first request, which carries the whole context to seed the dictionary. The synthetic text is tuned so that plain zstd saves about as much as it does on Portway's own source. Every run is reproducible byte for byte, and every restored body is checked against the SHA-256 of what was sent.

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

What it does not do

It saves bytes, not tokens.
The model receives the identical request and bills the identical tokens. Provider prompt caching cuts the cost of processing a repeated prefix; Portway cuts the cost of sending it. Use both.
It needs a receiver.
Pointed straight at an API that does not advertise the protocol, the sender forwards requests unchanged and uncompressed. The receiver is portway receive, or any server that implements the protocol.
It forgets on restart.
Dictionaries live only in memory on both ends, kept apart per credential. By default a body under 32 KiB never becomes one.
It speaks plain HTTP/1.1.
No WebSockets, CONNECT, HTTP/2 or inbound TLS; put your gateway in front for TLS and authentication. Requests are buffered up to a configurable 256 MiB, and responses stream.