#!/usr/bin/env bash # ocp-connect — Lightweight client script to connect to a remote OCP instance # No dependencies beyond curl and python3 (available on most Linux/Mac systems) # # Install: # curl -fsSL https://raw.githubusercontent.com/dtzp555-max/ocp/main/ocp-connect -o ocp-connect # chmod +x ocp-connect # # Or run directly: # curl -fsSL https://raw.githubusercontent.com/dtzp555-max/ocp/main/ocp-connect | bash -s -- --key # set -euo pipefail OCP_CONNECT_VERSION="1.3.0" show_version() { echo "ocp-connect $OCP_CONNECT_VERSION" } show_help() { cat <<'EOF' ocp-connect — Connect this machine to a remote OCP (Open Claude Proxy) Configures OPENAI_BASE_URL and OPENAI_API_KEY in your shell rc file so tools like Claude Code, Cline, Aider, etc. point to the remote OCP. Usage: ocp-connect [options] Options: --port PORT Port OCP listens on (default: 3456) --key API_KEY API key (prompted interactively if remote requires auth) --version Print version and exit --help, -h Show this help Examples: ocp-connect 192.168.1.10 ocp-connect 192.168.1.10 --port 8080 ocp-connect 192.168.1.10 --key ocp_abc123 What it does: 1. Tests connectivity to the remote OCP 2. Verifies your API key (if auth is enabled) 3. Writes OPENAI_BASE_URL and OPENAI_API_KEY to ~/.bashrc or ~/.zshrc 4. Sets system-level env vars (launchctl on macOS, systemd on Linux) 5. Configures OpenClaw automatically; prints setup hints for other IDEs (Cline, Continue.dev, Cursor — manual configuration required) 6. Runs a smoke test to confirm everything works After running, reload your shell: source ~/.bashrc (or ~/.zshrc) EOF } configure_ides() { local base_url="$1" key="$2" models_out="$3" # --- OpenClaw --- local oc_config="$HOME/.openclaw/openclaw.json" local oc_found=false if command -v openclaw &>/dev/null || [[ -f "$oc_config" ]]; then oc_found=true fi if $oc_found; then echo " IDE Configuration" echo " ─────────────────────────────────────" echo " Detected: OpenClaw ($oc_config)" echo "" printf " Configure OpenClaw to use this OCP? [Y/n] " local oc_answer { read -r oc_answer /dev/null || oc_answer="y" oc_answer="${oc_answer:-y}" if [[ "$oc_answer" =~ ^[Yy]$ ]]; then # Ask for provider name printf " Provider name (models show as /model-id) [ocp]: " local provider_name { read -r provider_name /dev/null || provider_name="" provider_name="${provider_name:-ocp}" # Sanitize: only allow alphanumeric, dash, underscore provider_name=$(echo "$provider_name" | tr -cd 'a-zA-Z0-9_-') [[ -z "$provider_name" ]] && provider_name="ocp" # Ask for priority echo "" echo " How should OCP models be configured?" echo " 1) Primary — use OCP by default, keep existing models as backup" echo " 2) Backup — keep current primary, add OCP as additional option" echo "" printf " Choice [1]: " local priority_choice { read -r priority_choice /dev/null || priority_choice="1" priority_choice="${priority_choice:-1}" echo "" echo " Writing OpenClaw config..." # Use python3 to safely manipulate JSON local py_ok=0 python3 - "$oc_config" "$base_url" "$key" "$provider_name" "$priority_choice" "$models_out" <<'PYEOF' && py_ok=1 import sys, json, os config_path = sys.argv[1] base_url = sys.argv[2] api_key = sys.argv[3] provider_name = sys.argv[4] priority = sys.argv[5] # "1" = primary, "2" = backup models_json_str = sys.argv[6] # Parse models from OCP /v1/models response try: models_data = json.loads(models_json_str) model_ids = [m["id"] for m in models_data.get("data", [])] except: model_ids = ["claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4"] # Build provider entry provider = { "baseUrl": base_url + "/v1", "api": "openai-completions", "authHeader": bool(api_key), "models": [] } # Model metadata mapping (prefix match for versioned IDs like claude-haiku-4-5-20251001) model_meta = { "claude-opus-4": {"name": "Claude Opus (OCP)", "reasoning": True, "maxTokens": 16384}, "claude-sonnet-4": {"name": "Claude Sonnet (OCP)", "reasoning": True, "maxTokens": 16384}, "claude-haiku-4": {"name": "Claude Haiku (OCP)", "reasoning": False, "maxTokens": 8192}, } def get_model_meta(mid): """Match model metadata by prefix.""" for prefix, meta in sorted(model_meta.items(), key=lambda x: -len(x[0])): if mid.startswith(prefix): return meta return {"name": mid + " (OCP)", "reasoning": False, "maxTokens": 8192} for mid in model_ids: meta = get_model_meta(mid) provider["models"].append({ "id": mid, "name": meta["name"], "reasoning": meta["reasoning"], "input": ["text"], "cost": {"input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0}, "contextWindow": 200000, "maxTokens": meta["maxTokens"] }) # Load or create config if os.path.exists(config_path): with open(config_path, "r") as f: config = json.load(f) else: os.makedirs(os.path.dirname(config_path), exist_ok=True) config = {} # Ensure models.providers exists config.setdefault("models", {}) config["models"].setdefault("mode", "merge") config["models"].setdefault("providers", {}) # Remove any previous OCP provider with the same name config["models"]["providers"][provider_name] = provider # Set up auth profile if key is provided if api_key: config.setdefault("auth", {}) config["auth"].setdefault("profiles", {}) config["auth"]["profiles"][provider_name + ":default"] = { "provider": provider_name, "mode": "api_key" } # Configure agent defaults — add model aliases config.setdefault("agents", {}) config["agents"].setdefault("defaults", {}) config["agents"]["defaults"].setdefault("models", {}) # Build alias map (prefix match) alias_prefixes = { "claude-opus-4": "Claude Opus", "claude-sonnet-4": "Claude Sonnet", "claude-haiku-4": "Claude Haiku", } for mid in model_ids: full_id = provider_name + "/" + mid alias = mid # fallback for prefix, name in sorted(alias_prefixes.items(), key=lambda x: -len(x[0])): if mid.startswith(prefix): alias = name break config["agents"]["defaults"]["models"][full_id] = {"alias": alias} # Handle primary/backup if priority == "1": # OCP as primary — pick the best model (prefer sonnet for daily use) primary_model = provider_name + "/claude-sonnet-4-6" if "claude-sonnet-4-6" in model_ids else provider_name + "/" + model_ids[0] config["agents"]["defaults"].setdefault("model", {}) config["agents"]["defaults"]["model"]["primary"] = primary_model # Keep existing fallbacks config["agents"]["defaults"]["model"].setdefault("fallbacks", []) # If backup (priority == "2"), don't change the primary — just add models to the list # Update agent list entries that use old provider name patterns # (only update if agents.list exists and has entries using old OCP-like providers) if "list" in config.get("agents", {}): for agent in config["agents"]["list"]: agent_model = agent.get("model", {}) if priority == "1" and agent_model.get("primary", "").startswith("claude-local/"): # Migrate from claude-local to new provider old_model_id = agent_model["primary"].split("/", 1)[1] if old_model_id in model_ids: agent_model["primary"] = provider_name + "/" + old_model_id # Also update subagents if they use claude-local sub = agent.get("subagents", config["agents"]["defaults"].get("subagents", {})) # Don't modify subagents in agent entries — they inherit from defaults # Update defaults subagents model if using claude-local if priority == "1": sub = config["agents"]["defaults"].get("subagents", {}) if sub.get("model", "").startswith("claude-local/"): old_id = sub["model"].split("/", 1)[1] if old_id in model_ids: sub["model"] = provider_name + "/" + old_id with open(config_path, "w") as f: json.dump(config, f, indent=2, ensure_ascii=False) f.write("\n") # === B1 fix: seed per-agent auth-profiles.json for OpenClaw multi-agent setups === # OpenClaw's per-agent auth loader reads /auth-profiles.json (NOT the # root openclaw.json's auth.profiles section). Without a real key in each agent's # agentDir, OpenClaw rejects the lane with "No API key found for provider X". # See https://github.com/dtzp555-max/ocp/issues/12 for the full investigation. _seeded = [] _failed = [] _skipped_anonymous = [] _profile_key = provider_name + ":default" # OpenClaw stores per-agent auth in /agents//agent/ when # the agent has no explicit `agentDir` field — derive that path so the # default `main` agent (which never sets agentDir) is also seeded. _openclaw_root = os.path.dirname(config_path) for _agent in config.get("agents", {}).get("list", []): _agent_dir = _agent.get("agentDir") if not _agent_dir: _agent_id = _agent.get("id") if not _agent_id: continue _agent_dir = os.path.join(_openclaw_root, "agents", _agent_id, "agent") if not api_key: # Anonymous mode is incompatible with OpenClaw per-agent auth (empty key # is dropped by OpenClaw's pi-auth-credentials). Per OCP issue #12 §14 # decision: take Path C (require --key for OpenClaw multi-agent setups). _skipped_anonymous.append(_agent_dir) continue _profiles_path = os.path.join(_agent_dir, "auth-profiles.json") try: os.makedirs(_agent_dir, exist_ok=True) except OSError as _e: _failed.append((_profiles_path, "mkdir: " + str(_e))) continue if os.path.exists(_profiles_path): try: with open(_profiles_path) as _f: _ap = json.load(_f) except json.JSONDecodeError: # Corrupted JSON — back up and rebuild from scratch. _bak = _profiles_path + ".bak" try: os.rename(_profiles_path, _bak) print(f" ⚠ {_profiles_path} was corrupt; backed up to {_bak}") except OSError: pass _ap = {"version": 1, "profiles": {}} except OSError as _e: # Read failure (permissions, disk) — skip to AVOID clobbering # the user's existing profile (which may hold other providers' keys). _failed.append((_profiles_path, "read: " + str(_e))) continue else: _ap = {"version": 1, "profiles": {}} _ap.setdefault("version", 1) _ap.setdefault("profiles", {}) _ap["profiles"][_profile_key] = { "type": "api_key", "provider": provider_name, "key": api_key } try: with open(_profiles_path, "w") as _f: json.dump(_ap, _f, indent=2, ensure_ascii=False) _f.write("\n") os.chmod(_profiles_path, 0o600) _seeded.append(_profiles_path) except OSError as _e: _failed.append((_profiles_path, "write: " + str(_e))) # Report — all three sections are independent (mixed scenarios are surfaced). if _seeded: print(f" ✓ Per-agent auth profile seeded ({len(_seeded)}):") for _p in _seeded: print(f" • {_p}") if _failed: print(f" ⚠ Per-agent auth profile FAILED ({len(_failed)}):") for _p, _err in _failed: print(f" • {_p}: {_err}") print(" OpenClaw will report \"No API key found\" for the failed agents.") print(" Fix the underlying error (permissions / disk) and re-run ocp-connect.") if _skipped_anonymous: print(f" ⚠ OpenClaw multi-agent mode detected ({len(_skipped_anonymous)} agents).") print(" Anonymous mode does not work for OpenClaw per-agent auth.") print(" Re-run with: ocp-connect --key ocp_xxx") PYEOF if [[ $py_ok -eq 1 ]]; then echo " ✓ OpenClaw configured" echo " Provider: $provider_name" echo " Models:" # List models from the already-fetched models_out echo "$models_out" | python3 -c " import sys,json d=json.loads(sys.stdin.read()) pn='$provider_name' for m in d.get('data',[]): print(' • ' + pn + '/' + m['id']) " 2>/dev/null if [[ "$priority_choice" == "1" ]]; then echo " Priority: PRIMARY (default model)" else echo " Priority: BACKUP (available in model selector)" fi echo "" echo " Restart OpenClaw to apply: openclaw gateway restart" else echo " ⚠ Failed to write OpenClaw config. You can configure it manually." fi else echo " Skipped OpenClaw configuration." fi echo "" fi # --- Other IDEs: print manual instructions --- local other_ides_shown=false # Collect VS Code extension list once (reused by Cline and Continue.dev checks) local _vscode_exts="" if command -v code &>/dev/null; then _vscode_exts=$(code --list-extensions 2>/dev/null || true) fi if [[ -z "$_vscode_exts" && -d "$HOME/.vscode/extensions" ]]; then _vscode_exts=$(ls "$HOME/.vscode/extensions/" 2>/dev/null || true) fi # Extract model IDs from /v1/models response for use in hints local _model_ids _model_ids=$(echo "$models_out" | python3 -c " import sys,json try: d=json.loads(sys.stdin.read()) print(', '.join(m['id'] for m in d.get('data', []))) except Exception: print('claude-sonnet-4-6, claude-opus-4-6, claude-haiku-4-5-20251001') " 2>/dev/null || echo "claude-sonnet-4-6, claude-opus-4-6, claude-haiku-4-5-20251001") # Key display: truncate if longer than 16 chars to avoid screenshot leakage, # show explicit "(none)" in anonymous mode so users don't paste blank fields. local _key_display if [[ -z "$key" ]]; then _key_display="(none — anonymous mode; most external IDEs require a non-empty API Key)" elif [[ ${#key} -gt 16 ]]; then _key_display="${key:0:8}...${key: -4}" else _key_display="$key" fi # Detect Cline (VS Code extension saoudrizwan.claude-dev, see issue #12) if echo "$_vscode_exts" | grep -qiE 'cline|saoudrizwan\.claude-dev'; then if ! $other_ides_shown; then echo " Other IDEs detected:" other_ides_shown=true fi echo " • Cline: VSCode → Cline panel → Settings → API Provider = \"OpenAI Compatible\"" echo " Base URL: $base_url/v1" echo " API Key: $_key_display" echo " Model ID: $_model_ids" fi # Detect Continue.dev (extension ID continue.continue or config file, see issue #12) if echo "$_vscode_exts" | grep -qi 'continue\.continue' \ || [[ -f "$HOME/.continue/config.yaml" ]] \ || [[ -f "$HOME/.continue/config.json" ]]; then if ! $other_ides_shown; then echo " Other IDEs detected:" other_ides_shown=true fi echo " • Continue.dev: edit ~/.continue/config.yaml — add under \`models:\` (top-level key):" echo " models:" echo " - name: OCP Sonnet" echo " provider: openai" echo " model: claude-sonnet-4-6" echo " apiBase: $base_url/v1" echo " apiKey: $_key_display" echo " (other model IDs: $_model_ids)" fi # Detect Cursor (command, ~/.cursor dir, or /Applications/Cursor.app, see issue #12) if command -v cursor &>/dev/null \ || [[ -d "$HOME/.cursor" ]] \ || [[ -d "/Applications/Cursor.app" ]]; then if ! $other_ides_shown; then echo " Other IDEs detected:" other_ides_shown=true fi echo " • Cursor: Cmd+Shift+P → 'Cursor Settings' → Models →" echo " OpenAI API Key: $_key_display" echo " Override OpenAI Base URL: $base_url/v1" echo " Custom OpenAI Models: $_model_ids" fi # Detect opencode (https://opencode.ai, SST team CLI, see issue #12) if command -v opencode &>/dev/null \ || [[ -x "$HOME/.opencode/bin/opencode" ]] \ || [[ -d "$HOME/.local/share/opencode" ]]; then if ! $other_ides_shown; then echo " Other IDEs detected:" other_ides_shown=true fi echo " • opencode: not yet auto-configured by ocp-connect (PR follow-up)." echo " Run \`opencode providers login openai\` and provide:" echo " Base URL: $base_url/v1" echo " API Key: $_key_display" echo " Available model IDs: $_model_ids" fi if $other_ides_shown; then echo "" fi } main() { local host="" port=3456 key="" # Parse args while [[ $# -gt 0 ]]; do case "$1" in --port) port="${2:?'--port requires a value'}"; shift 2 ;; --key) key="${2:?'--key requires a value'}" [[ -z "$key" ]] && { echo "Error: --key cannot be empty (omit --key entirely for anonymous mode)"; exit 1; } shift 2 ;; --version) show_version; exit 0 ;; --help|-h) show_help; exit 0 ;; -*) echo "Unknown option: $1"; show_help; exit 1 ;; *) host="$1"; shift ;; esac done if [[ -z "$host" ]]; then echo "Error: host IP is required." echo "" show_help exit 1 fi if ! [[ "$host" =~ ^[a-zA-Z0-9._-]+$ ]]; then echo "Error: invalid host '$host'" exit 1 fi # Check dependencies for cmd in curl python3; do if ! command -v "$cmd" &>/dev/null; then echo "Error: '$cmd' is required but not found." exit 1 fi done local base_url="http://$host:$port" echo "OCP Connect v$OCP_CONNECT_VERSION" echo "─────────────────────────────────────" echo " Remote: $base_url" echo "" # Step 1: Test connectivity via /health echo " Checking connectivity..." local health_json health_json=$(curl -sf --max-time 10 "$base_url/health" 2>/dev/null) || { echo " ✗ Cannot reach $base_url/health" echo " Make sure OCP is running on $host and bound to 0.0.0.0 (LAN mode)." exit 1 } echo " ✓ Connected" echo "" # Step 2: Show remote info local remote_version auth_mode remote_version=$(echo "$health_json" | python3 -c "import sys,json; d=json.loads(sys.stdin.read()); print(d.get('version','?'))" 2>/dev/null || echo "?") auth_mode=$(echo "$health_json" | python3 -c "import sys,json; d=json.loads(sys.stdin.read()); print(d.get('authMode','none'))" 2>/dev/null || echo "none") echo " Remote OCP v$remote_version (auth: $auth_mode)" echo "" # Step 2.5: auto-discover anonymous key from /health (issue #12 §14 Path A). # When the OCP admin set PROXY_ANONYMOUS_KEY, the server advertises it via # /health.anonymousKey. If the user didn't pass --key, use it automatically so # `ocp-connect ` works zero-config for OpenClaw multi-agent setups. if [[ -z "$key" ]]; then local anon_key anon_key=$(echo "$health_json" | python3 -c " import sys, json try: d = json.loads(sys.stdin.read()) k = d.get('anonymousKey') except Exception: k = None print(k if k else '') " 2>/dev/null || echo "") if [[ -n "$anon_key" ]]; then key="$anon_key" local _anon_display="$anon_key" if [[ ${#anon_key} -gt 16 ]]; then _anon_display="${anon_key:0:8}...${anon_key: -4}" fi echo " ⓘ Using server-advertised anonymous key: $_anon_display" echo " (set by admin via PROXY_ANONYMOUS_KEY; see issue #12 §14 Path A)" echo "" fi fi # Step 3: Determine if key is needed if [[ "$auth_mode" != "none" && -z "$key" ]]; then # Try anonymous access first (zero-config: server may allow it) if curl -sf --max-time 5 "$base_url/v1/models" >/dev/null 2>&1; then echo " Server allows anonymous access — no key needed." echo "" else echo " Remote requires authentication." echo " Ask the OCP admin to run: ocp keys add " printf " Enter API key (or press Enter to skip): " { read -rs key /dev/null || key="" echo if [[ -z "$key" ]]; then echo "" echo " ✗ No key provided — cannot connect to an auth-required remote without a key." echo " Use: ocp-connect $host --key " exit 1 fi fi fi # Step 4: Test API access via /v1/models echo " Testing API access..." local models_out models_ok=0 if [[ -n "$key" ]]; then models_out=$(curl -sf --max-time 10 \ -H "Authorization: Bearer $key" \ "$base_url/v1/models" 2>/dev/null) && models_ok=1 else models_out=$(curl -sf --max-time 10 "$base_url/v1/models" 2>/dev/null) && models_ok=1 fi if [[ $models_ok -eq 0 ]]; then echo " ✗ API access failed — key may be invalid or revoked." exit 1 fi local model_count model_count=$(echo "$models_out" | python3 -c "import sys,json; print(len(json.loads(sys.stdin.read()).get('data',[])))" 2>/dev/null || echo "?") echo " ✓ API accessible ($model_count models available)" echo "" # Step 5: Detect shell rc files and OS local rc_files=() local is_mac=false [[ "$(uname)" == "Darwin" ]] && is_mac=true # Write to all relevant rc files if [[ "${SHELL:-}" == */fish ]]; then echo " Note: fish shell detected. Writing to ~/.bashrc — add to fish config manually." rc_files+=("$HOME/.bashrc") else # Always write both on macOS (default shell is zsh but some tools source bashrc) [[ -f "$HOME/.bashrc" || "${SHELL:-}" == */bash ]] && rc_files+=("$HOME/.bashrc") [[ -f "$HOME/.zshrc" || "${SHELL:-}" == */zsh ]] && rc_files+=("$HOME/.zshrc") # If neither exists, create for current shell [[ ${#rc_files[@]} -eq 0 ]] && rc_files+=("$HOME/.${SHELL##*/}rc") fi # Step 6: Remove any previously written OCP LAN lines (idempotent) from all rc files for rc_file in "${rc_files[@]}"; do if [[ -f "$rc_file" ]]; then local tmp_rc tmp_rc=$(mktemp) if python3 - "$rc_file" "$tmp_rc" <<'PYEOF' import sys src, dst = sys.argv[1], sys.argv[2] lines = open(src).readlines() out = [] skip = False for line in lines: s = line.rstrip('\n') if s == '# OCP LAN (added by ocp connect)': skip = True continue if skip and (s == '' or s.startswith('export OPENAI_BASE_URL=') or s.startswith('export OPENAI_API_KEY=')): continue skip = False out.append(line) open(dst, 'w').writelines(out) PYEOF then cp "$tmp_rc" "$rc_file" fi rm -f "$tmp_rc" fi done # Step 7: Append new config to all rc files for rc_file in "${rc_files[@]}"; do { echo "" echo "# OCP LAN (added by ocp connect)" echo "export OPENAI_BASE_URL=$base_url/v1" if [[ -n "$key" ]]; then echo "export OPENAI_API_KEY=$key" fi } >> "$rc_file" done echo " Shell config:" for rc_file in "${rc_files[@]}"; do echo " ✓ $(basename "$rc_file")" done echo " OPENAI_BASE_URL=$base_url/v1" if [[ -n "$key" ]]; then echo " OPENAI_API_KEY=${key:0:8}..." fi # Step 7b: System-level env vars (for IDEs, daemons, GUI apps) if $is_mac; then # macOS: launchctl setenv makes vars visible to all GUI apps and launchd services launchctl setenv OPENAI_BASE_URL "$base_url/v1" 2>/dev/null if [[ -n "$key" ]]; then launchctl setenv OPENAI_API_KEY "$key" 2>/dev/null fi echo "" echo " System-level (launchctl):" echo " ✓ OPENAI_BASE_URL set for GUI apps and daemons" echo " Note: launchctl vars reset on reboot. Add to Login Items or re-run ocp-connect." else # Linux: write to environment.d for systemd user services local env_dir="$HOME/.config/environment.d" mkdir -p "$env_dir" 2>/dev/null { echo "OPENAI_BASE_URL=$base_url/v1" if [[ -n "$key" ]]; then echo "OPENAI_API_KEY=$key" fi } > "$env_dir/ocp.conf" echo "" echo " System-level (systemd):" echo " ✓ $env_dir/ocp.conf" echo " Applies to systemd user services after re-login." fi echo "" # Step 7c: Interactive IDE configuration configure_ides "$base_url" "$key" "$models_out" # Step 8: Quick smoke test echo " Running smoke test..." # Pick the first available model from /v1/models local smoke_model smoke_model=$(echo "$models_out" | python3 -c "import sys,json; d=json.loads(sys.stdin.read()); print(d['data'][0]['id'])" 2>/dev/null || echo "claude-haiku-4-5-20251001") local chat_payload="{\"model\":\"$smoke_model\",\"messages\":[{\"role\":\"user\",\"content\":\"Reply with OK only.\"}],\"max_tokens\":10}" local chat_out chat_ok=0 if [[ -n "$key" ]]; then chat_out=$(curl -sf --max-time 30 \ -H "Authorization: Bearer $key" \ -H "Content-Type: application/json" \ -d "$chat_payload" \ "$base_url/v1/chat/completions" 2>/dev/null) && chat_ok=1 else chat_out=$(curl -sf --max-time 30 \ -H "Content-Type: application/json" \ -d "$chat_payload" \ "$base_url/v1/chat/completions" 2>/dev/null) && chat_ok=1 fi if [[ $chat_ok -eq 1 ]]; then local reply reply=$(echo "$chat_out" | python3 -c "import sys,json; d=json.loads(sys.stdin.read()); print(d['choices'][0]['message']['content'].strip())" 2>/dev/null || echo "(response received)") echo " ✓ Smoke test passed: $reply" echo " Note: smoke test only verifies OCP is reachable and the key is valid." echo " It does not verify your IDE/agent end-to-end. To verify OpenClaw works," echo " restart it (\`openclaw gateway restart\`) and send a test message to your bot." else echo " ⚠ Smoke test failed (proxy is reachable but chat completion did not succeed)." echo " The env vars have still been written. Check the remote OCP logs." fi echo "" echo " Done. Reload your shell to apply:" echo " source $rc_file" } main "$@"