claude-glm 1.2.3 → 1.3.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.
@@ -6,6 +6,7 @@ import { chatOpenAI } from "./providers/openai.js";
6
6
  import { chatOpenRouter } from "./providers/openrouter.js";
7
7
  import { chatGemini } from "./providers/gemini.js";
8
8
  import { passThrough } from "./providers/anthropic-pass.js";
9
+ import { preprocessImages } from "./vision-preprocess.js";
9
10
  import { config } from "dotenv";
10
11
  import { join } from "path";
11
12
  import { homedir } from "os";
@@ -28,7 +29,7 @@ fastify.get("/healthz", async () => ({
28
29
 
29
30
  // Status endpoint (shows current active provider/model)
30
31
  fastify.get("/_status", async () => {
31
- return active ?? { provider: "glm", model: "glm-4.6" };
32
+ return active ?? { provider: "glm", model: "glm-5" };
32
33
  });
33
34
 
34
35
  // Main messages endpoint - routes by model prefix
@@ -47,7 +48,10 @@ fastify.post("/v1/messages", async (req, res) => {
47
48
  // Warn if using tools with providers that may not support them
48
49
  warnIfTools(body, provider);
49
50
 
50
- active = { provider, model };
51
+ // Don't let internal Claude Code requests (haiku for titles, etc.) override the user's active model
52
+ if (provider !== "anthropic") {
53
+ active = { provider, model };
54
+ }
51
55
 
52
56
  // Validate API keys BEFORE setting headers
53
57
  if (provider === "openai") {
@@ -122,6 +126,8 @@ fastify.post("/v1/messages", async (req, res) => {
122
126
  "GLM_UPSTREAM_URL and ZAI_API_KEY not set in ~/.claude-proxy/.env. Run: ccx --setup"
123
127
  );
124
128
  }
129
+ // Convert images to text descriptions since GLM doesn't support vision
130
+ await preprocessImages(body, process.env.OPENROUTER_API_KEY);
125
131
  // Don't set headers here - passThrough will do it after validation
126
132
  return passThrough({
127
133
  res,
package/adapters/map.ts CHANGED
@@ -57,6 +57,11 @@ export function parseProviderModel(
57
57
  return { provider: "anthropic", model: expanded };
58
58
  }
59
59
 
60
+ // Auto-detect GLM models (start with "glm-") and route to glm
61
+ if (expanded.toLowerCase().startsWith("glm-")) {
62
+ return { provider: "glm", model: expanded };
63
+ }
64
+
60
65
  const sep = expanded.includes(":")
61
66
  ? ":"
62
67
  : expanded.includes("/")
@@ -0,0 +1,106 @@
1
+ // Vision preprocessing: converts image blocks to text descriptions for non-vision models
2
+ import type { AnthropicRequest } from "./types.js";
3
+
4
+ const DEFAULT_VISION_MODEL = "google/gemini-2.5-flash";
5
+ const DESCRIBE_PROMPT =
6
+ "Describe this image in granular detail — layout, text, colors, objects, spatial relationships, any code or data visible.";
7
+
8
+ interface ImageBlock {
9
+ type: "image";
10
+ source: { type: string; media_type: string; data: string; url?: string };
11
+ }
12
+
13
+ function isImageBlock(block: any): block is ImageBlock {
14
+ return block?.type === "image";
15
+ }
16
+
17
+ async function describeImage(
18
+ block: ImageBlock,
19
+ model: string,
20
+ apiKey: string
21
+ ): Promise<string> {
22
+ const content: any[] = [
23
+ { type: "text", text: DESCRIBE_PROMPT },
24
+ ];
25
+
26
+ if (block.source.type === "url" && block.source.url) {
27
+ content.push({
28
+ type: "image_url",
29
+ image_url: { url: block.source.url },
30
+ });
31
+ } else {
32
+ // base64
33
+ content.push({
34
+ type: "image_url",
35
+ image_url: {
36
+ url: `data:${block.source.media_type};base64,${block.source.data}`,
37
+ },
38
+ });
39
+ }
40
+
41
+ const resp = await fetch("https://openrouter.ai/api/v1/chat/completions", {
42
+ method: "POST",
43
+ headers: {
44
+ "Content-Type": "application/json",
45
+ Authorization: `Bearer ${apiKey}`,
46
+ },
47
+ body: JSON.stringify({
48
+ model,
49
+ messages: [{ role: "user", content }],
50
+ max_tokens: 1024,
51
+ }),
52
+ });
53
+
54
+ if (!resp.ok) {
55
+ const text = await resp.text();
56
+ console.error(`[ccx] Vision model error (${resp.status}): ${text}`);
57
+ return "[Image description unavailable]";
58
+ }
59
+
60
+ const json = (await resp.json()) as any;
61
+ return json.choices?.[0]?.message?.content?.trim() ?? "[Image description unavailable]";
62
+ }
63
+
64
+ /**
65
+ * Scans messages for image blocks and replaces them with text descriptions.
66
+ * Mutates body.messages in-place.
67
+ */
68
+ export async function preprocessImages(
69
+ body: AnthropicRequest,
70
+ apiKey?: string
71
+ ): Promise<void> {
72
+ if (!apiKey) {
73
+ console.warn("[ccx] OPENROUTER_API_KEY not set — skipping image preprocessing");
74
+ return;
75
+ }
76
+
77
+ const model = process.env.VISION_MODEL || DEFAULT_VISION_MODEL;
78
+
79
+ // Collect all image blocks with their location
80
+ const tasks: { msg: any; idx: number; block: ImageBlock }[] = [];
81
+ for (const msg of body.messages) {
82
+ if (!Array.isArray(msg.content)) continue;
83
+ for (let i = 0; i < msg.content.length; i++) {
84
+ if (isImageBlock(msg.content[i])) {
85
+ tasks.push({ msg, idx: i, block: msg.content[i] as ImageBlock });
86
+ }
87
+ }
88
+ }
89
+
90
+ if (tasks.length === 0) return;
91
+
92
+ console.log(`[ccx] Describing ${tasks.length} image(s) via ${model}...`);
93
+
94
+ const descriptions = await Promise.all(
95
+ tasks.map((t) => describeImage(t.block, model, apiKey))
96
+ );
97
+
98
+ // Replace image blocks with text descriptions (reverse order to preserve indices)
99
+ for (let i = tasks.length - 1; i >= 0; i--) {
100
+ const { msg, idx } = tasks[i];
101
+ msg.content[idx] = {
102
+ type: "text",
103
+ text: `[Image Description: ${descriptions[i]}]`,
104
+ };
105
+ }
106
+ }
package/bin/ccx CHANGED
@@ -64,58 +64,54 @@ fi
64
64
 
65
65
  export ANTHROPIC_BASE_URL="http://127.0.0.1:${PORT}"
66
66
  export ANTHROPIC_AUTH_TOKEN="${ANTHROPIC_AUTH_TOKEN:-local-proxy-token}"
67
+ export ANTHROPIC_MODEL="${ANTHROPIC_MODEL:-glm-5}"
67
68
 
68
69
  echo "[ccx] Starting Claude Code with multi-provider proxy..."
69
70
  echo "[ccx] Proxy will listen on: ${ANTHROPIC_BASE_URL}"
70
71
 
71
- # Kill any stale ccx proxy (tsx anthropic-gateway) on the same port
72
- if lsof -ti:${PORT} >/dev/null 2>&1; then
73
- STALE_PIDS=$(lsof -ti:${PORT} 2>/dev/null | while read pid; do
74
- ps -p "$pid" -o args= 2>/dev/null | grep -q "anthropic-gateway" && echo "$pid"
75
- done)
76
- if [ -n "$STALE_PIDS" ]; then
77
- echo "[ccx] Killing stale proxy on port ${PORT}..."
78
- echo "$STALE_PIDS" | xargs kill -9 2>/dev/null || true
72
+ # Check if a proxy is already running on this port
73
+ SHARED_PROXY=false
74
+ if curl -sf "http://127.0.0.1:${PORT}/healthz" >/dev/null 2>&1; then
75
+ echo "[ccx] Reusing existing proxy on port ${PORT}"
76
+ SHARED_PROXY=true
77
+ else
78
+ # Start proxy in background
79
+ npx -y tsx "${ROOT_DIR}/adapters/anthropic-gateway.ts" > /tmp/claude-proxy.log 2>&1 &
80
+ PROXY_PID=$!
81
+
82
+ cleanup() {
83
+ echo ""
84
+ echo "[ccx] Shutting down proxy..."
85
+ kill ${PROXY_PID} 2>/dev/null || true
86
+ }
87
+ trap cleanup EXIT INT TERM
88
+
89
+ # Wait for proxy to be ready (health check)
90
+ echo "[ccx] Waiting for proxy to start..."
91
+ for i in {1..30}; do
92
+ if curl -sf "http://127.0.0.1:${PORT}/healthz" >/dev/null 2>&1; then
93
+ echo "[ccx] Proxy ready!"
94
+ break
95
+ fi
96
+ if [ $i -eq 30 ]; then
97
+ echo "❌ Proxy failed to start. Check /tmp/claude-proxy.log"
98
+ cat /tmp/claude-proxy.log
99
+ exit 1
100
+ fi
79
101
  sleep 0.5
80
- fi
102
+ done
81
103
  fi
82
104
 
83
- # Start proxy in background
84
- npx -y tsx "${ROOT_DIR}/adapters/anthropic-gateway.ts" > /tmp/claude-proxy.log 2>&1 &
85
- PROXY_PID=$!
86
-
87
- cleanup() {
88
- echo ""
89
- echo "[ccx] Shutting down proxy..."
90
- kill ${PROXY_PID} 2>/dev/null || true
91
- }
92
- trap cleanup EXIT INT TERM
93
-
94
- # Wait for proxy to be ready (health check)
95
- echo "[ccx] Waiting for proxy to start..."
96
- for i in {1..30}; do
97
- if curl -sf "http://127.0.0.1:${PORT}/healthz" >/dev/null 2>&1; then
98
- echo "[ccx] Proxy ready!"
99
- break
100
- fi
101
- if [ $i -eq 30 ]; then
102
- echo "❌ Proxy failed to start. Check /tmp/claude-proxy.log"
103
- cat /tmp/claude-proxy.log
104
- exit 1
105
- fi
106
- sleep 0.5
107
- done
108
-
109
105
  echo ""
110
106
  echo "🎯 Available model prefixes:"
111
107
  echo " openai:<model> - OpenAI models (gpt-4o, gpt-4o-mini, etc.)"
112
108
  echo " openrouter:<model> - OpenRouter models"
113
109
  echo " gemini:<model> - Google Gemini models"
114
- echo " glm:<model> - Z.AI GLM models (glm-4.6, glm-4.5, etc.)"
110
+ echo " glm:<model> - Z.AI GLM models (glm-5, glm-4.7, glm-4.5, etc.)"
115
111
  echo " anthropic:<model> - Anthropic Claude models"
116
112
  echo ""
117
113
  echo "💡 Switch models in-session with: /model <prefix>:<model-name>"
118
114
  echo ""
119
115
 
120
- # Hand off to Claude Code
121
- exec claude "$@"
116
+ # Hand off to Claude Code with glm-5 as default model
117
+ exec claude --model "${ANTHROPIC_MODEL:-glm-5}" "$@"
package/install.sh CHANGED
@@ -611,61 +611,57 @@ fi
611
611
 
612
612
  export ANTHROPIC_BASE_URL="http://127.0.0.1:${PORT}"
613
613
  export ANTHROPIC_AUTH_TOKEN="${ANTHROPIC_AUTH_TOKEN:-local-proxy-token}"
614
+ export ANTHROPIC_MODEL="${ANTHROPIC_MODEL:-glm-5}"
614
615
 
615
616
  echo "[ccx] Starting Claude Code with multi-provider proxy..."
616
617
  echo "[ccx] Proxy will listen on: ${ANTHROPIC_BASE_URL}"
617
618
 
618
- # Kill any stale ccx proxy (tsx anthropic-gateway) on the same port
619
- if lsof -ti:${PORT} >/dev/null 2>&1; then
620
- STALE_PIDS=$(lsof -ti:${PORT} 2>/dev/null | while read pid; do
621
- ps -p "$pid" -o args= 2>/dev/null | grep -q "anthropic-gateway" && echo "$pid"
622
- done)
623
- if [ -n "$STALE_PIDS" ]; then
624
- echo "[ccx] Killing stale proxy on port ${PORT}..."
625
- echo "$STALE_PIDS" | xargs kill -9 2>/dev/null || true
619
+ # Check if a proxy is already running on this port
620
+ SHARED_PROXY=false
621
+ if curl -sf "http://127.0.0.1:${PORT}/healthz" >/dev/null 2>&1; then
622
+ echo "[ccx] Reusing existing proxy on port ${PORT}"
623
+ SHARED_PROXY=true
624
+ else
625
+ # Start proxy in background
626
+ npx -y tsx "${ROOT_DIR}/adapters/anthropic-gateway.ts" > /tmp/claude-proxy.log 2>&1 &
627
+ PROXY_PID=$!
628
+
629
+ cleanup() {
630
+ echo ""
631
+ echo "[ccx] Shutting down proxy..."
632
+ kill ${PROXY_PID} 2>/dev/null || true
633
+ }
634
+ trap cleanup EXIT INT TERM
635
+
636
+ # Wait for proxy to be ready (health check)
637
+ echo "[ccx] Waiting for proxy to start..."
638
+ for i in {1..30}; do
639
+ if curl -sf "http://127.0.0.1:${PORT}/healthz" >/dev/null 2>&1; then
640
+ echo "[ccx] Proxy ready!"
641
+ break
642
+ fi
643
+ if [ $i -eq 30 ]; then
644
+ echo "❌ Proxy failed to start. Check /tmp/claude-proxy.log"
645
+ cat /tmp/claude-proxy.log
646
+ exit 1
647
+ fi
626
648
  sleep 0.5
627
- fi
649
+ done
628
650
  fi
629
651
 
630
- # Start proxy in background
631
- npx -y tsx "${ROOT_DIR}/adapters/anthropic-gateway.ts" > /tmp/claude-proxy.log 2>&1 &
632
- PROXY_PID=$!
633
-
634
- cleanup() {
635
- echo ""
636
- echo "[ccx] Shutting down proxy..."
637
- kill ${PROXY_PID} 2>/dev/null || true
638
- }
639
- trap cleanup EXIT INT TERM
640
-
641
- # Wait for proxy to be ready (health check)
642
- echo "[ccx] Waiting for proxy to start..."
643
- for i in {1..30}; do
644
- if curl -sf "http://127.0.0.1:${PORT}/healthz" >/dev/null 2>&1; then
645
- echo "[ccx] Proxy ready!"
646
- break
647
- fi
648
- if [ $i -eq 30 ]; then
649
- echo "❌ Proxy failed to start. Check /tmp/claude-proxy.log"
650
- cat /tmp/claude-proxy.log
651
- exit 1
652
- fi
653
- sleep 0.5
654
- done
655
-
656
652
  echo ""
657
653
  echo "🎯 Available model prefixes:"
658
654
  echo " openai:<model> - OpenAI models (gpt-4o, gpt-4o-mini, etc.)"
659
655
  echo " openrouter:<model> - OpenRouter models"
660
656
  echo " gemini:<model> - Google Gemini models"
661
- echo " glm:<model> - Z.AI GLM models (glm-4.7, glm-4.5, etc.)"
657
+ echo " glm:<model> - Z.AI GLM models (glm-5, glm-4.7, glm-4.5, etc.)"
662
658
  echo " anthropic:<model> - Anthropic Claude models"
663
659
  echo ""
664
660
  echo "💡 Switch models in-session with: /model <prefix>:<model-name>"
665
661
  echo ""
666
662
 
667
- # Hand off to Claude Code
668
- exec claude "$@"
663
+ # Hand off to Claude Code with glm-5 as default model
664
+ exec claude --model "${ANTHROPIC_MODEL:-glm-5}" "$@"
669
665
  CCXEOF
670
666
 
671
667
  chmod +x "$wrapper_path"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-glm",
3
- "version": "1.2.3",
3
+ "version": "1.3.0",
4
4
  "description": "Cross-platform installer for Claude Code with Z.AI GLM models, multi-provider proxy, and dangerously-skip-permissions shortcuts. Run with: npx claude-glm",
5
5
  "keywords": [
6
6
  "claude",