@@ -50,14 +50,16 @@ import {
5050} from "./usage.js" ;
5151
5252const DEFAULT_PROXY_PORT = 8797 ;
53+ const PROXY_PORT_SCAN_ATTEMPTS = 40 ;
5354const SHARED_PROXY_HEALTH_TIMEOUT_MS = 750 ;
5455
55- function configuredProxyPort ( ) : number {
56+ /** Explicit pin via env, or `null` → pick a free port dynamically. */
57+ function envProxyPort ( ) : number | null {
5658 const raw = process . env . OPENCODE_COMMANDCODE_PROXY_PORT ;
5759 const parsed = raw ? Number ( raw ) : NaN ;
5860 return Number . isInteger ( parsed ) && parsed > 0 && parsed < 65536
5961 ? parsed
60- : DEFAULT_PROXY_PORT ;
62+ : null ;
6163}
6264
6365const SSE_HEADERS = {
@@ -93,11 +95,12 @@ export function setStreamGenerateForTests(
9395}
9496
9597export function getCommandProxyBaseUrl ( ) : string {
96- return `http://127.0.0.1:${ proxyPort ?? configuredProxyPort ( ) } /v1` ;
98+ const port = proxyPort ?? envProxyPort ( ) ?? DEFAULT_PROXY_PORT ;
99+ return `http://127.0.0.1:${ port } /v1` ;
97100}
98101
99102export function getProxyPort ( ) : number | null {
100- return proxyPort ?? configuredProxyPort ( ) ;
103+ return proxyPort ?? envProxyPort ( ) ;
101104}
102105
103106function isAddrInUseError ( err : unknown ) : boolean {
@@ -111,19 +114,16 @@ function isAddrInUseError(err: unknown): boolean {
111114 ) ;
112115}
113116
114- async function isSharedProxyHealthy ( ) : Promise < boolean > {
117+ async function isProxyHealthyOn ( port : number ) : Promise < boolean > {
115118 const controller = new AbortController ( ) ;
116119 const timeout = setTimeout (
117120 ( ) => controller . abort ( ) ,
118121 SHARED_PROXY_HEALTH_TIMEOUT_MS ,
119122 ) ;
120123 try {
121- const res = await fetch (
122- `http://127.0.0.1:${ configuredProxyPort ( ) } /v1/models` ,
123- {
124- signal : controller . signal ,
125- } ,
126- ) ;
124+ const res = await fetch ( `http://127.0.0.1:${ port } /v1/models` , {
125+ signal : controller . signal ,
126+ } ) ;
127127 if ( ! res . ok ) return false ;
128128 const body = ( await res . json ( ) . catch ( ( ) => undefined ) ) as
129129 | { object ?: unknown ; data ?: unknown }
@@ -136,44 +136,76 @@ async function isSharedProxyHealthy(): Promise<boolean> {
136136 }
137137}
138138
139+ async function bindProxy ( port : number ) : Promise < number > {
140+ const hostname = "127.0.0.1" ;
141+ server = Bun . serve ( {
142+ hostname,
143+ port,
144+ async fetch ( req ) {
145+ return handleRequest ( req ) ;
146+ } ,
147+ } ) ;
148+ proxyPort = server . port ?? port ;
149+ log . info (
150+ `[opencode-commandcode] proxy listening on ${ getCommandProxyBaseUrl ( ) } ` ,
151+ ) ;
152+ return proxyPort ;
153+ }
154+
155+ /**
156+ * Start the local OpenAI-compatible proxy.
157+ *
158+ * Port selection:
159+ * - `OPENCODE_COMMANDCODE_PROXY_PORT` → bind/reuse that exact port
160+ * - otherwise prefer `8797`, then scan upward, then fall back to OS ephemeral (`0`)
161+ */
139162export async function startProxy ( tokenProvider : TokenProvider ) : Promise < number > {
140163 getAccessToken = tokenProvider ;
141164 if ( server && proxyPort ) return proxyPort ;
142165
143- const desiredPort = configuredProxyPort ( ) ;
166+ const fixed = envProxyPort ( ) ;
167+ if ( fixed !== null ) {
168+ if ( await isProxyHealthyOn ( fixed ) ) {
169+ proxyPort = fixed ;
170+ log . info (
171+ `[opencode-commandcode] reusing healthy proxy on ${ getCommandProxyBaseUrl ( ) } ` ,
172+ ) ;
173+ return proxyPort ;
174+ }
175+ try {
176+ return await bindProxy ( fixed ) ;
177+ } catch ( err ) {
178+ if ( isAddrInUseError ( err ) && ( await isProxyHealthyOn ( fixed ) ) ) {
179+ proxyPort = fixed ;
180+ log . info (
181+ `[opencode-commandcode] port ${ fixed } in use; reusing existing proxy` ,
182+ ) ;
183+ return proxyPort ;
184+ }
185+ throw err ;
186+ }
187+ }
144188
145- if ( await isSharedProxyHealthy ( ) ) {
146- proxyPort = desiredPort ;
189+ // Dynamic: reuse preferred default when a healthy proxy is already there.
190+ if ( await isProxyHealthyOn ( DEFAULT_PROXY_PORT ) ) {
191+ proxyPort = DEFAULT_PROXY_PORT ;
147192 log . info (
148193 `[opencode-commandcode] reusing healthy proxy on ${ getCommandProxyBaseUrl ( ) } ` ,
149194 ) ;
150195 return proxyPort ;
151196 }
152197
153- const hostname = "127.0.0.1" ;
154- try {
155- server = Bun . serve ( {
156- hostname,
157- port : desiredPort ,
158- async fetch ( req ) {
159- return handleRequest ( req ) ;
160- } ,
161- } ) ;
162- proxyPort = server . port ?? desiredPort ;
163- log . info (
164- `[opencode-commandcode] proxy listening on ${ getCommandProxyBaseUrl ( ) } ` ,
165- ) ;
166- return proxyPort ;
167- } catch ( err ) {
168- if ( isAddrInUseError ( err ) && ( await isSharedProxyHealthy ( ) ) ) {
169- proxyPort = desiredPort ;
170- log . info (
171- `[opencode-commandcode] port ${ desiredPort } in use; reusing existing proxy` ,
172- ) ;
173- return proxyPort ;
198+ for ( let i = 0 ; i < PROXY_PORT_SCAN_ATTEMPTS ; i ++ ) {
199+ const candidate = DEFAULT_PROXY_PORT + i ;
200+ try {
201+ return await bindProxy ( candidate ) ;
202+ } catch ( err ) {
203+ if ( ! isAddrInUseError ( err ) ) throw err ;
174204 }
175- throw err ;
176205 }
206+
207+ // Last resort — let the OS pick an ephemeral port.
208+ return await bindProxy ( 0 ) ;
177209}
178210
179211export async function stopProxy ( ) : Promise < void > {
0 commit comments