@koreanhole/opencodex 2.96.0 → 2.98.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,351 @@
1
+ import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
4
+ import type { OcxProviderModel } from "../types";
5
+ import { discoverClineLiveModels } from "../adapters/cli-agent/cline/live-models";
6
+ import { discoverGeminiLiveModels } from "../adapters/cli-agent/gemini/live-models";
7
+ import type { ClineTurnDeps } from "../adapters/cli-agent/cline/adapter";
8
+ import type { GeminiTurnDeps } from "../adapters/cli-agent/gemini/adapter";
9
+
10
+ // Cline ACP descriptors expose no context source; this fallback is Cline-only.
11
+ const CATALOG_FALLBACK_CONTEXT = 128_000;
12
+ const BASE_INSTRUCTIONS = "You are a helpful coding assistant.";
13
+ const MANAGED_PREFIXES = ["cline-cli/", "gemini-cli/"] as const;
14
+
15
+ export interface CodexCatalogModel {
16
+ slug: string;
17
+ display_name: string;
18
+ description: string;
19
+ default_reasoning_level: string;
20
+ supported_reasoning_levels: Array<{ effort: string; description: string }>;
21
+ shell_type: "unified_exec";
22
+ visibility: "list";
23
+ supported_in_api: true;
24
+ priority: number;
25
+ base_instructions: string;
26
+ web_search_tool_type: "text_and_image";
27
+ supports_search_tool: false;
28
+ tool_mode: "code_mode_only";
29
+ context_window: number;
30
+ max_context_window: number;
31
+ auto_compact_token_limit: number;
32
+ input_modalities: Array<"text" | "image" | "audio">;
33
+ node_repl_disabled: false;
34
+ node_repl_auto_review_required: false;
35
+ include_plugin_usage_instructions: false;
36
+ include_apps_usage_instructions: false;
37
+ include_skills_usage_instructions: false;
38
+ supports_reasoning_summaries: boolean;
39
+ default_reasoning_summary: string;
40
+ support_verbosity: boolean;
41
+ default_verbosity: "low";
42
+ apply_patch_tool_type: "freeform";
43
+ truncation_policy: { mode: "tokens"; limit: number };
44
+ supports_parallel_tool_calls: true;
45
+ supports_image_detail_original: false;
46
+ experimental_supported_tools: [];
47
+ effective_context_window_percent: number;
48
+ comp_hash: null;
49
+ multi_agent_version: "v1";
50
+ auto_review_model_override: null;
51
+ supports_reasoning_effort_updates: false;
52
+ use_responses_lite: false;
53
+ additional_speed_tiers: [];
54
+ service_tiers: [];
55
+ availability_nux: null;
56
+ upgrade: null;
57
+ }
58
+
59
+ export interface CodexCatalogDocument {
60
+ fetched_at: string;
61
+ etag: null;
62
+ client_version: string;
63
+ identity: string;
64
+ models: CodexCatalogModel[];
65
+ }
66
+
67
+ export interface CodexCatalogDiscoveryOptions {
68
+ env?: NodeJS.ProcessEnv;
69
+ clineDeps?: ClineTurnDeps;
70
+ geminiDeps?: GeminiTurnDeps;
71
+ geminiBinary?: string;
72
+ geminiPackageRoot?: string;
73
+ now?: Date;
74
+ forceRefresh?: boolean;
75
+ }
76
+
77
+ export interface CodexCatalogModels {
78
+ models: CodexCatalogModel[];
79
+ warnings: string[];
80
+ }
81
+
82
+ export type CodexCatalogSyncResult =
83
+ | {
84
+ ok: true;
85
+ path: string;
86
+ configured: boolean;
87
+ written: boolean;
88
+ modelCount: number;
89
+ geminiModelCount: number;
90
+ reasoningModelCount: number;
91
+ reloadRequired: true;
92
+ warnings: string[];
93
+ }
94
+ | {
95
+ ok: false;
96
+ path?: string;
97
+ reason: "unsafe_path" | "malformed_catalog" | "write_failed" | "discovery_failed";
98
+ message: string;
99
+ };
100
+
101
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
102
+ return value && typeof value === "object" && !Array.isArray(value)
103
+ ? value as Record<string, unknown>
104
+ : undefined;
105
+ }
106
+
107
+ function encodeRoutedId(id: string): string {
108
+ return id.replaceAll("/", "-");
109
+ }
110
+
111
+ export function buildCodexCatalogModel(providerId: "cline-cli" | "gemini-cli", model: OcxProviderModel, index: number): CodexCatalogModel {
112
+ const levels = (model.supportedReasoningLevels ?? []).map(level => ({
113
+ effort: level.effort,
114
+ description: level.description,
115
+ }));
116
+ const defaultLevel = model.defaultReasoningLevel ?? levels[0]?.effort ?? "low";
117
+ const isDefault = model.id === "default";
118
+ const providerLabel = providerId === "gemini-cli" ? "Gemini CLI" : "Cline CLI";
119
+ const contextWindow = model.contextWindowTokens ?? (providerId === "cline-cli" ? CATALOG_FALLBACK_CONTEXT : undefined);
120
+ if (contextWindow === undefined) {
121
+ throw new Error(`Gemini model "${model.id}" has no runtime context window.`);
122
+ }
123
+ const deterministicThreshold = providerId === "gemini-cli"
124
+ ? model.compressionThreshold
125
+ : model.compressionThreshold ?? 0.9;
126
+ const compactLimit = deterministicThreshold !== undefined
127
+ ? Math.max(1, Math.floor(contextWindow * deterministicThreshold))
128
+ : contextWindow;
129
+ const effectivePercent = deterministicThreshold !== undefined
130
+ ? Math.max(1, Math.min(100, Math.round(deterministicThreshold * 100)))
131
+ : 100;
132
+ return {
133
+ slug: `${providerId}/${encodeRoutedId(model.id)}`,
134
+ display_name: model.name ?? (isDefault ? `${providerLabel} default` : model.id),
135
+ description: model.description ?? `Routed via opencodex -> ${providerLabel}.`,
136
+ default_reasoning_level: levels.length > 0 ? defaultLevel : "low",
137
+ supported_reasoning_levels: levels,
138
+ shell_type: "unified_exec",
139
+ visibility: "list",
140
+ supported_in_api: true,
141
+ priority: 10 + index,
142
+ base_instructions: BASE_INSTRUCTIONS,
143
+ web_search_tool_type: "text_and_image",
144
+ supports_search_tool: false,
145
+ tool_mode: "code_mode_only",
146
+ context_window: contextWindow,
147
+ max_context_window: model.maxContextWindowTokens ?? contextWindow,
148
+ auto_compact_token_limit: compactLimit,
149
+ input_modalities: [...(model.inputModalities ?? ["text"])],
150
+ node_repl_disabled: false,
151
+ node_repl_auto_review_required: false,
152
+ include_plugin_usage_instructions: false,
153
+ include_apps_usage_instructions: false,
154
+ include_skills_usage_instructions: false,
155
+ supports_reasoning_summaries: levels.length > 0,
156
+ default_reasoning_summary: levels.length > 0 ? "auto" : "none",
157
+ support_verbosity: false,
158
+ default_verbosity: "low",
159
+ apply_patch_tool_type: "freeform",
160
+ truncation_policy: { mode: "tokens", limit: 10_000 },
161
+ supports_parallel_tool_calls: true,
162
+ supports_image_detail_original: false,
163
+ experimental_supported_tools: [],
164
+ effective_context_window_percent: effectivePercent,
165
+ comp_hash: null,
166
+ multi_agent_version: "v1",
167
+ auto_review_model_override: null,
168
+ supports_reasoning_effort_updates: false,
169
+ use_responses_lite: false,
170
+ additional_speed_tiers: [],
171
+ service_tiers: [],
172
+ availability_nux: null,
173
+ upgrade: null,
174
+ };
175
+ }
176
+
177
+ function defaultModel(): OcxProviderModel {
178
+ return { id: "default" };
179
+ }
180
+
181
+ function normalizedModels(models: readonly OcxProviderModel[]): OcxProviderModel[] {
182
+ const seen = new Set<string>();
183
+ const out: OcxProviderModel[] = [];
184
+ for (const model of [defaultModel(), ...models]) {
185
+ if (!seen.has(model.id)) {
186
+ seen.add(model.id);
187
+ out.push(model);
188
+ }
189
+ }
190
+ return out;
191
+ }
192
+
193
+ export async function discoverCodexCatalogModels(options: CodexCatalogDiscoveryOptions = {}): Promise<CodexCatalogModels> {
194
+ const env = options.env ?? process.env;
195
+ const [cline, gemini] = await Promise.all([
196
+ discoverClineLiveModels({ env, ...(options.clineDeps ? { deps: options.clineDeps } : {}) }),
197
+ discoverGeminiLiveModels({
198
+ env,
199
+ ...(options.geminiBinary ? { binary: options.geminiBinary } : {}),
200
+ ...(options.geminiDeps ? { deps: options.geminiDeps } : {}),
201
+ ...(options.geminiPackageRoot ? { packageRoot: options.geminiPackageRoot } : {}),
202
+ ...(options.forceRefresh ? { forceRefresh: true } : {}),
203
+ }),
204
+ ]);
205
+ const warnings: string[] = [];
206
+ if (!cline.ok && cline.reason !== "not_found" && env.OPENCODEX_CLINE_ACP?.trim() !== "0") warnings.push(`Cline: ${cline.detail}`);
207
+ if (!gemini.ok && gemini.reason !== "not_found" && env.OPENCODEX_GEMINI_ACP?.trim() !== "0") warnings.push(`Gemini: ${gemini.detail}`);
208
+ if (gemini.ok && gemini.metadataWarning) warnings.push(`Gemini metadata: ${gemini.metadataWarning}`);
209
+ const clineModels = normalizedModels(cline.ok ? cline.models.map(id => ({ id })) : []);
210
+ const geminiModels = normalizedModels(gemini.ok ? gemini.modelInfo : []);
211
+ const geminiDefault = geminiModels.find(model => model.id === "default");
212
+ const geminiContexts = geminiModels.flatMap(model => model.contextWindowTokens !== undefined ? [model.contextWindowTokens] : []);
213
+ if (geminiDefault && geminiContexts.length > 0) {
214
+ const conservative = Math.min(...geminiContexts);
215
+ geminiDefault.contextWindowTokens = conservative;
216
+ geminiDefault.maxContextWindowTokens = conservative;
217
+ geminiDefault.compressionThreshold = geminiModels.find(model => model.compressionThreshold !== undefined)?.compressionThreshold;
218
+ const modalities = geminiModels.flatMap(model => model.inputModalities ?? []);
219
+ geminiDefault.inputModalities = [...new Set(modalities.length > 0 ? modalities : ["text" as const])]
220
+ .filter((value): value is "text" | "image" | "audio" => value === "text" || value === "image" || value === "audio");
221
+ }
222
+ const models: CodexCatalogModel[] = [];
223
+ for (const model of clineModels) {
224
+ models.push(buildCodexCatalogModel("cline-cli", model, models.length));
225
+ }
226
+ for (const model of geminiModels) {
227
+ if (model.contextWindowTokens === undefined) {
228
+ warnings.push(`Gemini model "${model.id}" has no runtime context window; omitted from the Codex catalog.`);
229
+ continue;
230
+ }
231
+ models.push(buildCodexCatalogModel("gemini-cli", model, models.length));
232
+ }
233
+ return {
234
+ models,
235
+ warnings,
236
+ };
237
+ }
238
+
239
+ export function buildCodexCatalogDocument(
240
+ models: CodexCatalogModel[],
241
+ now = new Date(),
242
+ preserved: CodexCatalogModel[] = [],
243
+ ): CodexCatalogDocument {
244
+ return {
245
+ fetched_at: now.toISOString(),
246
+ etag: null,
247
+ client_version: "0.165.0",
248
+ identity: "opencodex",
249
+ models: [...preserved, ...models],
250
+ };
251
+ }
252
+
253
+ function readRootTomlString(text: string, key: string): string | undefined {
254
+ const firstTable = text.search(/^\s*\[/m);
255
+ const root = firstTable < 0 ? text : text.slice(0, firstTable);
256
+ const match = new RegExp(`^\\s*${key}\\s*=\\s*"([^"]+)"`, "m").exec(root);
257
+ return match?.[1]?.trim() || undefined;
258
+ }
259
+
260
+ export async function writeCodexCatalog(
261
+ path: string,
262
+ document: CodexCatalogDocument,
263
+ ): Promise<void> {
264
+ const parent = dirname(path);
265
+ await mkdir(parent, { recursive: true, mode: 0o700 });
266
+ const temporary = `${path}.${process.pid}.${crypto.randomUUID()}.tmp`;
267
+ try {
268
+ await writeFile(temporary, `${JSON.stringify(document, null, 2)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
269
+ await chmod(temporary, 0o600);
270
+ await rename(temporary, path);
271
+ await chmod(path, 0o600).catch(() => undefined);
272
+ } finally {
273
+ await rm(temporary, { force: true }).catch(() => undefined);
274
+ }
275
+ }
276
+
277
+ function managedSlug(slug: unknown): boolean {
278
+ return typeof slug === "string" && MANAGED_PREFIXES.some(prefix => slug.startsWith(prefix));
279
+ }
280
+
281
+ export async function syncCodexCatalog(
282
+ options: CodexCatalogDiscoveryOptions & {
283
+ path?: string;
284
+ discover?: typeof discoverCodexCatalogModels;
285
+ } = {},
286
+ ): Promise<CodexCatalogSyncResult> {
287
+ const env = options.env ?? process.env;
288
+ const codexHome = env.CODEX_HOME?.trim() || join(homedir(), ".codex");
289
+ let configuredPath: string | undefined;
290
+ try {
291
+ configuredPath = readRootTomlString(await readFile(join(codexHome, "config.toml"), "utf8"), "model_catalog_json");
292
+ } catch {
293
+ configuredPath = undefined;
294
+ }
295
+ const configured = configuredPath !== undefined;
296
+ const selected = options.path
297
+ ? (isAbsolute(options.path) ? options.path : resolve(options.path))
298
+ : configuredPath
299
+ ? resolve(codexHome, configuredPath)
300
+ : join(codexHome, "opencodex-catalog.json");
301
+ if (configured && basename(selected) !== "opencodex-catalog.json") {
302
+ return { ok: false, path: selected, reason: "unsafe_path", message: "Configured model_catalog_json is not an opencodex catalog; refusing to overwrite it." };
303
+ }
304
+
305
+ let existing: Record<string, unknown> = {};
306
+ try {
307
+ const parsed = asRecord(JSON.parse(await readFile(selected, "utf8")));
308
+ if (!parsed) return { ok: false, path: selected, reason: "malformed_catalog", message: "Existing Codex catalog is not a JSON object." };
309
+ existing = parsed;
310
+ } catch (error) {
311
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
312
+ return { ok: false, path: selected, reason: "malformed_catalog", message: "Existing Codex catalog could not be parsed; it was preserved." };
313
+ }
314
+ }
315
+
316
+ let discovered: CodexCatalogModels;
317
+ try {
318
+ discovered = await (options.discover ?? discoverCodexCatalogModels)({ ...options, forceRefresh: true });
319
+ } catch (error) {
320
+ return { ok: false, reason: "discovery_failed", message: error instanceof Error ? error.message : String(error) };
321
+ }
322
+ const existingModels = Array.isArray(existing.models) ? existing.models : [];
323
+ if (existing.models !== undefined && !Array.isArray(existing.models)) {
324
+ return { ok: false, path: selected, reason: "malformed_catalog", message: "Existing Codex catalog models field is not an array; it was preserved." };
325
+ }
326
+ if (existingModels.some(model => !asRecord(model))) {
327
+ return { ok: false, path: selected, reason: "malformed_catalog", message: "Existing Codex catalog contains a non-object model row; it was preserved." };
328
+ }
329
+ const preserved = existingModels.filter(model => !managedSlug(asRecord(model)?.slug));
330
+ const document = buildCodexCatalogDocument(
331
+ discovered.models,
332
+ options.now ?? new Date(),
333
+ preserved as CodexCatalogModel[],
334
+ );
335
+ try {
336
+ await writeCodexCatalog(selected, document);
337
+ } catch (error) {
338
+ return { ok: false, path: selected, reason: "write_failed", message: error instanceof Error ? error.message : String(error) };
339
+ }
340
+ return {
341
+ ok: true,
342
+ path: selected,
343
+ configured,
344
+ written: true,
345
+ modelCount: document.models.length,
346
+ geminiModelCount: document.models.filter(model => model.slug.startsWith("gemini-cli/")).length,
347
+ reasoningModelCount: document.models.filter(model => model.slug.startsWith("gemini-cli/") && (model.supported_reasoning_levels?.length ?? 0) > 0).length,
348
+ reloadRequired: true,
349
+ warnings: discovered.warnings,
350
+ };
351
+ }
@@ -12,8 +12,14 @@ import type { ClineTurnDeps } from "../adapters/cli-agent/cline/adapter";
12
12
  import type { GeminiTurnDeps } from "../adapters/cli-agent/gemini/adapter";
13
13
  import { discoverClineLiveModels } from "../adapters/cli-agent/cline/live-models";
14
14
  import { discoverGeminiLiveModels } from "../adapters/cli-agent/gemini/live-models";
15
- import { handleResponses, modelList, RequestValidationError, UnsupportedMediaError, type ProviderModels } from "./responses";
15
+ import { buildCodexCatalogDocument, discoverCodexCatalogModels } from "../lib/codex-catalog";
16
+ import { handleResponses, modelList, RequestValidationError, UnsupportedMediaError, UnsupportedOptionError, type ProviderModels } from "./responses";
16
17
  import { handleCountTokens, handleMessages } from "./messages";
18
+ import {
19
+ createWebSocketHandlers,
20
+ type WebSocketData,
21
+ type WebSocketRequestObservation,
22
+ } from "./websocket";
17
23
 
18
24
  const MAX_REQUEST_BYTES = 32 * 1024 * 1024;
19
25
  const REQUEST_TIMEOUT_MS = 300_000;
@@ -22,6 +28,8 @@ export interface ServerOptions {
22
28
  env?: NodeJS.ProcessEnv;
23
29
  clineDeps?: ClineTurnDeps;
24
30
  geminiDeps?: GeminiTurnDeps;
31
+ geminiPackageRoot?: string;
32
+ onWebSocketRequest?: (observation: WebSocketRequestObservation) => void;
25
33
  }
26
34
 
27
35
  export interface RunningServer {
@@ -64,6 +72,9 @@ async function boundedJson(req: Request): Promise<unknown> {
64
72
  }
65
73
 
66
74
  function errorResponse(error: unknown): Response {
75
+ if (error instanceof UnsupportedOptionError) {
76
+ return Response.json({ error: { message: error.message, type: error.type, code: error.code } }, { status: error.status });
77
+ }
67
78
  if (error instanceof UnsupportedMediaError) {
68
79
  return Response.json({ error: { message: error.message, type: error.type, code: error.code } }, { status: error.status });
69
80
  }
@@ -80,10 +91,11 @@ async function discoverModels(
80
91
  env: NodeJS.ProcessEnv,
81
92
  clineDeps: ClineTurnDeps | undefined,
82
93
  geminiDeps: GeminiTurnDeps | undefined,
94
+ geminiPackageRoot: string | undefined,
83
95
  ): Promise<ProviderModels> {
84
96
  const [cline, gemini] = await Promise.all([
85
97
  discoverClineLiveModels({ env, ...(clineDeps ? { deps: clineDeps } : {}) }),
86
- discoverGeminiLiveModels({ env, ...(geminiDeps ? { deps: geminiDeps } : {}) }),
98
+ discoverGeminiLiveModels({ env, ...(geminiDeps ? { deps: geminiDeps } : {}), ...(geminiPackageRoot ? { packageRoot: geminiPackageRoot } : {}) }),
87
99
  ]);
88
100
  if (!cline.ok && cline.reason !== "not_found" && env.OPENCODEX_CLINE_ACP?.trim() !== "0") {
89
101
  console.warn(`[opencodex] Cline model discovery degraded [${cline.reason}]: ${cline.detail}`);
@@ -93,7 +105,9 @@ async function discoverModels(
93
105
  }
94
106
  return {
95
107
  "cline-cli": cline.ok ? ["default", ...cline.models.filter(id => id !== "default")] : ["default"],
96
- "gemini-cli": gemini.ok ? ["default", ...gemini.models.filter(id => id !== "default")] : ["default"],
108
+ "gemini-cli": gemini.ok
109
+ ? ["default", ...gemini.modelInfo.filter(model => model.id !== "default")]
110
+ : ["default"],
97
111
  };
98
112
  }
99
113
 
@@ -109,7 +123,7 @@ export function startServer(config: RuntimeConfig | number, options: ServerOptio
109
123
  const token = apiToken(config, env);
110
124
  let server: ReturnType<typeof Bun.serve>;
111
125
 
112
- const fetch = async (req: Request): Promise<Response> => {
126
+ const fetch = async (req: Request, bunServer: Bun.Server<WebSocketData>): Promise<Response | undefined> => {
113
127
  const url = new URL(req.url);
114
128
  if (url.pathname === "/healthz") {
115
129
  return Response.json({ ok: true, service: "opencodex", version: packageInfo.version });
@@ -117,21 +131,35 @@ export function startServer(config: RuntimeConfig | number, options: ServerOptio
117
131
  if (!authorized(req, token)) {
118
132
  return Response.json({ error: { message: "Unauthorized", type: "authentication_error", code: "unauthorized" } }, { status: 401 });
119
133
  }
134
+ if (req.method === "GET" && url.pathname === "/v1/responses" && req.headers.get("upgrade")?.toLowerCase() === "websocket") {
135
+ const data: WebSocketData = { headers: req.headers, queue: Promise.resolve(), closed: false };
136
+ if (bunServer.upgrade(req, { data })) return undefined;
137
+ return new Response("WebSocket upgrade failed.", { status: 400 });
138
+ }
120
139
  try {
121
140
  if (req.method === "GET" && url.pathname === "/v1/models") {
122
- const known = await discoverModels(env, options.clineDeps, options.geminiDeps);
141
+ const known = await discoverModels(env, options.clineDeps, options.geminiDeps, options.geminiPackageRoot);
123
142
  return Response.json({ object: "list", data: modelList(known) });
124
143
  }
144
+ if (req.method === "GET" && url.pathname === "/v1/model-catalog") {
145
+ const discovered = await discoverCodexCatalogModels({
146
+ env,
147
+ ...(options.clineDeps ? { clineDeps: options.clineDeps } : {}),
148
+ ...(options.geminiDeps ? { geminiDeps: options.geminiDeps } : {}),
149
+ ...(options.geminiPackageRoot ? { geminiPackageRoot: options.geminiPackageRoot } : {}),
150
+ });
151
+ return Response.json(buildCodexCatalogDocument(discovered.models));
152
+ }
125
153
  if (req.method === "POST" && url.pathname === "/v1/responses") {
126
154
  const body = await boundedJson(req);
127
- const known = await discoverModels(env, options.clineDeps, options.geminiDeps);
155
+ const known = await discoverModels(env, options.clineDeps, options.geminiDeps, options.geminiPackageRoot);
128
156
  const signal = AbortSignal.any([req.signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)]);
129
157
  const incoming: IncomingMeta = { headers: req.headers, abortSignal: signal };
130
158
  return await handleResponses(body, incoming, known, turnDeps, config.defaultProvider);
131
159
  }
132
160
  if (req.method === "POST" && url.pathname === "/v1/messages") {
133
161
  const body = await boundedJson(req);
134
- const known = await discoverModels(env, options.clineDeps, options.geminiDeps);
162
+ const known = await discoverModels(env, options.clineDeps, options.geminiDeps, options.geminiPackageRoot);
135
163
  const signal = AbortSignal.any([req.signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)]);
136
164
  return await handleMessages(body, { headers: req.headers, abortSignal: signal }, known, turnDeps, config.defaultProvider);
137
165
  }
@@ -144,7 +172,20 @@ export function startServer(config: RuntimeConfig | number, options: ServerOptio
144
172
  }
145
173
  };
146
174
 
147
- server = Bun.serve({ hostname: config.host, port: config.port, idleTimeout: 255, fetch });
175
+ const websocketContext = {
176
+ knownModels: () => discoverModels(env, options.clineDeps, options.geminiDeps, options.geminiPackageRoot),
177
+ deps: turnDeps,
178
+ defaultProvider: config.defaultProvider,
179
+ timeoutMs: REQUEST_TIMEOUT_MS,
180
+ ...(options.onWebSocketRequest ? { onRequest: options.onWebSocketRequest } : {}),
181
+ };
182
+ server = Bun.serve({
183
+ hostname: config.host,
184
+ port: config.port,
185
+ idleTimeout: 255,
186
+ fetch,
187
+ websocket: createWebSocketHandlers(websocketContext),
188
+ });
148
189
  return {
149
190
  port: server.port ?? config.port,
150
191
  host: config.host,