Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions omlx/admin/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,12 @@ class GlobalSettingsRequest(BaseModel):
# ModelScope settings
ms_endpoint: Optional[str] = None

# Network settings
network_http_proxy: Optional[str] = None
network_https_proxy: Optional[str] = None
network_no_proxy: Optional[str] = None
network_ca_bundle: Optional[str] = None

# Sampling defaults
sampling_max_context_window: Optional[int] = None
sampling_max_tokens: Optional[int] = None
Expand Down Expand Up @@ -1703,6 +1709,35 @@ async def get_global_settings(is_admin: bool = Depends(require_admin)):
if global_settings is None:
raise HTTPException(status_code=503, detail="Server not initialized")

# Defensive hydration: if runtime network settings are empty but disk has
# values, hydrate from disk so UI/API round-trips do not wipe them.
if (
not global_settings.network.http_proxy
and not global_settings.network.https_proxy
and not global_settings.network.no_proxy
and not global_settings.network.ca_bundle
):
try:
settings_file = global_settings.base_path / "settings.json"
if settings_file.exists():
with open(settings_file, encoding="utf-8") as f:
disk_data = json.load(f)
disk_network = disk_data.get("network")
if isinstance(disk_network, dict):
hydrated = type(global_settings.network).from_dict(disk_network)
if (
hydrated.http_proxy
or hydrated.https_proxy
or hydrated.no_proxy
or hydrated.ca_bundle
):
global_settings.network = hydrated
logger.info(
"Hydrated network settings from disk for admin API response"
)
except (OSError, json.JSONDecodeError, TypeError) as e:
logger.warning(f"Failed to hydrate network settings from disk: {e}")

# Get system memory info for auto calculation
memory_info = get_system_memory_info()

