claude-glm 1.0.0 → 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
@@ -12,11 +12,19 @@ Switch freely between multiple AI providers: GLM, OpenAI, Gemini, OpenRouter, an
12
12
  **🔀 In-session switching**: With ccx, switch models without restarting
13
13
  **🎯 Perfect for**: Development, testing, or when you want model flexibility
14
14
 
15
- > **Note:** This is a fork of [JoeInnsp23/claude-glm-wrapper](https://github.com/JoeInnsp23/claude-glm-wrapper) with additional features (multi-provider proxy, dangerously-skip-permissions shortcuts, etc.) that haven't been merged upstream. The `npx claude-glm-installer` command installs from the **original** repo and won't include these features. **Use the local clone method below to get the full version.**
16
-
17
15
  ## Quick Start
18
16
 
19
- ### Installation (Clone from this fork)
17
+ ### Installation (npx - Recommended)
18
+
19
+ **One command, no cloning required:**
20
+
21
+ ```bash
22
+ npx claude-glm
23
+ ```
24
+
25
+ This downloads the latest version and runs the installer directly.
26
+
27
+ ### Installation (Clone from GitHub)
20
28
 
21
29
  ```bash
22
30
  # macOS / Linux
@@ -50,7 +58,7 @@ That's it!
50
58
  ## Features
51
59
 
52
60
  - 🚀 **Easy switching** between GLM and Claude models
53
- - ⚡ **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)
54
62
  - 🔒 **No sudo/admin required**: Installs to user's home directory
55
63
  - 🖥️ **Cross-platform**: Works on Windows, macOS, and Linux
56
64
  - 📁 **Isolated configs**: Each model uses its own config directory — no conflicts!
@@ -66,9 +74,34 @@ _Note: If you don't have Node.js, you can use the platform-specific installers (
66
74
 
67
75
  ## Installation
68
76
 
69
- ### Method 1: Local Clone (Recommended)
77
+ ### Method 1: npx (Recommended - No Clone Required)
78
+
79
+ ```bash
80
+ npx claude-glm
81
+ ```
82
+
83
+ This downloads the installer from npm and runs it directly. No git clone needed.
84
+
85
+ The installer will:
86
+ - Check if Claude Code is installed
87
+ - Ask for your Z.AI API key
88
+ - Create wrapper scripts in `~/.local/bin/`
89
+ - Add aliases (`cc`, `ccg`, `ccf`, `claude-d`, `claude-glm-d`, etc.) to your shell config
90
+ - Optionally install `ccx` multi-provider proxy
91
+
92
+ After installation, **activate the changes**:
70
93
 
71
- Clone this fork to get all features including multi-provider proxy and dangerously-skip-permissions shortcuts:
94
+ ```bash
95
+ # macOS / Linux:
96
+ source ~/.zshrc # or ~/.bashrc
97
+
98
+ # Windows PowerShell:
99
+ . $PROFILE
100
+ ```
101
+
102
+ ### Method 2: Clone from GitHub
103
+
104
+ If you prefer to clone the repository or want to modify the code:
72
105
 
73
106
  #### macOS / Linux
74
107
 
@@ -88,22 +121,6 @@ cd claude-glm-wrapper
88
121
  . $PROFILE
89
122
  ```
90
123
 
91
- The installer will:
92
-
93
- - Check if Claude Code is installed
94
- - Ask for your Z.AI API key
95
- - Create wrapper scripts in `~/.local/bin/`
96
- - Add aliases (`cc`, `ccg`, `ccf`, `claude-d`, `claude-glm-d`, etc.) to your shell config
97
- - Optionally install `ccx` multi-provider proxy
98
-
99
- ### Method 2: npx
100
-
101
- ```bash
102
- npx claude-glm
103
- ```
104
-
105
- This downloads and runs the installer directly from npm — no cloning needed.
106
-
107
124
  ## Usage
108
125
 
109
126
  ### Available Commands & Aliases
@@ -127,9 +144,9 @@ The installer creates these commands and aliases:
127
144
  The `ccx` command starts a local proxy that lets you switch between multiple AI providers in a single session:
128
145
 
129
146
  - **OpenAI**: GPT-4o, GPT-4o-mini, and more
130
- - **OpenRouter**: Access to hundreds of models
147
+ - **OpenRouter**: Access to hundreds of models (including GLM-5)
131
148
  - **Google Gemini**: Gemini 1.5 Pro and Flash
132
- - **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
133
150
  - **Anthropic**: Claude 3.5 Sonnet, etc.
134
151
 
135
152
  Switch models mid-session using `/model <provider>:<model-name>`. Perfect for comparing responses or using the right model for each task!
@@ -358,6 +375,8 @@ Use Claude Code's built-in `/model` command with provider prefixes:
358
375
  | `/model glm` | `glm:glm-4.7` | Friendly GLM shortcut |
359
376
  | `/model glm47` | `glm:glm-4.7` | Explicit version |
360
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 |
361
380
  | `/model flash` | `glm:glm-4-flash` | Fast model |
362
381
  | `/model opus` | `anthropic:claude-opus-4-5-20251101` | Claude Opus (API key required) |
363
382
  | `/model sonnet` | `anthropic:claude-sonnet-4-5-20250929` | Claude Sonnet (API key required) |
@@ -372,6 +391,8 @@ Use Claude Code's built-in `/model` command with provider prefixes:
372
391
  ```typescript
373
392
  const MODEL_SHORTCUTS: Record<string, string> = {
374
393
  g: "glm:glm-4.7",
394
+ glm5: "glm:glm-5",
395
+ glm5or: "openrouter:z-ai/glm-5",
375
396
  o1: "openai:o1-preview", // Add your own!
376
397
  fast: "glm:glm-4-flash",
377
398
  // ... more shortcuts
@@ -728,7 +749,8 @@ Then reload: `. $PROFILE`
728
749
  **A**:
729
750
 
730
751
  - Use **`ccx`** for: Maximum flexibility, model comparison, leveraging different model strengths
731
- - 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
732
754
  - Use **`ccg45` (GLM-4.5)** for: Previous version, if you need consistency with older projects
733
755
  - Use **`ccf` (GLM-4.5-Air)** for: Quick questions, simple tasks, faster responses
734
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/bin/ccx CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
3
 
4
- ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
- ENV_FILE="$HOME/.claude-proxy/.env"
4
+ ROOT_DIR="$HOME/.claude-proxy"
5
+ ENV_FILE="$ROOT_DIR/.env"
6
6
  PORT="${CLAUDE_PROXY_PORT:-17870}"
7
7
 
8
8
  # Check if --setup flag is provided
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-glm",
3
- "version": "1.0.0",
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",