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.
- package/adapters/anthropic-gateway.ts +8 -2
- package/adapters/map.ts +5 -0
- package/adapters/vision-preprocess.ts +106 -0
- package/bin/ccx +34 -38
- package/install.sh +34 -38
- package/package.json +1 -1
|
@@ -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-
|
|
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
|
-
|
|
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
|
-
#
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
#
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|