Expand Down Expand Up @@ -1753,6 +1788,12 @@ async def get_global_settings(is_admin: bool = Depends(require_admin)):
"modelscope": {
"endpoint": global_settings.modelscope.endpoint,
},
"network": {
"http_proxy": global_settings.network.http_proxy,
"https_proxy": global_settings.network.https_proxy,
"no_proxy": global_settings.network.no_proxy,
"ca_bundle": global_settings.network.ca_bundle,
},
"sampling": {
"max_context_window": global_settings.sampling.max_context_window,
"max_tokens": global_settings.sampling.max_tokens,
Expand Down Expand Up @@ -1823,6 +1864,45 @@ async def update_global_settings(
if global_settings is None:
raise HTTPException(status_code=503, detail="Server not initialized")

# Defensive hydration: if runtime network settings are empty and request
# does not explicitly carry network fields, recover from disk before save.
network_fields = {
"network_http_proxy",
"network_https_proxy",
"network_no_proxy",
"network_ca_bundle",
}
has_network_fields = any(f in request.model_fields_set for f in network_fields)
if (
not has_network_fields
and not global_settings.network.http_proxy
and not global_settings.network.https_proxy
and not global_settings.network.no_proxy
and not global_settings.network.ca_bundle
):
try:
settings_file = global_settings.base_path / "settings.json"
if settings_file.exists():
with open(settings_file, encoding="utf-8") as f:
disk_data = json.load(f)
disk_network = disk_data.get("network")
if isinstance(disk_network, dict):
hydrated = type(global_settings.network).from_dict(disk_network)
if (
hydrated.http_proxy
or hydrated.https_proxy
or hydrated.no_proxy
or hydrated.ca_bundle
):
global_settings.network = hydrated
logger.info(
"Hydrated network settings from disk before admin save"
)
except (OSError, json.JSONDecodeError, TypeError) as e:
logger.warning(
f"Failed to hydrate network settings from disk before save: {e}"
)

# Track which settings were applied at runtime
runtime_applied: List[str] = []

Expand Down Expand Up @@ -1984,6 +2064,52 @@ async def update_global_settings(
f"{request.ms_endpoint or '(default)'}"
)

# Apply network settings (Live - immediately applied via env vars)
network_changed = False
if request.network_http_proxy is not None:
global_settings.network.http_proxy = request.network_http_proxy
if request.network_http_proxy:
os.environ["HTTP_PROXY"] = request.network_http_proxy
os.environ["http_proxy"] = request.network_http_proxy
else:
os.environ.pop("HTTP_PROXY", None)
os.environ.pop("http_proxy", None)
network_changed = True

if request.network_https_proxy is not None:
global_settings.network.https_proxy = request.network_https_proxy
if request.network_https_proxy:
os.environ["HTTPS_PROXY"] = request.network_https_proxy
os.environ["https_proxy"] = request.network_https_proxy
else:
os.environ.pop("HTTPS_PROXY", None)
os.environ.pop("https_proxy", None)
network_changed = True

if request.network_no_proxy is not None:
global_settings.network.no_proxy = request.network_no_proxy
if request.network_no_proxy:
os.environ["NO_PROXY"] = request.network_no_proxy
os.environ["no_proxy"] = request.network_no_proxy
else:
os.environ.pop("NO_PROXY", None)
os.environ.pop("no_proxy", None)
network_changed = True

if request.network_ca_bundle is not None:
global_settings.network.ca_bundle = request.network_ca_bundle
if request.network_ca_bundle:
os.environ["REQUESTS_CA_BUNDLE"] = request.network_ca_bundle
os.environ["SSL_CERT_FILE"] = request.network_ca_bundle
else:
os.environ.pop("REQUESTS_CA_BUNDLE", None)
os.environ.pop("SSL_CERT_FILE", None)
network_changed = True

if network_changed:
runtime_applied.append("network")
logger.info("Network settings updated")

# Apply sampling settings (Live - immediately applied)
sampling_changed = False
if request.sampling_max_context_window is not None:
Expand Down
6 changes: 6 additions & 0 deletions omlx/admin/static/js/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
sampling: { max_context_window: 32768, max_tokens: 32768, temperature: 1.0, top_p: 0.95, top_k: 0, repetition_penalty: 1.0 },
mcp: { config_path: '' },
huggingface: { endpoint: '' },
network: { http_proxy: '', https_proxy: '', no_proxy: '', ca_bundle: '' },
auth: { api_key_set: false, api_key: '', skip_api_key_verification: false, sub_keys: [] },
claude_code: { context_scaling_enabled: false, target_context_size: 200000, mode: 'cloud', opus_model: null, sonnet_model: null, haiku_model: null },
integrations: { codex_model: null, opencode_model: null, openclaw_model: null, openclaw_tools_profile: 'full' },
Expand Down Expand Up @@ -564,6 +565,7 @@
sampling: { ...this.globalSettings.sampling, ...data.sampling },
mcp: { ...this.globalSettings.mcp, ...data.mcp },
huggingface: { ...this.globalSettings.huggingface, ...data.huggingface },
network: { ...this.globalSettings.network, ...data.network },
auth: { ...this.globalSettings.auth, ...data.auth },
claude_code: { ...this.globalSettings.claude_code, ...data.claude_code },
integrations: { ...this.globalSettings.integrations, ...data.integrations },
Expand Down Expand Up @@ -680,6 +682,10 @@
sampling_top_k: this.globalSettings.sampling.top_k,
sampling_repetition_penalty: this.globalSettings.sampling.repetition_penalty,
mcp_config: this.globalSettings.mcp.config_path,
network_http_proxy: this.globalSettings.network.http_proxy,
network_https_proxy: this.globalSettings.network.https_proxy,
network_no_proxy: this.globalSettings.network.no_proxy,
network_ca_bundle: this.globalSettings.network.ca_bundle,
...(this.globalSettings.auth.api_key ? { api_key: this.globalSettings.auth.api_key } : {}),
skip_api_key_verification: this.globalSettings.auth.skip_api_key_verification,
}),
Expand Down
47 changes: 47 additions & 0 deletions omlx/admin/templates/dashboard/_settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,53 @@ <h3 class="text-2xl font-bold tracking-tight text-neutral-900">{{ t('settings.gl
</div>
</div>

<!-- Network Section -->
<div class="bg-white rounded-2xl border border-neutral-200 overflow-hidden">
<div class="flex items-center justify-between px-6 py-4 bg-neutral-100 border-b border-neutral-200">
<div class="flex items-center gap-3">
<i data-lucide="network" class="w-4 h-4 text-neutral-500"></i>
<span class="text-xs font-bold uppercase tracking-wider text-neutral-600">Network</span>
</div>
<span class="px-2 py-0.5 text-[10px] font-medium rounded-full bg-green-50 text-green-600 border border-green-200">
Live
</span>
</div>
<div class="divide-y divide-neutral-100">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 px-4 sm:px-6 py-4">
<div>
<label class="text-sm text-neutral-700">HTTP Proxy</label>
<p class="text-xs text-neutral-400 mt-0.5">Example: http://proxy.company.com:8080</p>
</div>
<input type="text" x-model="globalSettings.network.http_proxy"
class="w-full sm:w-64 px-3 py-2 text-sm text-right border border-neutral-200 rounded-lg focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all font-mono text-xs">
</div>
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 px-4 sm:px-6 py-4">
<div>
<label class="text-sm text-neutral-700">HTTPS Proxy</label>
<p class="text-xs text-neutral-400 mt-0.5">Example: http://proxy.company.com:8080</p>
</div>
<input type="text" x-model="globalSettings.network.https_proxy"
class="w-full sm:w-64 px-3 py-2 text-sm text-right border border-neutral-200 rounded-lg focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all font-mono text-xs">
</div>
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 px-4 sm:px-6 py-4">
<div>
<label class="text-sm text-neutral-700">No Proxy</label>
<p class="text-xs text-neutral-400 mt-0.5">Comma-separated hosts to bypass proxy</p>
</div>
<input type="text" x-model="globalSettings.network.no_proxy"
class="w-full sm:w-64 px-3 py-2 text-sm text-right border border-neutral-200 rounded-lg focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all font-mono text-xs">
</div>
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 px-4 sm:px-6 py-4">
<div>
<label class="text-sm text-neutral-700">CA Bundle</label>
<p class="text-xs text-neutral-400 mt-0.5">Path to PEM file for corporate TLS interception</p>
</div>
<input type="text" x-model="globalSettings.network.ca_bundle"
class="w-full sm:w-64 px-3 py-2 text-sm text-right border border-neutral-200 rounded-lg focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all font-mono text-xs">
</div>
</div>
</div>

<!-- Advanced Settings (collapsible, collapsed by default) -->
<div class="bg-white rounded-2xl border border-neutral-200 overflow-hidden"
x-data="{ advancedOpen: false }">
Expand Down
2 changes: 1 addition & 1 deletion omlx/api/thinking.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ def __call__(self, tokens, logits):
# Accept each genuinely generated token exactly once (see grammar.py).
n = len(tokens)
if not hasattr(self, "_accepted_up_to"):
self._accepted_up_to = n + 1 # skip first append (prompt token)
self._accepted_up_to = n # skip first call; accept newly appended tokens thereafter
elif n > self._accepted_up_to:
for i in range(self._accepted_up_to, n):
self._update_state(int(tokens[i]))
Expand Down
33 changes: 33 additions & 0 deletions omlx/cache/boundary_snapshot_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,15 +321,48 @@ def _writer_loop(self) -> None:
if pw_key[0] in self._cancelled_requests:
with self._pending_lock:
self._pending_writes.pop(pw_key, None)
try:
req_dir = file_path.parent
if req_dir.exists():
shutil.rmtree(req_dir)
except Exception:
pass
continue

temp_path = None
try:
file_path.parent.mkdir(parents=True, exist_ok=True)
temp_path = file_path.with_name(
file_path.stem + "_tmp.safetensors"
)
_write_safetensors_no_mx(str(temp_path), tensors_raw, metadata)

# Request may have been cleaned up while serializing.
if pw_key[0] in self._cancelled_requests:
try:
if temp_path.exists():
temp_path.unlink()
except Exception:
pass
with self._pending_lock:
self._pending_writes.pop(pw_key, None)
continue

os.rename(str(temp_path), str(file_path))

# Cleanup may race with a queued write; remove any late file.
if pw_key[0] in self._cancelled_requests:
try:
if file_path.exists():
file_path.unlink()
except Exception:
pass
req_dir = file_path.parent
try:
if req_dir.exists():
shutil.rmtree(req_dir)
except Exception:
pass
except Exception as e:
logger.debug("Background snapshot write failed: %s", e)
for p in (temp_path, file_path):
Expand Down
54 changes: 54 additions & 0 deletions omlx/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,20 @@ def _has_cli_overrides(args) -> bool:
return True
if hasattr(args, "log_level") and args.log_level is not None:
return True
if hasattr(args, "mcp_config") and args.mcp_config is not None:
return True
if hasattr(args, "hf_endpoint") and args.hf_endpoint is not None:
return True
if hasattr(args, "ms_endpoint") and args.ms_endpoint is not None:
return True
if hasattr(args, "http_proxy") and args.http_proxy is not None:
return True
if hasattr(args, "https_proxy") and args.https_proxy is not None:
return True
if hasattr(args, "no_proxy") and args.no_proxy is not None:
return True
if hasattr(args, "ca_bundle") and args.ca_bundle is not None:
return True
return False


Expand Down Expand Up @@ -104,6 +118,20 @@ def serve_command(args):
if settings.modelscope.endpoint:
os.environ["MODELSCOPE_DOMAIN"] = settings.modelscope.endpoint

# Apply proxy/TLS settings if configured
if settings.network.http_proxy:
os.environ["HTTP_PROXY"] = settings.network.http_proxy
os.environ["http_proxy"] = settings.network.http_proxy
if settings.network.https_proxy:
os.environ["HTTPS_PROXY"] = settings.network.https_proxy
os.environ["https_proxy"] = settings.network.https_proxy
if settings.network.no_proxy:
os.environ["NO_PROXY"] = settings.network.no_proxy
os.environ["no_proxy"] = settings.network.no_proxy
if settings.network.ca_bundle:
os.environ["REQUESTS_CA_BUNDLE"] = settings.network.ca_bundle
os.environ["SSL_CERT_FILE"] = settings.network.ca_bundle

# Save CLI args to settings.json if non-default values provided
if _has_cli_overrides(args):
try:
Expand Down Expand Up @@ -497,6 +525,32 @@ def main():
help="Custom ModelScope Hub endpoint URL",
)

# Network options
serve_parser.add_argument(
"--http-proxy",
type=str,
default=None,
help="HTTP proxy URL (e.g., http://proxy.company.com:8080)",
)
serve_parser.add_argument(
"--https-proxy",
type=str,
default=None,
help="HTTPS proxy URL (e.g., http://proxy.company.com:8080)",
)
serve_parser.add_argument(
"--no-proxy",
type=str,
default=None,
help="Comma-separated hosts/IPs to bypass proxy (e.g., localhost,127.0.0.1)",
)
serve_parser.add_argument(
"--ca-bundle",
type=str,
default=None,
help="Path to CA bundle PEM file for TLS interception environments",
)

# Base path and auth
serve_parser.add_argument(
"--base-path",
Expand Down
Loading