diff --git a/claude-dev/Dockerfile b/claude-dev/Dockerfile index 5fc7d46..36d15f1 100644 --- a/claude-dev/Dockerfile +++ b/claude-dev/Dockerfile @@ -6,9 +6,11 @@ COPY required-tools.txt /opt/claude-dev/required-tools.txt COPY scripts /opt/claude-dev/scripts # LiteLLM translation proxy config +RUN mkdir -p /etc/claude-dev COPY config/litellm.yaml /etc/claude-dev/litellm.yaml COPY scripts/start-litellm.sh /opt/claude-dev/scripts/start-litellm.sh RUN chmod +x /opt/claude-dev/scripts/*.sh -CMD ["bash"] +# Start LiteLLM proxy, then bash +CMD ["sh", "-c", "DEEPSEEK_API_KEY=${DEEPSEEK_API_KEY} litellm --config /etc/claude-dev/litellm.yaml --port 4008 --host 0.0.0.0 > /tmp/litellm.log 2>&1 & exec bash"] diff --git a/claude-dev/Dockerfile.base b/claude-dev/Dockerfile.base index 9358645..8bf218a 100644 --- a/claude-dev/Dockerfile.base +++ b/claude-dev/Dockerfile.base @@ -37,56 +37,38 @@ RUN wget -q -O /usr/local/bin/tea "https://gitea.com/gitea/tea/releases/download RUN npm install -g @anthropic-ai/claude-code -# Bake LiteLLM auto-start script into the image (used by Dockerfile runtime) -COPY scripts/start-litellm.sh /opt/claude-dev/scripts/start-litellm.sh -RUN chmod +x /opt/claude-dev/scripts/start-litellm.sh - -# Preflight bypass: Claude Code tries to validate connectivity against api.anthropic.com. -# Since we use a custom proxy (LiteLLM → DeepSeek), we disable this check. -# Strategy: replace the preflight function body with a no-op return. +# Preflight bypass: replace all Anthropic URLs in the bundled binary with localhost:4008 +# (LiteLLM proxy). This is a binary-level patch since the bundled cli.js is minified. RUN python3 - <<'PY' -from pathlib import Path -import re +path = "/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js" +with open(path, "rb") as f: + d = bytearray(f.read()) -path = Path("/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js") -if not path.exists(): - raise SystemExit(f"claude cli not found: {path}") +replacements = [ + (b"https://api.anthropic.com", b"http://localhost:4008"), + (b"https://platform.claude.com", b"http://localhost:4008"), + (b"https://claude.ai", b"http://localhost:4008"), + (b"api.anthropic.com", b"localhost:4008"), + (b"platform.claude.com", b"localhost:4008"), +] +total = 0 +for old, new in replacements: + c = d.count(old) + if c: + d = d.replace(old, new) + total += c +d = d.replace(b"https://localhost:4008", b"http://localhost:4008") -content = path.read_text() +with open(path, "wb") as f: + f.write(d) -# For v2.1.x bundled binary: find and remove the preflight connectivity check -# Pattern: the function that GETs api/hello and oauth/hello endpoints -# Strategy 1: try the known old regex pattern -old_pattern = r'let A=U7\(\),q=new URL\(A\.TOKEN_URL\),K=\[`\$\{A\.BASE_API_URL\}/api/hello`,`\$\{q\.origin\}/v1/oauth/hello`\],Y=async\(_\)=>\{try\{let \$=await I8\.get\(_,\{headers:\{"User-Agent":ey\(\)\}\}\);if\(\$\.status!==200\)return\{success:!1,error:`Failed to connect to \$\{new URL\(_\)\.hostname\}: Status \$\{\$\.status\}`\};return\{success:!0\}\}catch\(\$\)\{' -replacement = 'return' -patched, count = re.subn(old_pattern, replacement, content, count=1) - -if count == 1: - path.write_text(patched) - print("Applied preflight bypass (old pattern)") -else: - # Strategy 2: more general - remove any function calling api/hello for connectivity check - print("Old pattern not found, trying broader patch...") - # Find the connectivity check function and replace it - broad_pattern = r'Failed to connect to \$\{new URL\(_\)\.hostname\}: Status' - if re.search(broad_pattern, content): - # Replace whole preflight function - match from "let A=U7" up to the catch block - content = re.sub( - r'let \w+=U7\(\)[^;]+;[^}]+Failed to connect to[^}]+}\}catch\([^}]+\)\{', - 'return', - content, - count=1 - ) - path.write_text(content) - print("Applied preflight bypass (broad pattern)") - else: - # Strategy 3: replace api/hello references with empty health endpoint - content = content.replace( - 'api/hello', - 'x-no-op' - ) - path.write_text(content) - print("Applied preflight bypass (url-replace pattern)") +# Verify +with open(path, "rb") as f: + d = f.read() +remaining = sum(d.count(p) for p in [b"api.anthropic.com", b"platform.claude.com"]) +if remaining: + raise SystemExit(f"Patch incomplete: {remaining} anthropic refs remain") +print(f"Patched {total} Anthropic URL refs → localhost:4008") PY CMD ["bash"]