Skip to content

Commit c92ab32

Browse files
RobLe3claude
andcommitted
[python] port 9484 default + auto-increment; v0.7.5
Why: 9484 is the official IICP port. CLI now resolves the listen port before NAT detection and auto-increments to the next free port, so N nodes on one host bind 9484/9485/9486 → N pinholes, with multiple models sharing one port. Skipped when --public-endpoint is set. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 66dd7d5 commit c92ab32

5 files changed

Lines changed: 55 additions & 8 deletions

File tree

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,17 @@ async def main():
135135
asyncio.run(main())
136136
```
137137

138+
### Listen port — default 9484, auto-increment (v0.7.5+)
139+
140+
The official IICP port **9484** is the default listen port (`IICP_PORT`, `--port`).
141+
The `iicp-node` CLI auto-increments to the next free port when 9484 is already in
142+
use, so you can run several nodes on one host without picking ports by hand — the
143+
first binds 9484, the second 9485, the third 9486, and so on. Each node gets its
144+
own port, hence its own NAT pinhole; multiple models served by one node share that
145+
single port. Auto-increment is skipped when you pass an explicit `--public-endpoint`
146+
(you own the port mapping in that case). `IicpNode.serve(port=…)` uses the port you
147+
give it as-is (no auto-increment at the library level).
148+
138149
---
139150

140151
## NAT traversal — automatic (v0.7.3+)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "iicp-client"
7-
version = "0.7.4"
7+
version = "0.7.5"
88
description = "Official Python client SDK for the IICP protocol"
99
readme = "README.md"
1010
license = {text = "Apache-2.0"}

src/iicp_client/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
TaskResponse,
5050
)
5151

52-
__version__ = "0.7.4"
52+
__version__ = "0.7.5"
5353
__all__ = [
5454
"IicpClient",
5555
"IicpError",

src/iicp_client/cli.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import json
2727
import logging
2828
import os
29+
import socket
2930
import subprocess
3031
import sys
3132
import urllib.parse
@@ -53,6 +54,26 @@ def _env(name: str, default: str | None = None) -> str | None:
5354
return os.environ.get(name, default)
5455

5556

57+
def _find_available_port(host: str, start: int, max_tries: int = 64) -> int:
58+
"""Return the first bindable TCP port >= ``start`` on ``host``.
59+
60+
The official IICP port 9484 is the starting point; when running multiple
61+
nodes on one host (each model on its own port → its own pinhole) the second
62+
node auto-increments to 9485, the third to 9486, and so on. Probes by
63+
attempting a real bind so the chosen port is genuinely free before NAT
64+
detection opens a pinhole and the directory registration advertises it.
65+
"""
66+
bind_host = host if host not in ("", "0.0.0.0") else "0.0.0.0"
67+
for candidate in range(start, start + max_tries):
68+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
69+
try:
70+
probe.bind((bind_host, candidate))
71+
return candidate
72+
except OSError:
73+
continue
74+
return start # exhausted — let serve() surface the real bind error
75+
76+
5677
def _build_parser() -> argparse.ArgumentParser:
5778
p = argparse.ArgumentParser(
5879
prog="iicp-node",
@@ -128,7 +149,7 @@ def _build_parser() -> argparse.ArgumentParser:
128149
serve.add_argument(
129150
"--port",
130151
type=int,
131-
default=int(_env("IICP_PORT", "8020") or "8020"),
152+
default=int(_env("IICP_PORT", "9484") or "9484"),
132153
help="HTTP listen port. env: IICP_PORT",
133154
)
134155
serve.add_argument(
@@ -208,7 +229,7 @@ async def _serve(args: argparse.Namespace) -> int:
208229
args.node_id = args.node_id or saved.node_id
209230
if args.max_concurrent == 4:
210231
args.max_concurrent = saved.max_concurrent
211-
if args.port == 8020:
232+
if args.port == 9484:
212233
args.port = saved.port
213234
if args.host == "0.0.0.0":
214235
args.host = saved.host
@@ -225,6 +246,21 @@ async def _serve(args: argparse.Namespace) -> int:
225246
)
226247
return 2
227248

249+
# Resolve the actual listen port before NAT detection: start at the
250+
# requested port (default 9484, the official IICP port) and auto-increment
251+
# to the next free port. This keeps one port per node (multiple models on
252+
# one node share it) while N nodes on one host each get a distinct port →
253+
# distinct pinhole. Skipped when the operator supplies an explicit
254+
# --public-endpoint (they own the port mapping in that case).
255+
if not args.public_endpoint:
256+
resolved_port = _find_available_port(args.host, args.port)
257+
if resolved_port != args.port:
258+
logger.info(
259+
"Port %d in use — auto-incremented to first free port %d.",
260+
args.port, resolved_port,
261+
)
262+
args.port = resolved_port
263+
228264
node_id = (args.node_id or str(uuid.uuid4()))[:36]
229265
public_endpoint = args.public_endpoint or f"http://localhost:{args.port}"
230266

@@ -467,7 +503,7 @@ def _cmd_init(args: argparse.Namespace) -> int:
467503
intent = _prompt("Intent URN", "urn:iicp:intent:llm:chat:v1")
468504
region = _prompt("Region tag", "eu-central")
469505
directory_url = _prompt("Directory URL", "https://iicp.network/api")
470-
port_str = _prompt("Local HTTP port", "8020")
506+
port_str = _prompt("Local HTTP port", "9484")
471507
port = int(port_str)
472508
public_endpoint = _prompt(
473509
"Public endpoint URL (leave blank if you'll use --auto-detect-nat)",

src/iicp_client/node.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ async def my_handler(task: dict) -> dict:
204204
max_concurrent=4,
205205
))
206206
token = await node.register()
207-
await node.serve(my_handler, port=8020, node_token=token)
207+
await node.serve(my_handler, port=9484, node_token=token)
208208
"""
209209

210210
def __init__(self, config: NodeConfig) -> None:
@@ -468,15 +468,15 @@ async def serve(
468468
self,
469469
handler: TaskHandler,
470470
host: str = "0.0.0.0",
471-
port: int = 8020,
471+
port: int = 9484,
472472
node_token: str | None = None,
473473
) -> None:
474474
"""Start the task server (blocks until interrupted).
475475
476476
Args:
477477
handler: ``async def handler(task: dict) -> dict``
478478
host: Bind address (default ``0.0.0.0``).
479-
port: Bind port (default 8020).
479+
port: Bind port (default 9484).
480480
node_token: If provided, starts a background heartbeat loop.
481481
"""
482482
loop = asyncio.get_event_loop()

0 commit comments

Comments
 (0)