Skip to content

Instantly share code, notes, and snippets.

@vitobotta
Last active September 1, 2025 19:39
Show Gist options
  • Save vitobotta/2be3f33722e05e8d4f9d2b0138b8c863 to your computer and use it in GitHub Desktop.
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;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment