claude-glm 1.0.1 → 1.1.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/README.md CHANGED
@@ -58,7 +58,7 @@ That's it!
58
58
  ## Features
59
59
 
60
60
  - 🚀 **Easy switching** between GLM and Claude models
61
- - ⚡ **Multiple GLM models**: GLM-4.7 (latest), GLM-4.5, and GLM-4.5-Air (fast)
61
+ - ⚡ **Multiple GLM models**: GLM-5 (latest), GLM-4.7, GLM-4.5, and GLM-4.5-Air (fast)
62
62
  - 🔒 **No sudo/admin required**: Installs to user's home directory
63
63
  - 🖥️ **Cross-platform**: Works on Windows, macOS, and Linux
64
64
  - 📁 **Isolated configs**: Each model uses its own config directory — no conflicts!
@@ -144,9 +144,9 @@ The installer creates these commands and aliases:
144
144
  The `ccx` command starts a local proxy that lets you switch between multiple AI providers in a single session:
145
145
 
146
146
  - **OpenAI**: GPT-4o, GPT-4o-mini, and more
147
- - **OpenRouter**: Access to hundreds of models
147
+ - **OpenRouter**: Access to hundreds of models (including GLM-5)
148
148
  - **Google Gemini**: Gemini 1.5 Pro and Flash
149
- - **Z.AI GLM**: GLM-4.7, GLM-4.5, GLM-4.5-Air
149
+ - **Z.AI GLM**: GLM-5, GLM-4.7, GLM-4.5, GLM-4.5-Air
150
150
  - **Anthropic**: Claude 3.5 Sonnet, etc.
151
151
 
152
152
  Switch models mid-session using `/model <provider>:<model-name>`. Perfect for comparing responses or using the right model for each task!
@@ -375,6 +375,8 @@ Use Claude Code's built-in `/model` command with provider prefixes:
375
375
  | `/model glm` | `glm:glm-4.7` | Friendly GLM shortcut |
376
376
  | `/model glm47` | `glm:glm-4.7` | Explicit version |
377
377
  | `/model glm45` | `glm:glm-4.5` | Previous version |
378
+ | `/model glm5` | `glm:glm-5` | Latest GLM-5 model |
379
+ | `/model glm5or` | `openrouter:z-ai/glm-5` | GLM-5 via OpenRouter |
378
380
  | `/model flash` | `glm:glm-4-flash` | Fast model |
379
381
  | `/model opus` | `anthropic:claude-opus-4-5-20251101` | Claude Opus (API key required) |
380
382
  | `/model sonnet` | `anthropic:claude-sonnet-4-5-20250929` | Claude Sonnet (API key required) |
@@ -389,6 +391,8 @@ Use Claude Code's built-in `/model` command with provider prefixes:
389
391
  ```typescript
390
392
  const MODEL_SHORTCUTS: Record<string, string> = {
391
393
  g: "glm:glm-4.7",
394
+ glm5: "glm:glm-5",
395
+ glm5or: "openrouter:z-ai/glm-5",
392
396
  o1: "openai:o1-preview", // Add your own!
393
397
  fast: "glm:glm-4-flash",
394
398
  // ... more shortcuts
@@ -745,7 +749,8 @@ Then reload: `. $PROFILE`
745
749
  **A**:
746
750
 
747
751
  - Use **`ccx`** for: Maximum flexibility, model comparison, leveraging different model strengths
748
- - Use **`ccg` (GLM-4.7)** for: Latest model, complex coding, refactoring, detailed explanations
752
+ - Use **`/model glm5`** for: Latest GLM-5 with advanced agentic capabilities and long-horizon workflows
753
+ - Use **`ccg` (GLM-4.7)** for: Complex coding, refactoring, detailed explanations
749
754
  - Use **`ccg45` (GLM-4.5)** for: Previous version, if you need consistency with older projects
750
755
  - Use **`ccf` (GLM-4.5-Air)** for: Quick questions, simple tasks, faster responses
751
756
  - Use **`cc` (Claude)** for: Your regular Anthropic Claude setup
package/adapters/map.ts CHANGED
@@ -1,20 +1,33 @@
1
1
  // Provider parsing and message mapping utilities
2
- import { AnthropicMessage, AnthropicRequest, ProviderKey, ProviderModel } from "./types.js";
2
+ import {
3
+ AnthropicMessage,
4
+ AnthropicRequest,
5
+ ProviderKey,
6
+ ProviderModel,
7
+ } from "./types.js";
3
8
 
4
- const PROVIDER_PREFIXES: ProviderKey[] = ["openai", "openrouter", "gemini", "glm", "anthropic"];
9
+ const PROVIDER_PREFIXES: ProviderKey[] = [
10
+ "openai",
11
+ "openrouter",
12
+ "gemini",
13
+ "glm",
14
+ "anthropic",
15
+ ];
5
16
 
6
17
  // Model shortcuts - add your own aliases here
7
18
  const MODEL_SHORTCUTS: Record<string, string> = {
8
19
  // GLM shortcuts
9
- "g": "glm:glm-4.7",
10
- "glm": "glm:glm-4.7",
11
- "glm47": "glm:glm-4.7",
12
- "glm45": "glm:glm-4.5",
13
- "flash": "glm:glm-4-flash",
20
+ g: "glm:glm-4.7",
21
+ glm: "glm:glm-4.7",
22
+ glm47: "glm:glm-4.7",
23
+ glm45: "glm:glm-4.5",
24
+ glm5: "glm:glm-5",
25
+ glm5or: "openrouter:z-ai/glm-5",
26
+ flash: "glm:glm-4-flash",
14
27
  // Claude shortcuts (for API users)
15
- "opus": "anthropic:claude-opus-4-5-20251101",
16
- "sonnet": "anthropic:claude-sonnet-4-5-20250929",
17
- "haiku": "anthropic:claude-haiku-4-5-20251001",
28
+ opus: "anthropic:claude-opus-4-5-20251101",
29
+ sonnet: "anthropic:claude-sonnet-4-5-20250929",
30
+ haiku: "anthropic:claude-haiku-4-5-20251001",
18
31
  // Add more shortcuts as needed
19
32
  };
20
33
 
@@ -23,7 +36,10 @@ const MODEL_SHORTCUTS: Record<string, string> = {
23
36
  * Supports formats: "provider:model" or "provider/model"
24
37
  * Falls back to defaults if no valid prefix found
25
38
  */
26
- export function parseProviderModel(modelField: string, defaults?: ProviderModel): ProviderModel {
39
+ export function parseProviderModel(
40
+ modelField: string,
41
+ defaults?: ProviderModel,
42
+ ): ProviderModel {
27
43
  if (!modelField) {
28
44
  if (defaults) return defaults;
29
45
  throw new Error("Missing 'model' in request");
@@ -37,7 +53,11 @@ export function parseProviderModel(modelField: string, defaults?: ProviderModel)
37
53
  return { provider: "anthropic", model: expanded };
38
54
  }
39
55
 
40
- const sep = expanded.includes(":") ? ":" : expanded.includes("/") ? "/" : null;
56
+ const sep = expanded.includes(":")
57
+ ? ":"
58
+ : expanded.includes("/")
59
+ ? "/"
60
+ : null;
41
61
  if (!sep) {
42
62
  // no prefix: fall back to defaults or assume glm as legacy
43
63
  return defaults ?? { provider: "glm", model: expanded };
@@ -57,11 +77,16 @@ export function parseProviderModel(modelField: string, defaults?: ProviderModel)
57
77
  /**
58
78
  * Warn if tools are being used with providers that may not support them
59
79
  */
60
- export function warnIfTools(req: AnthropicRequest, provider: ProviderKey): void {
80
+ export function warnIfTools(
81
+ req: AnthropicRequest,
82
+ provider: ProviderKey,
83
+ ): void {
61
84
  if (req.tools && req.tools.length > 0) {
62
85
  // Only GLM and Anthropic support tools natively
63
86
  if (provider !== "glm" && provider !== "anthropic") {
64
- console.warn(`[proxy] Warning: ${provider} may not fully support Anthropic-style tools. Passing through anyway.`);
87
+ console.warn(
88
+ `[proxy] Warning: ${provider} may not fully support Anthropic-style tools. Passing through anyway.`,
89
+ );
65
90
  }
66
91
  }
67
92
  }
@@ -91,7 +116,7 @@ export function toPlainText(content: AnthropicMessage["content"]): string {
91
116
  export function toOpenAIMessages(messages: AnthropicMessage[]) {
92
117
  return messages.map((m) => ({
93
118
  role: m.role,
94
- content: toPlainText(m.content)
119
+ content: toPlainText(m.content),
95
120
  }));
96
121
  }
97
122
 
@@ -101,6 +126,6 @@ export function toOpenAIMessages(messages: AnthropicMessage[]) {
101
126
  export function toGeminiContents(messages: AnthropicMessage[]) {
102
127
  return messages.map((m) => ({
103
128
  role: m.role === "assistant" ? "model" : "user",
104
- parts: [{ text: toPlainText(m.content) }]
129
+ parts: [{ text: toPlainText(m.content) }],
105
130
  }));
106
131
  }
@@ -71,6 +71,7 @@ export async function chatGemini(
71
71
  }
72
72
 
73
73
  stopAnthropicMessage(res);
74
+ res.raw.end();
74
75
  }
75
76
 
76
77
  function withStatus(status: number, message: string) {
@@ -72,6 +72,7 @@ export async function chatOpenAI(
72
72
  }
73
73
 
74
74
  stopAnthropicMessage(res);
75
+ res.raw.end();
75
76
  }
76
77
 
77
78
  function withStatus(status: number, message: string) {
@@ -1,12 +1,86 @@
1
- // OpenRouter adapter (OpenAI-compatible API)
1
+ // OpenRouter adapter (OpenAI-compatible API) with full tool calling support
2
2
  import { FastifyReply } from "fastify";
3
3
  import { createParser } from "eventsource-parser";
4
- import { deltaText, startAnthropicMessage, stopAnthropicMessage } from "../sse.js";
5
- import { toOpenAIMessages } from "../map.js";
6
- import type { AnthropicRequest } from "../types.js";
4
+ import { sendEvent } from "../sse.js";
5
+ import type { AnthropicRequest, AnthropicMessage, AnthropicTool, AnthropicContentBlock } from "../types.js";
7
6
 
8
7
  const OR_BASE = process.env.OPENROUTER_BASE_URL || "https://openrouter.ai/api/v1";
9
8
 
9
+ // ── Format converters: Anthropic → OpenAI ──────────────────────────────
10
+
11
+ /** Convert Anthropic tools to OpenAI tools format */
12
+ function toOpenAITools(tools: AnthropicTool[]) {
13
+ return tools.map((t) => ({
14
+ type: "function" as const,
15
+ function: {
16
+ name: t.name,
17
+ description: t.description ?? "",
18
+ parameters: t.input_schema ?? { type: "object", properties: {} },
19
+ },
20
+ }));
21
+ }
22
+
23
+ /** Convert Anthropic messages (with tool_use/tool_result) to OpenAI messages */
24
+ function toOpenAIMessagesWithTools(messages: AnthropicMessage[]) {
25
+ const out: any[] = [];
26
+
27
+ for (const m of messages) {
28
+ if (typeof m.content === "string") {
29
+ out.push({ role: m.role, content: m.content });
30
+ continue;
31
+ }
32
+
33
+ // Complex content blocks - need to split into separate messages
34
+ const textParts: string[] = [];
35
+ const toolCalls: any[] = [];
36
+ const toolResults: any[] = [];
37
+
38
+ for (const block of m.content as AnthropicContentBlock[]) {
39
+ if (block.type === "text") {
40
+ textParts.push(block.text);
41
+ } else if (block.type === "tool_use") {
42
+ toolCalls.push({
43
+ id: block.id,
44
+ type: "function",
45
+ function: {
46
+ name: block.name,
47
+ arguments: typeof block.input === "string" ? block.input : JSON.stringify(block.input),
48
+ },
49
+ });
50
+ } else if (block.type === "tool_result") {
51
+ const content = typeof block.content === "string"
52
+ ? block.content
53
+ : JSON.stringify(block.content);
54
+ toolResults.push({
55
+ role: "tool",
56
+ tool_call_id: block.tool_use_id,
57
+ content,
58
+ });
59
+ }
60
+ }
61
+
62
+ // Assistant message with tool calls
63
+ if (m.role === "assistant" && toolCalls.length > 0) {
64
+ out.push({
65
+ role: "assistant",
66
+ content: textParts.join("") || null,
67
+ tool_calls: toolCalls,
68
+ });
69
+ } else if (textParts.length > 0) {
70
+ out.push({ role: m.role, content: textParts.join("") });
71
+ }
72
+
73
+ // Tool results become separate "tool" role messages
74
+ for (const tr of toolResults) {
75
+ out.push(tr);
76
+ }
77
+ }
78
+
79
+ return out;
80
+ }
81
+
82
+ // ── Main adapter ────────────────────────────────────────────────────────
83
+
10
84
  export async function chatOpenRouter(
11
85
  res: FastifyReply,
12
86
  body: AnthropicRequest,
@@ -20,10 +94,9 @@ export async function chatOpenRouter(
20
94
  const url = `${OR_BASE}/chat/completions`;
21
95
  const headers: Record<string, string> = {
22
96
  Authorization: `Bearer ${apiKey}`,
23
- "Content-Type": "application/json"
97
+ "Content-Type": "application/json",
24
98
  };
25
99
 
26
- // Add optional OpenRouter headers
27
100
  if (process.env.OPENROUTER_REFERER) {
28
101
  headers["HTTP-Referer"] = process.env.OPENROUTER_REFERER;
29
102
  }
@@ -31,24 +104,34 @@ export async function chatOpenRouter(
31
104
  headers["X-Title"] = process.env.OPENROUTER_TITLE;
32
105
  }
33
106
 
107
+ // Build OpenAI-format request
108
+ const hasTools = body.tools && body.tools.length > 0;
109
+ const messages = hasTools
110
+ ? toOpenAIMessagesWithTools(body.messages)
111
+ : toOpenAIMessagesWithTools(body.messages);
112
+
113
+ // Add system message if present
114
+ if (body.system) {
115
+ messages.unshift({ role: "system", content: body.system });
116
+ }
117
+
34
118
  const reqBody: any = {
35
119
  model,
36
- messages: toOpenAIMessages(body.messages),
120
+ messages,
37
121
  stream: true,
38
122
  temperature: body.temperature ?? 0.7,
39
- max_tokens: body.max_tokens
123
+ max_tokens: body.max_tokens,
40
124
  };
41
125
 
42
- // Pass through tools if provided
43
- if (body.tools && body.tools.length > 0) {
44
- console.warn("[openrouter] Tools passed through but format may not be compatible");
45
- reqBody.tools = body.tools;
126
+ if (hasTools) {
127
+ reqBody.tools = toOpenAITools(body.tools!);
128
+ console.log(`[openrouter] Sending ${body.tools!.length} tools (converted to OpenAI format)`);
46
129
  }
47
130
 
48
131
  const resp = await fetch(url, {
49
132
  method: "POST",
50
133
  headers,
51
- body: JSON.stringify(reqBody)
134
+ body: JSON.stringify(reqBody),
52
135
  });
53
136
 
54
137
  if (!resp.ok || !resp.body) {
@@ -56,7 +139,83 @@ export async function chatOpenRouter(
56
139
  throw withStatus(resp.status || 500, `OpenRouter error: ${text}`);
57
140
  }
58
141
 
59
- startAnthropicMessage(res, model);
142
+ // ── Stream response and convert back to Anthropic SSE format ──────
143
+
144
+ const msgId = `msg_${Date.now()}`;
145
+ let contentIndex = 0;
146
+ let hasStartedMessage = false;
147
+ let hasStartedThinking = false;
148
+ let hasStartedContent = false;
149
+
150
+ // Accumulate tool calls from streaming chunks
151
+ const pendingToolCalls: Record<number, { id: string; name: string; arguments: string }> = {};
152
+
153
+ function ensureMessageStarted() {
154
+ if (!hasStartedMessage) {
155
+ hasStartedMessage = true;
156
+ sendEvent(res, "message_start", {
157
+ type: "message_start",
158
+ message: {
159
+ id: msgId,
160
+ type: "message",
161
+ role: "assistant",
162
+ model,
163
+ content: [],
164
+ stop_reason: null,
165
+ stop_sequence: null,
166
+ usage: { input_tokens: 0, output_tokens: 0 },
167
+ },
168
+ });
169
+ }
170
+ }
171
+
172
+ function ensureThinkingBlockStarted() {
173
+ if (!hasStartedThinking) {
174
+ hasStartedThinking = true;
175
+ ensureMessageStarted();
176
+ sendEvent(res, "content_block_start", {
177
+ type: "content_block_start",
178
+ index: contentIndex,
179
+ content_block: { type: "thinking", thinking: "" },
180
+ });
181
+ }
182
+ }
183
+
184
+ function closeThinkingBlock() {
185
+ if (hasStartedThinking) {
186
+ sendEvent(res, "content_block_stop", {
187
+ type: "content_block_stop",
188
+ index: contentIndex,
189
+ });
190
+ contentIndex++;
191
+ hasStartedThinking = false;
192
+ }
193
+ }
194
+
195
+ function ensureContentBlockStarted() {
196
+ if (!hasStartedContent) {
197
+ // Close thinking block first if open
198
+ closeThinkingBlock();
199
+ hasStartedContent = true;
200
+ ensureMessageStarted();
201
+ sendEvent(res, "content_block_start", {
202
+ type: "content_block_start",
203
+ index: contentIndex,
204
+ content_block: { type: "text", text: "" },
205
+ });
206
+ }
207
+ }
208
+
209
+ function closeContentBlock() {
210
+ if (hasStartedContent) {
211
+ sendEvent(res, "content_block_stop", {
212
+ type: "content_block_stop",
213
+ index: contentIndex,
214
+ });
215
+ contentIndex++;
216
+ hasStartedContent = false;
217
+ }
218
+ }
60
219
 
61
220
  const reader = resp.body.getReader();
62
221
  const decoder = new TextDecoder();
@@ -66,8 +225,46 @@ export async function chatOpenRouter(
66
225
  if (!data || data === "[DONE]") return;
67
226
  try {
68
227
  const json = JSON.parse(data);
69
- const chunk = json.choices?.[0]?.delta?.content ?? "";
70
- if (chunk) deltaText(res, chunk);
228
+ const choice = json.choices?.[0];
229
+ if (!choice) return;
230
+
231
+ const delta = choice.delta;
232
+ if (!delta) return;
233
+
234
+ // Handle reasoning/thinking tokens (GLM-5 and other reasoning models)
235
+ const reasoningChunk = delta.reasoning || "";
236
+ if (reasoningChunk) {
237
+ ensureThinkingBlockStarted();
238
+ sendEvent(res, "content_block_delta", {
239
+ type: "content_block_delta",
240
+ index: contentIndex,
241
+ delta: { type: "thinking_delta", thinking: reasoningChunk },
242
+ });
243
+ }
244
+
245
+ // Handle text content
246
+ const textChunk = delta.content || "";
247
+ if (textChunk) {
248
+ ensureContentBlockStarted();
249
+ sendEvent(res, "content_block_delta", {
250
+ type: "content_block_delta",
251
+ index: contentIndex,
252
+ delta: { type: "text_delta", text: textChunk },
253
+ });
254
+ }
255
+
256
+ // Handle streaming tool calls
257
+ if (delta.tool_calls) {
258
+ for (const tc of delta.tool_calls) {
259
+ const idx = tc.index ?? 0;
260
+ if (!pendingToolCalls[idx]) {
261
+ pendingToolCalls[idx] = { id: "", name: "", arguments: "" };
262
+ }
263
+ if (tc.id) pendingToolCalls[idx].id = tc.id;
264
+ if (tc.function?.name) pendingToolCalls[idx].name += tc.function.name;
265
+ if (tc.function?.arguments) pendingToolCalls[idx].arguments += tc.function.arguments;
266
+ }
267
+ }
71
268
  } catch {
72
269
  // ignore parse errors
73
270
  }
@@ -79,7 +276,62 @@ export async function chatOpenRouter(
79
276
  parser.feed(decoder.decode(value));
80
277
  }
81
278
 
82
- stopAnthropicMessage(res);
279
+ // ── Finalize: emit tool_use blocks if any ─────────────────────────
280
+
281
+ ensureMessageStarted();
282
+
283
+ // Close any open blocks
284
+ closeThinkingBlock();
285
+ closeContentBlock();
286
+
287
+ // Emit tool_use content blocks
288
+ const toolCallEntries = Object.values(pendingToolCalls);
289
+ if (toolCallEntries.length > 0) {
290
+ for (const tc of toolCallEntries) {
291
+ // Start tool_use content block
292
+ sendEvent(res, "content_block_start", {
293
+ type: "content_block_start",
294
+ index: contentIndex,
295
+ content_block: {
296
+ type: "tool_use",
297
+ id: tc.id,
298
+ name: tc.name,
299
+ input: {},
300
+ },
301
+ });
302
+
303
+ // Send the input as a delta
304
+ sendEvent(res, "content_block_delta", {
305
+ type: "content_block_delta",
306
+ index: contentIndex,
307
+ delta: {
308
+ type: "input_json_delta",
309
+ partial_json: tc.arguments,
310
+ },
311
+ });
312
+
313
+ // Stop tool_use content block
314
+ sendEvent(res, "content_block_stop", {
315
+ type: "content_block_stop",
316
+ index: contentIndex,
317
+ });
318
+
319
+ contentIndex++;
320
+ }
321
+ }
322
+
323
+ // Determine stop reason
324
+ const stopReason = toolCallEntries.length > 0 ? "tool_use" : "end_turn";
325
+
326
+ // Send message_delta and message_stop
327
+ sendEvent(res, "message_delta", {
328
+ type: "message_delta",
329
+ delta: { stop_reason: stopReason, stop_sequence: null },
330
+ usage: { output_tokens: 0 },
331
+ });
332
+ sendEvent(res, "message_stop", { type: "message_stop" });
333
+
334
+ res.raw.end();
83
335
  }
84
336
 
85
337
  function withStatus(status: number, message: string) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-glm",
3
- "version": "1.0.1",
3
+ "version": "1.1.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",