Last active
June 16, 2026 17:53
-
-
Save maciekish/381fae72eb1875e413295e6685d61213 to your computer and use it in GitHub Desktop.
Run Claude Code CLI against a local LM Studio instance. Tested on macOS only.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/zsh | |
| # localclaude.sh - Claude Code launcher for real Claude vs local LM Studio | |
| # Claude Code -> local Anthropic-compatible proxy -> LM Studio /v1/messages | |
| # --- persisted choices; this script edits these lines --- | |
| LAST_PROVIDER="local" | |
| LAST_MODEL="claude-local-qwen-qwen3-6-27b" | |
| LAST_CONTEXT="65536" | |
| LM_BASE_URL="${LM_BASE_URL:-http://127.0.0.1:1234}" | |
| LM_AUTH_TOKEN="${LM_AUTH_TOKEN:-lmstudio}" | |
| PROXY_HOST="${PROXY_HOST:-127.0.0.1}" | |
| PROXY_PORT="${PROXY_PORT:-1235}" | |
| PROXY_BASE_URL="http://${PROXY_HOST}:${PROXY_PORT}" | |
| EARLY_PROXY_MODE="normal" | |
| USING_EXISTING_PROXY=0 | |
| SCRIPT_PATH="${(%):-%x}" | |
| [[ "$SCRIPT_PATH" != /* ]] && SCRIPT_PATH="$PWD/$SCRIPT_PATH" | |
| LOCALCLAUDE_CONTEXT_STATE="${LOCALCLAUDE_CONTEXT_STATE:-$HOME/.localclaude_contexts.tsv}" | |
| MODEL_ALIASES=() | |
| MODEL_REAL_IDS=() | |
| MODEL_NAMES=() | |
| MODEL_CONTEXTS=() | |
| CONTEXT_OPTIONS=() | |
| RESET=$'\033[0m' | |
| REV=$'\033[7m' | |
| CYAN=$'\033[36m' | |
| YELLOW=$'\033[33m' | |
| GREEN=$'\033[32m' | |
| DIM=$'\033[2m' | |
| cleanup() { | |
| stty sane 2>/dev/null | |
| printf "\033[?25h\033[0m" | |
| } | |
| fail() { | |
| cleanup | |
| echo "Error: $*" >&2 | |
| return 1 | |
| } | |
| abort_script() { | |
| local code="${1:-1}" | |
| cleanup | |
| return "$code" 2>/dev/null || exit "$code" | |
| } | |
| handle_interrupt() { | |
| cleanup | |
| echo | |
| echo "Cancelled." | |
| return 130 2>/dev/null || exit 130 | |
| } | |
| trap handle_interrupt INT | |
| trap cleanup EXIT TERM | |
| have() { | |
| command -v "$1" >/dev/null 2>&1 | |
| } | |
| term_cols() { | |
| local c | |
| c="$(tput cols 2>/dev/null)" | |
| [[ -z "$c" || "$c" -lt 100 ]] && c=100 | |
| echo "$c" | |
| } | |
| repeat_char() { | |
| local ch="$1" | |
| local n="$2" | |
| local out="" | |
| while (( ${#out} < n )); do out+="$ch"; done | |
| printf "%s" "${out[1,$n]}" | |
| } | |
| truncate_plain() { | |
| local s="$1" | |
| local max="$2" | |
| if (( max <= 0 )); then | |
| printf "" | |
| elif (( ${#s} <= max )); then | |
| printf "%s" "$s" | |
| elif (( max <= 1 )); then | |
| printf "…" | |
| else | |
| printf "%s…" "${s[1,$((max - 1))]}" | |
| fi | |
| } | |
| pad_plain() { | |
| local s="$1" | |
| local w="$2" | |
| local len="${#s}" | |
| if (( len >= w )); then | |
| printf "%s" "$(truncate_plain "$s" "$w")" | |
| else | |
| printf "%s%*s" "$s" $((w - len)) "" | |
| fi | |
| } | |
| fetch_models_json() { | |
| curl -fsS --max-time 3 \ | |
| -H "Authorization: Bearer ${LM_AUTH_TOKEN}" \ | |
| "${LM_BASE_URL}/api/v1/models" 2>/dev/null && return 0 | |
| return 1 | |
| } | |
| parse_models() { | |
| python3 -c ' | |
| import json | |
| import re | |
| import sys | |
| raw = sys.stdin.read() | |
| try: | |
| data = json.loads(raw) | |
| except Exception as e: | |
| print(f"JSON parse error: {e}", file=sys.stderr) | |
| sys.exit(1) | |
| def alias_for(real_id): | |
| s = real_id.lower() | |
| s = re.sub(r"[^a-z0-9]+", "-", s) | |
| s = re.sub(r"-+", "-", s).strip("-") | |
| return "claude-local-" + s | |
| out = [] | |
| if isinstance(data, dict) and isinstance(data.get("models"), list): | |
| for m in data["models"]: | |
| if not isinstance(m, dict): | |
| continue | |
| if m.get("type") != "llm": | |
| continue | |
| real = m.get("key") or m.get("id") or m.get("selected_variant") | |
| if not real: | |
| continue | |
| display = m.get("display_name") or real | |
| quant = m.get("quantization") | |
| q = quant.get("name") if isinstance(quant, dict) else None | |
| params = m.get("params_string") | |
| ctx = m.get("max_context_length") or 65536 | |
| variant = m.get("selected_variant") | |
| details = [] | |
| if params: | |
| details.append(str(params)) | |
| if q: | |
| details.append(str(q)) | |
| if ctx: | |
| details.append("ctx " + str(ctx)) | |
| if variant and variant != real: | |
| details.append("variant " + str(variant).split("@")[-1]) | |
| name = display + (" (" + ", ".join(details) + ")" if details else "") | |
| out.append((alias_for(real), real, name, str(ctx))) | |
| seen = set() | |
| for alias, real, name, ctx in out: | |
| if alias in seen: | |
| continue | |
| seen.add(alias) | |
| print(alias + "\t" + real + "\t" + name + "\t" + ctx) | |
| ' | |
| } | |
| load_models() { | |
| local json lines alias real name ctx | |
| have curl || { | |
| fail "curl is required." | |
| return 1 | |
| } | |
| have python3 || { | |
| fail "python3 is required." | |
| return 1 | |
| } | |
| json="$(fetch_models_json)" || { | |
| fail "Could not reach LM Studio at ${LM_BASE_URL}/api/v1/models using Authorization: Bearer token." | |
| return 1 | |
| } | |
| lines="$(printf "%s" "$json" | parse_models)" || { | |
| fail "Could not parse LM Studio model list." | |
| return 1 | |
| } | |
| if [[ -z "$lines" ]]; then | |
| fail "LM Studio returned no usable LLM models." | |
| return 1 | |
| fi | |
| MODEL_ALIASES=() | |
| MODEL_REAL_IDS=() | |
| MODEL_NAMES=() | |
| MODEL_CONTEXTS=() | |
| while IFS=$'\t' read -r alias real name ctx; do | |
| [[ -z "$alias" || -z "$real" ]] && continue | |
| MODEL_ALIASES+=("$alias") | |
| MODEL_REAL_IDS+=("$real") | |
| MODEL_NAMES+=("$name") | |
| MODEL_CONTEXTS+=("${ctx:-65536}") | |
| done <<< "$lines" | |
| if (( ${#MODEL_ALIASES[@]} == 0 )); then | |
| fail "Model parser produced zero usable models." | |
| return 1 | |
| fi | |
| return 0 | |
| } | |
| model_map_json() { | |
| python3 - "$@" <<'PY' | |
| import json | |
| import sys | |
| pairs = sys.argv[1:] | |
| m = {} | |
| for p in pairs: | |
| alias, real = p.split("\t", 1) | |
| m[alias] = real | |
| print(json.dumps(m, separators=(",", ":"))) | |
| PY | |
| } | |
| make_context_options() { | |
| # Kept only for compatibility with older draw/event-loop patches. | |
| # Context is now a single editable value, not a preset list. | |
| CONTEXT_OPTIONS=() | |
| } | |
| get_saved_context_for_model() { | |
| local alias="$1" | |
| local max_ctx="$2" | |
| local saved="" | |
| if [[ -f "$LOCALCLAUDE_CONTEXT_STATE" ]]; then | |
| saved="$(awk -F '\t' -v a="$alias" '$1 == a { v = $2 } END { print v }' "$LOCALCLAUDE_CONTEXT_STATE" 2>/dev/null)" | |
| fi | |
| if [[ "$saved" == <-> ]]; then | |
| (( saved > max_ctx )) && saved="$max_ctx" | |
| (( saved < 4096 )) && saved=4096 | |
| echo "$saved" | |
| else | |
| echo "$max_ctx" | |
| fi | |
| } | |
| remember_context_for_model() { | |
| local alias="$1" | |
| local ctx="$2" | |
| local tmp | |
| [[ -n "$alias" ]] || return 0 | |
| [[ "$ctx" == <-> ]] || return 0 | |
| tmp="${LOCALCLAUDE_CONTEXT_STATE}.tmp.$$" | |
| mkdir -p "$(dirname "$LOCALCLAUDE_CONTEXT_STATE")" 2>/dev/null || true | |
| if [[ -f "$LOCALCLAUDE_CONTEXT_STATE" ]]; then | |
| awk -F '\t' -v OFS='\t' -v a="$alias" -v c="$ctx" ' | |
| BEGIN { done = 0 } | |
| $1 == a { | |
| print a, c | |
| done = 1 | |
| next | |
| } | |
| { print } | |
| END { | |
| if (!done) print a, c | |
| } | |
| ' "$LOCALCLAUDE_CONTEXT_STATE" > "$tmp" | |
| else | |
| printf "%s\t%s\n" "$alias" "$ctx" > "$tmp" | |
| fi | |
| mv "$tmp" "$LOCALCLAUDE_CONTEXT_STATE" | |
| } | |
| set_context_to_model_default() { | |
| local idx="$1" | |
| local alias="${MODEL_ALIASES[$((idx + 1))]}" | |
| local max_ctx="${MODEL_CONTEXTS[$((idx + 1))]:-65536}" | |
| context_value="$(get_saved_context_for_model "$alias" "$max_ctx")" | |
| } | |
| set_context_to_model_max() { | |
| # Historical function name used by the event loop. | |
| # New behavior: saved per-model value if one exists, otherwise max. | |
| set_context_to_model_default "$1" | |
| } | |
| clamp_context_to_model() { | |
| local idx="$1" | |
| local alias="${MODEL_ALIASES[$((idx + 1))]}" | |
| local max_ctx="${MODEL_CONTEXTS[$((idx + 1))]:-65536}" | |
| # On startup/model change, prefer per-model saved context. | |
| # If no saved value exists, use model max. | |
| context_value="$(get_saved_context_for_model "$alias" "$max_ctx")" | |
| } | |
| save_choice() { | |
| local provider="$1" | |
| local model="$2" | |
| local ctx="$3" | |
| [[ -n "$SCRIPT_PATH" ]] || return 0 | |
| [[ -f "$SCRIPT_PATH" ]] || return 0 | |
| [[ -w "$SCRIPT_PATH" ]] || return 0 | |
| local tmp="${SCRIPT_PATH}.tmp.$$" | |
| awk -v p="$provider" -v m="$model" -v c="$ctx" ' | |
| BEGIN { donep = 0; donem = 0; donec = 0 } | |
| /^LAST_PROVIDER=/ && donep == 0 { | |
| gsub(/\\/,"\\\\",p) | |
| gsub(/"/,"\\\"",p) | |
| print "LAST_PROVIDER=\"" p "\"" | |
| donep = 1 | |
| next | |
| } | |
| /^LAST_MODEL=/ && donem == 0 { | |
| gsub(/\\/,"\\\\",m) | |
| gsub(/"/,"\\\"",m) | |
| print "LAST_MODEL=\"" m "\"" | |
| donem = 1 | |
| next | |
| } | |
| /^LAST_CONTEXT=/ && donec == 0 { | |
| gsub(/\\/,"\\\\",c) | |
| gsub(/"/,"\\\"",c) | |
| print "LAST_CONTEXT=\"" c "\"" | |
| donec = 1 | |
| next | |
| } | |
| { print } | |
| ' "$SCRIPT_PATH" > "$tmp" && mv "$tmp" "$SCRIPT_PATH" && chmod 700 "$SCRIPT_PATH" | |
| } | |
| load_lmstudio_model_context() { | |
| local real_model="$1" | |
| local ctx="$2" | |
| local min_ctx=32768 | |
| local resp applied tmp | |
| echo "Loading LM Studio model with context:" | |
| echo " Model: $real_model" | |
| echo " Requested context length: $ctx" | |
| echo | |
| tmp="/tmp/localclaude_load_$$.json" | |
| # Important: unload everything first. If LM Studio already has this model loaded | |
| # at a smaller context, requests may keep using that old instance. | |
| if command -v lms >/dev/null 2>&1; then | |
| lms unload --all >/dev/null 2>&1 || true | |
| fi | |
| curl -fsS --max-time 120 \ | |
| -X POST \ | |
| -H "Authorization: Bearer ${LM_AUTH_TOKEN}" \ | |
| -H "Content-Type: application/json" \ | |
| "${LM_BASE_URL}/api/v1/models/load" \ | |
| -d "{\"model\":\"${real_model}\",\"context_length\":${ctx},\"flash_attention\":true,\"echo_load_config\":true}" \ | |
| > "$tmp" 2>/dev/null | |
| if [[ $? -ne 0 ]]; then | |
| echo "REST load failed; trying lms CLI." >&2 | |
| if command -v lms >/dev/null 2>&1; then | |
| lms unload --all >/dev/null 2>&1 || true | |
| lms load "$real_model" --context-length "$ctx" --ttl 3600 >/dev/null 2>&1 || { | |
| echo "Error: LM Studio did not load $real_model with context $ctx." >&2 | |
| echo "Load it manually in LM Studio with context >= $min_ctx, then retry." >&2 | |
| return 1 | |
| } | |
| else | |
| echo "Error: REST load failed and lms CLI is not available." >&2 | |
| return 1 | |
| fi | |
| fi | |
| # Verify the REST echo if available. | |
| if [[ -s "$tmp" ]]; then | |
| applied="$(python3 - "$tmp" <<'PY2' | |
| import json, sys | |
| p = sys.argv[1] | |
| try: | |
| d = json.load(open(p)) | |
| except Exception: | |
| print("") | |
| raise SystemExit | |
| cfg = d.get("load_config") or {} | |
| def find_ctx(x): | |
| if isinstance(x, dict): | |
| for k, v in x.items(): | |
| lk = str(k).lower() | |
| if lk in ("context_length", "contextlength", "n_ctx", "context") and isinstance(v, int): | |
| return v | |
| r = find_ctx(v) | |
| if r: | |
| return r | |
| elif isinstance(x, list): | |
| for v in x: | |
| r = find_ctx(v) | |
| if r: | |
| return r | |
| return None | |
| print(find_ctx(cfg) or "") | |
| PY2 | |
| )" | |
| fi | |
| if [[ -n "$applied" ]]; then | |
| echo "LM Studio applied context length: $applied" | |
| if (( applied < min_ctx )); then | |
| echo "Error: LM Studio applied only $applied context, which is too small for Claude Code." >&2 | |
| echo "Requested: $ctx. Minimum practical: $min_ctx." >&2 | |
| echo "Try a smaller quant, reduce GPU offload, or manually reload in LM Studio with >= $min_ctx." >&2 | |
| return 1 | |
| fi | |
| if (( applied < ctx )); then | |
| echo "Warning: LM Studio applied $applied instead of requested $ctx." >&2 | |
| echo "Continuing because it is still >= $min_ctx." >&2 | |
| fi | |
| else | |
| echo "Could not verify applied context from load response; continuing after explicit load." | |
| fi | |
| return 0 | |
| } | |
| write_proxy() { | |
| local proxy_file="$1" | |
| cat > "$proxy_file" <<'PY' | |
| #!/usr/bin/env python3 | |
| import json | |
| import os | |
| import sys | |
| import urllib.error | |
| import urllib.request | |
| from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer | |
| from urllib.parse import urlparse | |
| LM_BASE_URL = os.environ.get("LM_BASE_URL", "http://127.0.0.1:1234").rstrip("/") | |
| LM_AUTH_TOKEN = os.environ.get("LM_AUTH_TOKEN", "") | |
| PROXY_HOST = os.environ.get("PROXY_HOST", "127.0.0.1") | |
| PROXY_PORT = int(os.environ.get("PROXY_PORT", "1235")) | |
| MODEL_MAP = json.loads(os.environ.get("LOCALCLAUDE_MODEL_MAP", "{}")) | |
| DEBUG = os.environ.get("LOCALCLAUDE_PROXY_DEBUG", "1") == "1" | |
| SYSTEM_PROMPT_LOG = os.environ.get( | |
| "LOCALCLAUDE_SYSTEM_PROMPT_LOG", | |
| os.path.expanduser("~/.localclaude_system_prompt.txt"), | |
| ) | |
| def log(msg): | |
| if DEBUG: | |
| print("[localclaude-proxy]", msg, file=sys.stderr, flush=True) | |
| def overwrite_system_prompt_log(payload): | |
| if not isinstance(payload, dict): | |
| return | |
| system = payload.get("system") | |
| if not system: | |
| return | |
| if isinstance(system, str): | |
| text = system | |
| else: | |
| try: | |
| text = json.dumps(system, ensure_ascii=False, indent=2) | |
| except Exception: | |
| text = str(system) | |
| tmp = SYSTEM_PROMPT_LOG + ".tmp" | |
| with open(tmp, "w", encoding="utf-8") as f: | |
| f.write(text) | |
| if not text.endswith("\n"): | |
| f.write("\n") | |
| os.replace(tmp, SYSTEM_PROMPT_LOG) | |
| log("wrote full system prompt to " + SYSTEM_PROMPT_LOG) | |
| def content_to_text(content): | |
| if content is None: | |
| return "" | |
| if isinstance(content, str): | |
| return content | |
| if isinstance(content, list): | |
| parts = [] | |
| for item in content: | |
| if isinstance(item, dict): | |
| t = item.get("type") | |
| if t == "text": | |
| parts.append(str(item.get("text", ""))) | |
| else: | |
| parts.append("[" + str(t or "block") + "]\n" + json.dumps(item, ensure_ascii=False)) | |
| else: | |
| parts.append(str(item)) | |
| return "\n\n".join([p for p in parts if p]) | |
| if isinstance(content, dict): | |
| return json.dumps(content, ensure_ascii=False) | |
| return str(content) | |
| def clean_content(content): | |
| # Preserve Anthropic content blocks. Claude Code depends on tool_use/tool_result | |
| # round-tripping as structured blocks. Flattening these to text makes the | |
| # agent stop behaving like an agent. | |
| if isinstance(content, str): | |
| return content | |
| if isinstance(content, list): | |
| cleaned = [] | |
| for block in content: | |
| if not isinstance(block, dict): | |
| cleaned.append({"type": "text", "text": str(block)}) | |
| continue | |
| btype = block.get("type") | |
| if btype in ("text", "tool_use", "tool_result", "image", "document", "thinking", "redacted_thinking"): | |
| cleaned.append(block) | |
| else: | |
| cleaned.append({ | |
| "type": "text", | |
| "text": "[" + str(btype or "unknown_block") + "]\n" + json.dumps(block, ensure_ascii=False), | |
| }) | |
| return cleaned | |
| return content_to_text(content) | |
| def normalise_payload(payload): | |
| if not isinstance(payload, dict): | |
| return payload | |
| model = payload.get("model") | |
| if model in MODEL_MAP: | |
| payload["model"] = MODEL_MAP[model] | |
| original = payload.get("messages", []) | |
| if not isinstance(original, list): | |
| return payload | |
| system_parts = [] | |
| if payload.get("system"): | |
| system_parts.append(content_to_text(payload.get("system"))) | |
| fixed = [] | |
| for msg in original: | |
| if not isinstance(msg, dict): | |
| fixed.append({"role": "user", "content": content_to_text(msg)}) | |
| continue | |
| role = msg.get("role") | |
| content = msg.get("content", "") | |
| if role == "system": | |
| system_parts.append(content_to_text(content)) | |
| continue | |
| if role == "assistant": | |
| fixed.append({"role": "assistant", "content": clean_content(content)}) | |
| continue | |
| if role == "user": | |
| fixed.append({"role": "user", "content": clean_content(content)}) | |
| continue | |
| fixed.append({ | |
| "role": "user", | |
| "content": "[Converted from role " + str(role) + "]\n" + content_to_text(content), | |
| }) | |
| if system_parts: | |
| payload["system"] = "\n\n".join(system_parts) | |
| payload["messages"] = fixed | |
| payload.pop("metadata", None) | |
| return payload | |
| class Handler(BaseHTTPRequestHandler): | |
| protocol_version = "HTTP/1.1" | |
| def send_json(self, code, obj): | |
| body = json.dumps(obj).encode("utf-8") | |
| self.send_response(code) | |
| self.send_header("Content-Type", "application/json") | |
| self.send_header("Content-Length", str(len(body))) | |
| self.end_headers() | |
| self.wfile.write(body) | |
| def do_HEAD(self): | |
| parsed = urlparse(self.path) | |
| path = parsed.path | |
| if path in ("/", "/health", "/healthz", "/v1/messages", "/v1/models"): | |
| self.send_response(200) | |
| self.send_header("Content-Length", "0") | |
| self.end_headers() | |
| return | |
| self.send_response(404) | |
| self.send_header("Content-Length", "0") | |
| self.end_headers() | |
| def do_GET(self): | |
| parsed = urlparse(self.path) | |
| path = parsed.path | |
| if path in ("/health", "/healthz"): | |
| self.send_json(200, {"ok": True}) | |
| return | |
| if path == "/v1/models": | |
| data = { | |
| "object": "list", | |
| "data": [ | |
| { | |
| "id": alias, | |
| "type": "model", | |
| "object": "model", | |
| "owned_by": "localclaude", | |
| "display_name": alias, | |
| } | |
| for alias in MODEL_MAP.keys() | |
| ], | |
| } | |
| self.send_json(200, data) | |
| return | |
| self.send_json(404, {"error": "not found", "path": self.path}) | |
| def do_POST(self): | |
| length = int(self.headers.get("Content-Length", "0") or "0") | |
| raw = self.rfile.read(length) if length else b"" | |
| parsed = urlparse(self.path) | |
| path = parsed.path | |
| if path == "/messages": | |
| path = "/v1/messages" | |
| if path != "/v1/messages": | |
| self.send_json(404, {"error": "not found", "path": self.path}) | |
| return | |
| try: | |
| payload = json.loads(raw.decode("utf-8")) | |
| except Exception as e: | |
| self.send_json(400, {"error": "invalid json", "detail": str(e)}) | |
| return | |
| overwrite_system_prompt_log(payload) | |
| before_model = payload.get("model") | |
| payload = normalise_payload(payload) | |
| after_model = payload.get("model") | |
| if DEBUG: | |
| roles = [m.get("role") for m in payload.get("messages", []) if isinstance(m, dict)] | |
| log("POST /v1/messages model " + repr(before_model) + " -> " + repr(after_model) + " roles=" + repr(roles)) | |
| body = json.dumps(payload).encode("utf-8") | |
| self.proxy_to_lmstudio("POST", LM_BASE_URL + "/v1/messages", body) | |
| def proxy_to_lmstudio(self, method, url, body): | |
| headers = { | |
| "Content-Type": "application/json", | |
| "Accept": self.headers.get("Accept", "application/json"), | |
| } | |
| if LM_AUTH_TOKEN: | |
| headers["Authorization"] = "Bearer " + LM_AUTH_TOKEN | |
| req = urllib.request.Request(url, data=body, method=method, headers=headers) | |
| try: | |
| with urllib.request.urlopen(req, timeout=None) as r: | |
| data = r.read() | |
| self.send_response(r.status) | |
| for k, v in r.headers.items(): | |
| lk = k.lower() | |
| if lk in ("connection", "transfer-encoding", "content-encoding", "content-length"): | |
| continue | |
| self.send_header(k, v) | |
| self.send_header("Content-Length", str(len(data))) | |
| self.end_headers() | |
| self.wfile.write(data) | |
| except urllib.error.HTTPError as e: | |
| data = e.read() | |
| self.send_response(e.code) | |
| for k, v in e.headers.items(): | |
| lk = k.lower() | |
| if lk in ("connection", "transfer-encoding", "content-encoding", "content-length"): | |
| continue | |
| self.send_header(k, v) | |
| self.send_header("Content-Length", str(len(data))) | |
| self.end_headers() | |
| self.wfile.write(data) | |
| except Exception as e: | |
| self.send_json(502, {"error": "proxy error", "detail": str(e)}) | |
| def log_message(self, fmt, *args): | |
| if DEBUG: | |
| super().log_message(fmt, *args) | |
| def main(): | |
| log("model map: " + json.dumps(MODEL_MAP)) | |
| server = ThreadingHTTPServer((PROXY_HOST, PROXY_PORT), Handler) | |
| print( | |
| f"localclaude proxy listening on http://{PROXY_HOST}:{PROXY_PORT}, forwarding to {LM_BASE_URL}", | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| server.serve_forever() | |
| if __name__ == "__main__": | |
| main() | |
| PY | |
| chmod 700 "$proxy_file" | |
| } | |
| proxy_health_ok() { | |
| curl -fsS --max-time 1 "${PROXY_BASE_URL}/health" >/dev/null 2>&1 | |
| } | |
| port_has_listener() { | |
| if command -v lsof >/dev/null 2>&1; then | |
| lsof -tiTCP:"$PROXY_PORT" -sTCP:LISTEN >/dev/null 2>&1 && return 0 | |
| fi | |
| if command -v nc >/dev/null 2>&1; then | |
| nc -z "$PROXY_HOST" "$PROXY_PORT" >/dev/null 2>&1 && return 0 | |
| fi | |
| return 1 | |
| } | |
| find_existing_proxy_pids() { | |
| local pids="" | |
| if command -v lsof >/dev/null 2>&1; then | |
| pids="$(lsof -tiTCP:"$PROXY_PORT" -sTCP:LISTEN 2>/dev/null || true)" | |
| fi | |
| if [[ -z "$pids" ]]; then | |
| pids="$(pgrep -f 'localclaude_proxy_.*\.py' 2>/dev/null || true)" | |
| fi | |
| echo "$pids" | |
| } | |
| kill_old_proxies() { | |
| local pids pid cmd | |
| pids="$(find_existing_proxy_pids)" | |
| for pid in ${(f)pids}; do | |
| [[ -z "$pid" ]] && continue | |
| [[ "$pid" == "$$" ]] && continue | |
| cmd="$(ps -p "$pid" -o command= 2>/dev/null || true)" | |
| # Be aggressive only with our generated proxy. Do not kill LM Studio or random services. | |
| if printf "%s" "$cmd" | grep -q 'localclaude_proxy_.*\.py'; then | |
| kill "$pid" 2>/dev/null || true | |
| fi | |
| done | |
| sleep 0.2 | |
| pids="$(find_existing_proxy_pids)" | |
| for pid in ${(f)pids}; do | |
| [[ -z "$pid" ]] && continue | |
| [[ "$pid" == "$$" ]] && continue | |
| cmd="$(ps -p "$pid" -o command= 2>/dev/null || true)" | |
| if printf "%s" "$cmd" | grep -q 'localclaude_proxy_.*\.py'; then | |
| kill -9 "$pid" 2>/dev/null || true | |
| fi | |
| done | |
| } | |
| ask_existing_proxy_action() { | |
| local pids pid cmd answer | |
| # First and most important test: does the proxy itself answer? | |
| if ! proxy_health_ok && ! port_has_listener; then | |
| echo "none" | |
| return 0 | |
| fi | |
| echo | |
| echo "Existing service detected on ${PROXY_BASE_URL}." | |
| if proxy_health_ok; then | |
| echo "It answers /health, so it looks like an existing localclaude proxy." | |
| else | |
| echo "Port ${PROXY_PORT} is occupied, but /health did not answer." | |
| fi | |
| pids="$(find_existing_proxy_pids)" | |
| if [[ -n "$pids" ]]; then | |
| echo | |
| echo "Detected listener/processes:" | |
| for pid in ${(f)pids}; do | |
| [[ -z "$pid" ]] && continue | |
| cmd="$(ps -p "$pid" -o command= 2>/dev/null || true)" | |
| echo " PID $pid: $cmd" | |
| done | |
| fi | |
| echo | |
| echo "Choose:" | |
| echo " k = kill/restart proxy and reload selected LM Studio model/context" | |
| echo " u = use existing proxy as-is, do NOT reload model/context" | |
| echo " a = abort" | |
| echo | |
| while true; do | |
| printf "Action [k/u/a]: " | |
| IFS= read -r answer | |
| case "$answer" in | |
| k|K) | |
| echo "kill" | |
| return 0 | |
| ;; | |
| u|U|"") | |
| echo "use" | |
| return 0 | |
| ;; | |
| a|A|q|Q) | |
| echo "abort" | |
| return 0 | |
| ;; | |
| *) | |
| echo "Please type k, u, or a." | |
| ;; | |
| esac | |
| done | |
| } | |
| start_proxy() { | |
| local proxy_file="/tmp/localclaude_proxy_$$.py" | |
| local proxy_log="/tmp/localclaude_proxy_$$.log" | |
| local pairs=() | |
| local map_json | |
| local action | |
| USING_EXISTING_PROXY=0 | |
| action="$(ask_existing_proxy_action)" | |
| case "$action" in | |
| use) | |
| if proxy_health_ok; then | |
| USING_EXISTING_PROXY=1 | |
| PROXY_PID="" | |
| LOCALCLAUDE_PROXY_LOG="<existing proxy>" | |
| export LOCALCLAUDE_PROXY_LOG | |
| echo | |
| echo "Using existing proxy. LM Studio model/context will be left alone." | |
| echo | |
| return 0 | |
| else | |
| echo "Cannot use existing process because ${PROXY_BASE_URL}/health does not answer." >&2 | |
| echo "Choose kill/restart instead." >&2 | |
| return 1 | |
| fi | |
| ;; | |
| kill) | |
| kill_old_proxies | |
| sleep 0.2 | |
| ;; | |
| abort) | |
| return 1 | |
| ;; | |
| none) | |
| ;; | |
| esac | |
| for i in {1..${#MODEL_ALIASES[@]}}; do | |
| pairs+=("${MODEL_ALIASES[$i]}"$'\t'"${MODEL_REAL_IDS[$i]}") | |
| done | |
| map_json="$(model_map_json "${pairs[@]}")" | |
| write_proxy "$proxy_file" | |
| LM_BASE_URL="$LM_BASE_URL" \ | |
| LM_AUTH_TOKEN="$LM_AUTH_TOKEN" \ | |
| PROXY_HOST="$PROXY_HOST" \ | |
| PROXY_PORT="$PROXY_PORT" \ | |
| LOCALCLAUDE_MODEL_MAP="$map_json" \ | |
| LOCALCLAUDE_PROXY_DEBUG="${LOCALCLAUDE_PROXY_DEBUG:-1}" \ | |
| LOCALCLAUDE_SYSTEM_PROMPT_LOG="${LOCALCLAUDE_SYSTEM_PROMPT_LOG:-$HOME/.localclaude_system_prompt.txt}" \ | |
| python3 "$proxy_file" > "$proxy_log" 2>&1 & | |
| PROXY_PID=$! | |
| sleep 0.4 | |
| if ! kill -0 "$PROXY_PID" 2>/dev/null; then | |
| # If bind failed because an old proxy exists, offer to reuse it instead of just failing. | |
| if proxy_health_ok; then | |
| echo "New proxy failed to start, but an existing proxy is healthy. Reusing it." | |
| PROXY_PID="" | |
| USING_EXISTING_PROXY=1 | |
| LOCALCLAUDE_PROXY_LOG="<existing proxy>" | |
| export LOCALCLAUDE_PROXY_LOG | |
| return 0 | |
| fi | |
| echo "Proxy failed to start. Log:" | |
| cat "$proxy_log" 2>/dev/null | |
| return 1 | |
| fi | |
| if ! proxy_health_ok; then | |
| echo "Proxy did not answer health check. Log:" | |
| cat "$proxy_log" 2>/dev/null | |
| kill "$PROXY_PID" 2>/dev/null | |
| sleep 0.1 | |
| kill -9 "$PROXY_PID" 2>/dev/null || true | |
| return 1 | |
| fi | |
| export LOCALCLAUDE_PROXY_LOG="$proxy_log" | |
| return 0 | |
| } | |
| early_existing_proxy_check() { | |
| local answer pids pid cmd | |
| # This runs BEFORE model discovery/UI. It must not touch LM Studio models. | |
| if ! curl -fsS --max-time 1 "${PROXY_BASE_URL}/health" >/dev/null 2>&1; then | |
| return 0 | |
| fi | |
| echo | |
| echo "Existing localclaude proxy is already running at ${PROXY_BASE_URL}." | |
| echo | |
| if command -v lsof >/dev/null 2>&1; then | |
| pids="$(lsof -tiTCP:"$PROXY_PORT" -sTCP:LISTEN 2>/dev/null || true)" | |
| if [[ -n "$pids" ]]; then | |
| echo "Detected listener/processes:" | |
| for pid in ${(f)pids}; do | |
| [[ -z "$pid" ]] && continue | |
| cmd="$(ps -p "$pid" -o command= 2>/dev/null || true)" | |
| echo " PID $pid: $cmd" | |
| done | |
| echo | |
| fi | |
| fi | |
| echo "Choose:" | |
| echo " u = use existing proxy now, skip model selection, do NOT reload LM Studio" | |
| echo " k = kill existing proxy and continue to model selection" | |
| echo " a = abort" | |
| echo | |
| while true; do | |
| printf "Action [u/k/a]: " | |
| IFS= read -r answer | |
| case "$answer" in | |
| u|U|"") | |
| EARLY_PROXY_MODE="use" | |
| USING_EXISTING_PROXY=1 | |
| LOCALCLAUDE_PROXY_LOG="<existing proxy>" | |
| export LOCALCLAUDE_PROXY_LOG | |
| return 0 | |
| ;; | |
| k|K) | |
| EARLY_PROXY_MODE="kill" | |
| kill_old_proxies 2>/dev/null || true | |
| # fallback kill by port if kill_old_proxies misses it | |
| if command -v lsof >/dev/null 2>&1; then | |
| pids="$(lsof -tiTCP:"$PROXY_PORT" -sTCP:LISTEN 2>/dev/null || true)" | |
| for pid in ${(f)pids}; do | |
| [[ -z "$pid" ]] && continue | |
| cmd="$(ps -p "$pid" -o command= 2>/dev/null || true)" | |
| if printf "%s" "$cmd" | grep -q 'localclaude_proxy_.*\.py'; then | |
| kill "$pid" 2>/dev/null || true | |
| sleep 0.1 | |
| kill -9 "$pid" 2>/dev/null || true | |
| fi | |
| done | |
| fi | |
| return 0 | |
| ;; | |
| a|A|q|Q) | |
| cleanup | |
| echo "Cancelled." | |
| exit 0 | |
| ;; | |
| *) | |
| echo "Please type u, k, or a." | |
| ;; | |
| esac | |
| done | |
| } | |
| launch_existing_proxy_now() { | |
| cleanup | |
| local model_alias="$LAST_MODEL" | |
| echo | |
| echo "Launching Claude Code using existing localclaude proxy:" | |
| echo " Claude Code base: $PROXY_BASE_URL" | |
| echo " LM Studio model/context: left exactly as currently loaded" | |
| if [[ -n "$model_alias" ]]; then | |
| echo " Claude Code model alias: $model_alias" | |
| else | |
| echo " Claude Code model alias: <not set; Claude Code default will be used>" | |
| fi | |
| echo | |
| if [[ -n "$model_alias" ]]; then | |
| env \ | |
| -u ANTHROPIC_API_KEY \ | |
| -u ANTHROPIC_SMALL_FAST_MODEL \ | |
| -u ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION \ | |
| -u CLAUDE_CODE_USE_BEDROCK \ | |
| -u CLAUDE_CODE_USE_VERTEX \ | |
| -u ANTHROPIC_VERTEX_PROJECT_ID \ | |
| -u ANTHROPIC_VERTEX_REGION \ | |
| -u API_TIMEOUT_MS \ | |
| -u AWS_ACCESS_KEY_ID \ | |
| -u AWS_SECRET_ACCESS_KEY \ | |
| -u AWS_SESSION_TOKEN \ | |
| -u GOOGLE_APPLICATION_CREDENTIALS \ | |
| ANTHROPIC_BASE_URL="$PROXY_BASE_URL" \ | |
| ANTHROPIC_AUTH_TOKEN="localclaude-proxy-token" \ | |
| ANTHROPIC_MODEL="$model_alias" \ | |
| CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1 \ | |
| CLAUDE_CODE_ATTRIBUTION_HEADER=0 \ | |
| CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \ | |
| DISABLE_TELEMETRY=1 \ | |
| DISABLE_LOGIN_COMMAND=1 \ | |
| claude --model "$model_alias" | |
| else | |
| env \ | |
| -u ANTHROPIC_API_KEY \ | |
| -u ANTHROPIC_SMALL_FAST_MODEL \ | |
| -u ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION \ | |
| -u CLAUDE_CODE_USE_BEDROCK \ | |
| -u CLAUDE_CODE_USE_VERTEX \ | |
| -u ANTHROPIC_VERTEX_PROJECT_ID \ | |
| -u ANTHROPIC_VERTEX_REGION \ | |
| -u API_TIMEOUT_MS \ | |
| -u AWS_ACCESS_KEY_ID \ | |
| -u AWS_SECRET_ACCESS_KEY \ | |
| -u AWS_SESSION_TOKEN \ | |
| -u GOOGLE_APPLICATION_CREDENTIALS \ | |
| ANTHROPIC_BASE_URL="$PROXY_BASE_URL" \ | |
| ANTHROPIC_AUTH_TOKEN="localclaude-proxy-token" \ | |
| CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1 \ | |
| CLAUDE_CODE_ATTRIBUTION_HEADER=0 \ | |
| CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \ | |
| DISABLE_TELEMETRY=1 \ | |
| DISABLE_LOGIN_COMMAND=1 \ | |
| claude | |
| fi | |
| exit $? | |
| } | |
| providers=("real claude" "local lm studio") | |
| provider_idx=1 | |
| if [[ "$LAST_PROVIDER" == "real" ]]; then | |
| provider_idx=0 | |
| elif [[ "$LAST_PROVIDER" == "local" ]]; then | |
| provider_idx=1 | |
| fi | |
| early_existing_proxy_check | |
| if [[ "$EARLY_PROXY_MODE" == "use" ]]; then | |
| launch_existing_proxy_now | |
| fi | |
| load_models || abort_script 1 | |
| model_idx=0 | |
| if [[ -n "$LAST_MODEL" ]]; then | |
| for i in {1..${#MODEL_ALIASES[@]}}; do | |
| if [[ "${MODEL_ALIASES[$i]}" == "$LAST_MODEL" ]]; then | |
| model_idx=$((i - 1)) | |
| break | |
| fi | |
| done | |
| fi | |
| context_value="" | |
| clamp_context_to_model "$model_idx" | |
| active_col=0 | |
| draw() { | |
| local cols provider_w context_w gap model_w line max gap_s | |
| cols="$(term_cols)" | |
| gap=3 | |
| gap_s="$(repeat_char " " "$gap")" | |
| provider_w=$(( cols / 4 )) | |
| (( provider_w < 22 )) && provider_w=22 | |
| (( provider_w > 34 )) && provider_w=34 | |
| context_w=18 | |
| model_w=$(( cols - provider_w - context_w - (gap * 2) - 2 )) | |
| (( model_w < 34 )) && model_w=34 | |
| line="$(repeat_char "─" "$cols")" | |
| printf "\033[2J\033[H\033[?25l" | |
| printf "%sClaude Code launcher%s\n\n" "$CYAN" "$RESET" | |
| printf "LM Studio: %s\n" "$LM_BASE_URL" | |
| printf "Proxy: %s -> %s\n\n" "$PROXY_BASE_URL" "$LM_BASE_URL" | |
| local h0="$DIM" h1="$DIM" h2="$DIM" | |
| (( active_col == 0 )) && h0="$YELLOW" | |
| (( active_col == 1 )) && h1="$YELLOW" | |
| (( active_col == 2 )) && h2="$YELLOW" | |
| local hp hm hc | |
| hp="$(pad_plain "Provider" "$provider_w")" | |
| hm="$(pad_plain "Local model" "$model_w")" | |
| hc="$(pad_plain "Context" "$context_w")" | |
| printf "%s%s%s%s%s%s%s%s%s\n" \ | |
| "$h0" "$hp" "$RESET" "$gap_s" \ | |
| "$h1" "$hm" "$RESET" "$gap_s" \ | |
| "$h2$hc$RESET" | |
| printf "%s\n" "$line" | |
| max=${#MODEL_ALIASES[@]} | |
| (( max < ${#providers[@]} )) && max=${#providers[@]} | |
| for ((i=0; i<max; i++)); do | |
| local p_text="" m_text="" c_text="" p_prefix=" " m_prefix=" " c_prefix=" " | |
| local p_cell="" m_cell="" c_cell="" | |
| if (( i < ${#providers[@]} )); then | |
| p_text="${providers[$((i + 1))]}" | |
| (( i == provider_idx )) && p_prefix="› " | |
| fi | |
| if (( i < ${#MODEL_ALIASES[@]} )); then | |
| m_text="${MODEL_NAMES[$((i + 1))]} [${MODEL_ALIASES[$((i + 1))]} -> ${MODEL_REAL_IDS[$((i + 1))]}]" | |
| (( i == model_idx )) && m_prefix="› " | |
| fi | |
| # Context is a single editable value, shown only on the selected model row. | |
| if (( i == model_idx )); then | |
| c_text="$context_value" | |
| (( active_col == 2 )) && c_prefix="› " | |
| fi | |
| p_text="$(truncate_plain "$p_text" $((provider_w - 2)))" | |
| m_text="$(truncate_plain "$m_text" $((model_w - 2)))" | |
| c_text="$(truncate_plain "$c_text" $((context_w - 2)))" | |
| p_cell="$(pad_plain "${p_prefix}${p_text}" "$provider_w")" | |
| m_cell="$(pad_plain "${m_prefix}${m_text}" "$model_w")" | |
| c_cell="$(pad_plain "${c_prefix}${c_text}" "$context_w")" | |
| if (( active_col == 0 && i == provider_idx )); then | |
| printf "%s%s%s%s%s%s%s\n" "$REV" "$p_cell" "$RESET" "$gap_s" "$m_cell" "$gap_s" "$c_cell" | |
| elif (( active_col == 1 && i == model_idx )); then | |
| printf "%s%s%s%s%s%s%s\n" "$p_cell" "$gap_s" "$REV" "$m_cell" "$RESET" "$gap_s" "$c_cell" | |
| elif (( active_col == 2 && i == model_idx )); then | |
| printf "%s%s%s%s%s%s%s%s%s\n" "$p_cell" "$gap_s" "$m_cell" "$gap_s" "$REV" "$c_cell" "$RESET" "" "" | |
| else | |
| printf "%s%s%s%s%s\n" "$p_cell" "$gap_s" "$m_cell" "$gap_s" "$c_cell" | |
| fi | |
| done | |
| printf "\n" | |
| if (( provider_idx == 1 )); then | |
| printf "%sSelected:%s local LM Studio through proxy\n" "$GREEN" "$RESET" | |
| printf "Claude-visible model: %s\n" "${MODEL_ALIASES[$((model_idx + 1))]}" | |
| printf "LM Studio model: %s\n" "${MODEL_REAL_IDS[$((model_idx + 1))]}" | |
| printf "Context length: %s / max %s\n" "$context_value" "${MODEL_CONTEXTS[$((model_idx + 1))]}" | |
| printf "Claude Code base URL: %s\n" "$PROXY_BASE_URL" | |
| else | |
| printf "%sSelected:%s real/default Claude Code\n" "$GREEN" "$RESET" | |
| printf "Command: claude\n" | |
| fi | |
| } | |
| read_key() { | |
| local k rest | |
| IFS= read -rs -k1 k | |
| if [[ "$k" == $'\x03' ]]; then | |
| echo CTRL_C | |
| return | |
| fi | |
| if [[ "$k" == $'\e' ]]; then | |
| IFS= read -rs -k2 -t 0.05 rest | |
| case "$rest" in | |
| "[A") echo UP ;; | |
| "[B") echo DOWN ;; | |
| "[C") echo RIGHT ;; | |
| "[D") echo LEFT ;; | |
| "") echo ESC ;; | |
| *) echo OTHER ;; | |
| esac | |
| return | |
| fi | |
| if [[ "$k" == $'\r' || "$k" == $'\n' ]]; then | |
| echo ENTER | |
| return | |
| fi | |
| if [[ "$k" == $'\x7f' || "$k" == $'\b' ]]; then | |
| echo BACKSPACE | |
| return | |
| fi | |
| case "$k" in | |
| [0-9]) | |
| echo "DIGIT_$k" | |
| return | |
| ;; | |
| q|Q) | |
| echo QUIT | |
| return | |
| ;; | |
| m|M) | |
| echo MAX_CONTEXT | |
| return | |
| ;; | |
| esac | |
| echo OTHER | |
| } | |
| launch_selected() { | |
| cleanup | |
| echo | |
| if (( provider_idx == 1 )); then | |
| local alias="${MODEL_ALIASES[$((model_idx + 1))]}" | |
| local real="${MODEL_REAL_IDS[$((model_idx + 1))]}" | |
| local ctx="$context_value" | |
| save_choice "local" "$alias" "$ctx" | |
| start_proxy || return 1 | |
| if [[ "${USING_EXISTING_PROXY:-0}" != "1" ]]; then | |
| load_lmstudio_model_context "$real" "$ctx" || return 1 | |
| remember_context_for_model "$alias" "$ctx" | |
| else | |
| echo "Skipping LM Studio model/context reload because existing proxy is being reused." | |
| echo | |
| fi | |
| echo "Launching Claude Code via LM Studio proxy:" | |
| echo " Claude Code base: $PROXY_BASE_URL" | |
| echo " Claude model alias: $alias" | |
| echo " LM Studio model: $real" | |
| echo " Context length: $ctx" | |
| echo " Proxy log: $LOCALCLAUDE_PROXY_LOG" | |
| echo | |
| env \ | |
| -u ANTHROPIC_API_KEY \ | |
| -u ANTHROPIC_SMALL_FAST_MODEL \ | |
| -u ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION \ | |
| -u CLAUDE_CODE_USE_BEDROCK \ | |
| -u CLAUDE_CODE_USE_VERTEX \ | |
| -u ANTHROPIC_VERTEX_PROJECT_ID \ | |
| -u ANTHROPIC_VERTEX_REGION \ | |
| -u API_TIMEOUT_MS \ | |
| -u AWS_ACCESS_KEY_ID \ | |
| -u AWS_SECRET_ACCESS_KEY \ | |
| -u AWS_SESSION_TOKEN \ | |
| -u GOOGLE_APPLICATION_CREDENTIALS \ | |
| ANTHROPIC_BASE_URL="$PROXY_BASE_URL" \ | |
| ANTHROPIC_AUTH_TOKEN="localclaude-proxy-token" \ | |
| ANTHROPIC_MODEL="$alias" \ | |
| CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1 \ | |
| CLAUDE_CODE_ATTRIBUTION_HEADER=0 \ | |
| CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \ | |
| DISABLE_TELEMETRY=1 \ | |
| DISABLE_LOGIN_COMMAND=1 \ | |
| claude --model "$alias" | |
| local code=$? | |
| if [[ -n "$PROXY_PID" && "${USING_EXISTING_PROXY:-0}" != "1" ]]; then | |
| kill "$PROXY_PID" 2>/dev/null | |
| wait "$PROXY_PID" 2>/dev/null | |
| fi | |
| echo | |
| echo "Proxy log was: $LOCALCLAUDE_PROXY_LOG" | |
| return "$code" | |
| else | |
| save_choice "real" "${MODEL_ALIASES[$((model_idx + 1))]}" "$context_value" | |
| echo "Launching real/default Claude Code." | |
| echo | |
| env \ | |
| -u ANTHROPIC_BASE_URL \ | |
| -u ANTHROPIC_AUTH_TOKEN \ | |
| -u ANTHROPIC_API_KEY \ | |
| -u ANTHROPIC_MODEL \ | |
| -u ANTHROPIC_SMALL_FAST_MODEL \ | |
| -u ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION \ | |
| -u CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY \ | |
| -u CLAUDE_CODE_USE_BEDROCK \ | |
| -u CLAUDE_CODE_USE_VERTEX \ | |
| -u ANTHROPIC_VERTEX_PROJECT_ID \ | |
| -u ANTHROPIC_VERTEX_REGION \ | |
| -u API_TIMEOUT_MS \ | |
| claude | |
| fi | |
| } | |
| stty -echo -icanon min 1 time 0 | |
| while true; do | |
| draw | |
| key="$(read_key)" | |
| case "$key" in | |
| LEFT) | |
| (( active_col-- )) | |
| (( active_col < 0 )) && active_col=2 | |
| ;; | |
| RIGHT) | |
| (( active_col++ )) | |
| (( active_col > 2 )) && active_col=0 | |
| ;; | |
| UP) | |
| if (( active_col == 0 )); then | |
| (( provider_idx-- )) | |
| (( provider_idx < 0 )) && provider_idx=$((${#providers[@]} - 1)) | |
| elif (( active_col == 1 )); then | |
| (( model_idx-- )) | |
| (( model_idx < 0 )) && model_idx=$((${#MODEL_ALIASES[@]} - 1)) | |
| set_context_to_model_max "$model_idx" | |
| else | |
| max_ctx="${MODEL_CONTEXTS[$((model_idx + 1))]:-65536}" | |
| step=4096 | |
| (( context_value += step )) | |
| (( context_value > max_ctx )) && context_value="$max_ctx" | |
| fi | |
| ;; | |
| DOWN) | |
| if (( active_col == 0 )); then | |
| (( provider_idx++ )) | |
| (( provider_idx >= ${#providers[@]} )) && provider_idx=0 | |
| elif (( active_col == 1 )); then | |
| (( model_idx++ )) | |
| (( model_idx >= ${#MODEL_ALIASES[@]} )) && model_idx=0 | |
| set_context_to_model_max "$model_idx" | |
| else | |
| step=4096 | |
| (( context_value -= step )) | |
| (( context_value < step )) && context_value="$step" | |
| fi | |
| ;; | |
| DIGIT_*) | |
| if (( active_col == 2 )); then | |
| d="${key#DIGIT_}" | |
| max_ctx="${MODEL_CONTEXTS[$((model_idx + 1))]:-65536}" | |
| if [[ "$context_value" == "$max_ctx" ]]; then | |
| context_value="$d" | |
| else | |
| context_value="${context_value}${d}" | |
| fi | |
| context_value="${context_value##+(0)}" | |
| [[ -z "$context_value" ]] && context_value=0 | |
| (( context_value > max_ctx )) && context_value="$max_ctx" | |
| fi | |
| ;; | |
| BACKSPACE) | |
| if (( active_col == 2 )); then | |
| context_value="${context_value[1,-2]}" | |
| [[ -z "$context_value" ]] && set_context_to_model_max "$model_idx" | |
| fi | |
| ;; | |
| MAX_CONTEXT) | |
| if (( active_col == 2 )); then | |
| set_context_to_model_max "$model_idx" | |
| fi | |
| ;; | |
| ENTER) | |
| launch_selected | |
| break | |
| ;; | |
| QUIT|ESC|CTRL_C) | |
| cleanup | |
| echo "Cancelled." | |
| break | |
| ;; | |
| esac | |
| done |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment