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 +47 -25
- package/adapters/map.ts +41 -16
- package/adapters/providers/gemini.ts +1 -0
- package/adapters/providers/openai.ts +1 -0
- package/adapters/providers/openrouter.ts +269 -17
- package/bin/ccx +2 -2
- package/package.json +1 -1
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 (
|
|
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-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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 {
|
|
2
|
+
import {
|
|
3
|
+
AnthropicMessage,
|
|
4
|
+
AnthropicRequest,
|
|
5
|
+
ProviderKey,
|
|
6
|
+
ProviderModel,
|
|
7
|
+
} from "./types.js";
|
|
3
8
|
|
|
4
|
-
const PROVIDER_PREFIXES: ProviderKey[] = [
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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(
|
|
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(":")
|
|
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(
|
|
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(
|
|
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
|
}
|
|
@@ -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 {
|
|
5
|
-
import {
|
|
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
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
console.
|
|
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
|
-
|
|
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
|
|
70
|
-
if (
|
|
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
|
-
|
|
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="$
|
|
5
|
-
ENV_FILE="$
|
|
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.
|
|
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",
|