Skip to content

Instantly share code, notes, and snippets.

@vitobotta
Last active October 27, 2025 16:06
Show Gist options
  • Select an option

  • Save vitobotta/2be3f33722e05e8d4f9d2b0138b8c863 to your computer and use it in GitHub Desktop.

Select an option

Save vitobotta/2be3f33722e05e8d4f9d2b0138b8c863 to your computer and use it in GitHub Desktop.
class ChutesGLMTransformer {
constructor(options = {}) {
this.name = "chutes-glm";
this.endPoint = "/v1/chat/completions";
this.enable = options.enable ?? true;
}
async auth(request, provider) {
return {
body: request,
config: {
headers: {
...this.normalizeRequestHeaders(provider?.headers || {}),
Authorization: `Bearer ${provider.apiKey || "sk-no-key-required"}`,
"Content-Type": "application/json",
},
},
};
}
normalizeRequestHeaders(headers) {
const out = {};
for (const [key, value] of Object.entries(headers)) {
const lower = key.toLowerCase();
if (lower === "connection" || lower === "content-length" || lower === "transfer-encoding") continue;
if (lower === "authorization") { out["Authorization"] = value; continue; }
out[key] = value;
}
out["Accept-Encoding"] = "identity";
out["Host"] = "llm.chutes.ai";
return out;
}
async transformRequestIn(request) {
request.stream = false;
request.max_tokens = 65536;
request.messages.forEach(message => {
if (typeof message.content === 'string') {
message.content += " /nothink";
}
});
return request;
}
async transformResponseOut(response) {
const ct = response.headers.get("Content-Type") || "";
if (!ct.includes("application/json")) return response;
const json = await response.clone().json();
const transformed = this.moveTagsIntoToolCalls(json);
const newHeaders = new Headers(response.headers);
newHeaders.delete("connection");
newHeaders.delete("Connection");
newHeaders.delete("transfer-encoding");
newHeaders.delete("Transfer-Encoding");
return new Response(JSON.stringify(transformed), {
status: response.status,
statusText: response.statusText,
headers: newHeaders,
});
}
moveTagsIntoToolCalls(obj) {
if (!obj?.choices || !Array.isArray(obj.choices)) return obj;
const out = { ...obj, choices: [] };
for (const choice of obj.choices) {
const c = JSON.parse(JSON.stringify(choice));
const content = c?.message?.content;
const { toolCalls, modified } = this.extractToolCallsFromContent(content);
if (c?.message) {
c.message.content = modified;
c.message.tool_calls = (c.message.tool_calls || []).concat(toolCalls);
}
out.choices.push(c);
}
return out;
}
extractToolCallsFromContent(content) {
if (typeof content !== "string") return { toolCalls: [], modified: content };
const toolCalls = [];
const toolCallPattern = /(<tool_call>\s*(\w+)\s*([\s\S]*?)\s*<\/tool_call>)/gm;
let modified = content.replace(toolCallPattern, "");
const matches = [...content.matchAll(toolCallPattern)];
for (const match of matches) {
const [fullMatch, toolName, argsContent] = match;
const id = String(Math.abs(this.hashString(fullMatch)));
const args = this.parseArgsSection(argsContent);
toolCalls.push({
id,
type: "function",
function: {
name: toolName.trim(),
arguments: JSON.stringify(args),
},
});
}
return { toolCalls, modified };
}
parseArgsSection(argsSection) {
const args = {};
if (!argsSection) return args;
const argPattern = /<([^>]+)>(.*?)<\/\1>/gs;
const matches = [...argsSection.matchAll(argPattern)];
for (const match of matches) {
const [fullMatch, key, value] = match;
let processedValue = value;
const isBytesSingle = processedValue.startsWith("b'") && processedValue.endsWith("'");
const isBytesDouble = processedValue.startsWith('b"') && processedValue.endsWith('"');
if (isBytesSingle || isBytesDouble) {
processedValue = processedValue.slice(2, -1);
}
processedValue = processedValue.replace(/\\"/g, '"');
try {
args[key] = JSON.parse(processedValue);
} catch {
// If not valid JSON, return the raw (possibly bytes-decoded) string
args[key] = processedValue;
}
}
return args;
}
hashString(str) {
let h = 0;
for (let i = 0; i < str.length; i++) {
h = (h << 5) - h + str.charCodeAt(i);
h |= 0; // Convert to 32-bit integer
}
return h;
}
}
module.exports = ChutesGLMTransformer;
@jeffery9
Copy link

有问题,导致启动不了

@vitobotta
Copy link
Author

@jeffery9 This transformer is no longer needed. Chutes fixed the issues with GLM and now it works well with Claude Code Router without transformers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment