mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 08:19:59 +00:00
feat(glmt): convert GLMT system to TypeScript
- Add reasoning-enforcer.ts with 4 effort-level prompts - Add glmt-transformer.ts (~1000 LOC) for Anthropic/OpenAI format conversion - Add glmt-proxy.ts (~530 LOC) HTTP proxy with SSE streaming - Extend delta-accumulator.ts with state getters/setters for streaming - All 39 tests passing
This commit is contained in:
@@ -346,4 +346,137 @@ export class DeltaAccumulator {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ========== State Getters ==========
|
||||
|
||||
/**
|
||||
* Check if message has been finalized
|
||||
*/
|
||||
isFinalized(): boolean {
|
||||
return this.finalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if message has started
|
||||
*/
|
||||
isMessageStarted(): boolean {
|
||||
return this.messageStarted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get message ID
|
||||
*/
|
||||
getMessageId(): string {
|
||||
return this.messageId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get model name
|
||||
*/
|
||||
getModel(): string | null {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get role
|
||||
*/
|
||||
getRole(): string {
|
||||
return this.role;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get input tokens
|
||||
*/
|
||||
getInputTokens(): number {
|
||||
return this.inputTokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get output tokens
|
||||
*/
|
||||
getOutputTokens(): number {
|
||||
return this.outputTokens;
|
||||
}
|
||||
|
||||
// ========== State Setters ==========
|
||||
|
||||
/**
|
||||
* Set model name
|
||||
*/
|
||||
setModel(model: string): void {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set message started flag
|
||||
*/
|
||||
setMessageStarted(started: boolean): void {
|
||||
this.messageStarted = started;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set role
|
||||
*/
|
||||
setRole(role: string): void {
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set finalized flag
|
||||
*/
|
||||
setFinalized(finalized: boolean): void {
|
||||
this.finalized = finalized;
|
||||
}
|
||||
|
||||
// ========== Finish Reason ==========
|
||||
|
||||
private finishReason: string | null = null;
|
||||
private usageReceived: boolean = false;
|
||||
|
||||
/**
|
||||
* Set finish reason
|
||||
*/
|
||||
setFinishReason(reason: string): void {
|
||||
this.finishReason = reason;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get finish reason
|
||||
*/
|
||||
getFinishReason(): string | null {
|
||||
return this.finishReason;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if usage stats have been received
|
||||
*/
|
||||
hasUsageReceived(): boolean {
|
||||
return this.usageReceived;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark usage as received
|
||||
*/
|
||||
setUsageReceived(received: boolean): void {
|
||||
this.usageReceived = received;
|
||||
}
|
||||
|
||||
// ========== Tool Call Helpers ==========
|
||||
|
||||
/**
|
||||
* Check if there are any tool calls, or check if a specific index exists
|
||||
*/
|
||||
hasToolCall(index?: number): boolean {
|
||||
if (index === undefined) {
|
||||
return this.toolCalls.length > 0;
|
||||
}
|
||||
return this.toolCallsIndex[index] !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tool call by index
|
||||
*/
|
||||
getToolCall(index: number): ToolCall | undefined {
|
||||
return this.toolCallsIndex[index];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,548 @@
|
||||
/**
|
||||
* GlmtProxy - Embedded HTTP proxy for GLM thinking support
|
||||
*
|
||||
* Architecture:
|
||||
* - Intercepts Claude CLI → Z.AI calls
|
||||
* - Transforms Anthropic format → OpenAI format
|
||||
* - Converts reasoning_content → thinking blocks
|
||||
* - Supports both streaming and buffered modes
|
||||
*
|
||||
* Lifecycle:
|
||||
* - Spawned by bin/ccs.js when 'glmt' profile detected
|
||||
* - Binds to 127.0.0.1:random_port (security + avoid conflicts)
|
||||
* - Terminates when parent process exits
|
||||
*
|
||||
* Debugging:
|
||||
* - Verbose: Pass --verbose to see request/response logs
|
||||
* - Debug: Set CCS_DEBUG=1 to write logs to ~/.ccs/logs/
|
||||
*/
|
||||
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import { GlmtTransformer } from './glmt-transformer';
|
||||
import { SSEParser } from './sse-parser';
|
||||
import { DeltaAccumulator } from './delta-accumulator';
|
||||
|
||||
interface GlmtProxyConfig {
|
||||
verbose?: boolean;
|
||||
debugLog?: boolean;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
interface ThinkingConfig {
|
||||
thinking: boolean;
|
||||
effort: string;
|
||||
}
|
||||
|
||||
interface OpenAIRequest {
|
||||
model: string;
|
||||
messages: unknown[];
|
||||
max_tokens?: number;
|
||||
stream?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface AnthropicRequest {
|
||||
model: string;
|
||||
messages: unknown[];
|
||||
max_tokens?: number;
|
||||
stream?: boolean;
|
||||
thinking?: {
|
||||
type: 'enabled' | 'disabled';
|
||||
budget_tokens?: number;
|
||||
};
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface OpenAIResponse {
|
||||
id?: string;
|
||||
model?: string;
|
||||
choices?: Array<{
|
||||
message: {
|
||||
role: string;
|
||||
content?: string;
|
||||
reasoning_content?: string;
|
||||
tool_calls?: unknown[];
|
||||
};
|
||||
finish_reason?: string;
|
||||
}>;
|
||||
usage?: {
|
||||
prompt_tokens?: number;
|
||||
completion_tokens?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export class GlmtProxy {
|
||||
private transformer: GlmtTransformer;
|
||||
private upstreamUrl: string;
|
||||
private server: http.Server | null;
|
||||
private port: number | null;
|
||||
private verbose: boolean;
|
||||
private timeout: number;
|
||||
|
||||
constructor(config: GlmtProxyConfig = {}) {
|
||||
this.transformer = new GlmtTransformer({
|
||||
verbose: config.verbose,
|
||||
debugLog: config.debugLog || process.env.CCS_DEBUG === '1' || process.env.CCS_DEBUG_LOG === '1'
|
||||
});
|
||||
// Use ANTHROPIC_BASE_URL from environment (set by settings.json) or fallback to Z.AI default
|
||||
this.upstreamUrl = process.env.ANTHROPIC_BASE_URL || 'https://api.z.ai/api/coding/paas/v4/chat/completions';
|
||||
this.server = null;
|
||||
this.port = null;
|
||||
this.verbose = config.verbose || false;
|
||||
this.timeout = config.timeout || 120000; // 120s default
|
||||
}
|
||||
|
||||
/**
|
||||
* Start HTTP server on random port
|
||||
*/
|
||||
async start(): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.server = http.createServer((req, res) => {
|
||||
this.handleRequest(req, res);
|
||||
});
|
||||
|
||||
// Bind to 127.0.0.1:0 (random port for security + avoid conflicts)
|
||||
this.server.listen(0, '127.0.0.1', () => {
|
||||
const address = this.server!.address();
|
||||
this.port = typeof address === 'object' && address ? address.port : 0;
|
||||
// Signal parent process
|
||||
console.log(`PROXY_READY:${this.port}`);
|
||||
|
||||
// Info message (only show in verbose mode)
|
||||
if (this.verbose) {
|
||||
console.error(`[glmt] Proxy listening on port ${this.port} (streaming with auto-fallback)`);
|
||||
}
|
||||
|
||||
// Debug mode notice
|
||||
if ((this.transformer as unknown as { debugLog: boolean }).debugLog) {
|
||||
console.error(`[glmt] Debug logging enabled: ${(this.transformer as unknown as { debugLogDir: string }).debugLogDir}`);
|
||||
console.error(`[glmt] WARNING: Debug logs contain full request/response data`);
|
||||
}
|
||||
|
||||
this.log(`Verbose logging enabled`);
|
||||
resolve(this.port);
|
||||
});
|
||||
|
||||
this.server.on('error', (error) => {
|
||||
console.error('[glmt-proxy] Server error:', error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming HTTP request
|
||||
*/
|
||||
async handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
||||
const startTime = Date.now();
|
||||
this.log(`Request: ${req.method} ${req.url}`);
|
||||
|
||||
try {
|
||||
// Only accept POST requests
|
||||
if (req.method !== 'POST') {
|
||||
res.writeHead(405, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Read request body
|
||||
const body = await this.readBody(req);
|
||||
this.log(`Request body size: ${body.length} bytes`);
|
||||
|
||||
// Parse JSON with error handling
|
||||
let anthropicRequest: AnthropicRequest;
|
||||
try {
|
||||
anthropicRequest = JSON.parse(body);
|
||||
} catch (jsonError) {
|
||||
const err = jsonError as Error;
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
error: {
|
||||
type: 'invalid_request_error',
|
||||
message: 'Invalid JSON in request body: ' + err.message
|
||||
}
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// Log thinking parameter for debugging
|
||||
if (anthropicRequest.thinking) {
|
||||
this.log(`Request contains thinking parameter: ${JSON.stringify(anthropicRequest.thinking)}`);
|
||||
} else {
|
||||
this.log(`Request does NOT contain thinking parameter (will use message tags or default)`);
|
||||
}
|
||||
|
||||
// Try streaming first (default), fallback to buffered on error
|
||||
const useStreaming = anthropicRequest.stream !== false;
|
||||
|
||||
if (useStreaming) {
|
||||
try {
|
||||
await this.handleStreamingRequest(req, res, anthropicRequest, startTime);
|
||||
} catch (streamError) {
|
||||
const err = streamError as Error;
|
||||
this.log(`Streaming failed: ${err.message}, retrying buffered mode`);
|
||||
try {
|
||||
await this.handleBufferedRequest(req, res, anthropicRequest, startTime);
|
||||
} catch (bufferedError) {
|
||||
// Both modes failed, propagate error
|
||||
throw bufferedError;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await this.handleBufferedRequest(req, res, anthropicRequest, startTime);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.error('[glmt-proxy] Request error:', err.message);
|
||||
const duration = Date.now() - startTime;
|
||||
this.log(`Request failed after ${duration}ms: ${err.message}`);
|
||||
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
error: {
|
||||
type: 'proxy_error',
|
||||
message: err.message
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle buffered (non-streaming) request
|
||||
*/
|
||||
private async handleBufferedRequest(
|
||||
_req: http.IncomingMessage,
|
||||
res: http.ServerResponse,
|
||||
anthropicRequest: AnthropicRequest,
|
||||
startTime: number
|
||||
): Promise<void> {
|
||||
// Transform to OpenAI format
|
||||
const { openaiRequest, thinkingConfig } =
|
||||
this.transformer.transformRequest(anthropicRequest as unknown as Parameters<typeof this.transformer.transformRequest>[0]);
|
||||
|
||||
this.log(`Transformed request, thinking: ${thinkingConfig.thinking}`);
|
||||
|
||||
// Forward to Z.AI
|
||||
const openaiResponse = await this.forwardToUpstream(
|
||||
openaiRequest as unknown as OpenAIRequest,
|
||||
{}
|
||||
) as OpenAIResponse;
|
||||
|
||||
this.log(`Received response from upstream`);
|
||||
|
||||
// Transform back to Anthropic format
|
||||
const anthropicResponse = this.transformer.transformResponse(
|
||||
openaiResponse as Parameters<typeof this.transformer.transformResponse>[0],
|
||||
thinkingConfig
|
||||
);
|
||||
|
||||
// Return to Claude CLI
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
});
|
||||
res.end(JSON.stringify(anthropicResponse));
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
this.log(`Request completed in ${duration}ms`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle streaming request
|
||||
*/
|
||||
private async handleStreamingRequest(
|
||||
_req: http.IncomingMessage,
|
||||
res: http.ServerResponse,
|
||||
anthropicRequest: AnthropicRequest,
|
||||
startTime: number
|
||||
): Promise<void> {
|
||||
this.log('Using streaming mode');
|
||||
|
||||
// Transform request
|
||||
const { openaiRequest, thinkingConfig } =
|
||||
this.transformer.transformRequest(anthropicRequest as unknown as Parameters<typeof this.transformer.transformRequest>[0]);
|
||||
|
||||
// Force streaming
|
||||
(openaiRequest as OpenAIRequest).stream = true;
|
||||
|
||||
// Set SSE headers
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'X-Accel-Buffering': 'no' // Disable proxy buffering
|
||||
});
|
||||
|
||||
// Disable Nagle's algorithm to prevent buffering at socket level
|
||||
if (res.socket) {
|
||||
res.socket.setNoDelay(true);
|
||||
}
|
||||
|
||||
this.log('Starting SSE stream to Claude CLI (socket buffering disabled)');
|
||||
|
||||
// Forward and stream
|
||||
await this.forwardAndStreamUpstream(
|
||||
openaiRequest as unknown as OpenAIRequest,
|
||||
{},
|
||||
res,
|
||||
thinkingConfig,
|
||||
startTime
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read request body
|
||||
*/
|
||||
private readBody(req: http.IncomingMessage): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
const maxSize = 10 * 1024 * 1024; // 10MB limit
|
||||
let totalSize = 0;
|
||||
|
||||
req.on('data', (chunk: Buffer) => {
|
||||
totalSize += chunk.length;
|
||||
if (totalSize > maxSize) {
|
||||
reject(new Error('Request body too large (max 10MB)'));
|
||||
return;
|
||||
}
|
||||
chunks.push(chunk);
|
||||
});
|
||||
|
||||
req.on('end', () => resolve(Buffer.concat(chunks).toString()));
|
||||
req.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Forward request to Z.AI upstream
|
||||
*/
|
||||
private forwardToUpstream(
|
||||
openaiRequest: OpenAIRequest,
|
||||
_originalHeaders: Record<string, string | undefined>
|
||||
): Promise<unknown> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = new URL(this.upstreamUrl);
|
||||
const requestBody = JSON.stringify(openaiRequest);
|
||||
|
||||
const options: https.RequestOptions = {
|
||||
hostname: url.hostname,
|
||||
port: url.port || 443,
|
||||
path: url.pathname || '/api/coding/paas/v4/chat/completions',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(requestBody),
|
||||
'Authorization': process.env.ANTHROPIC_AUTH_TOKEN || '',
|
||||
'User-Agent': 'CCS-GLMT-Proxy/1.0'
|
||||
}
|
||||
};
|
||||
|
||||
// Debug logging
|
||||
this.log(`Forwarding to: ${url.hostname}${url.pathname}`);
|
||||
|
||||
// Set timeout
|
||||
const timeoutHandle = setTimeout(() => {
|
||||
req.destroy();
|
||||
reject(new Error('Upstream request timeout'));
|
||||
}, this.timeout);
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
clearTimeout(timeoutHandle);
|
||||
|
||||
const chunks: Buffer[] = [];
|
||||
res.on('data', (chunk: Buffer) => chunks.push(chunk));
|
||||
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const body = Buffer.concat(chunks).toString();
|
||||
this.log(`Upstream response size: ${body.length} bytes`);
|
||||
|
||||
// Check for non-200 status
|
||||
if (res.statusCode !== 200) {
|
||||
reject(new Error(
|
||||
`Upstream error: ${res.statusCode} ${res.statusMessage}\n${body}`
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
const response = JSON.parse(body);
|
||||
resolve(response);
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
reject(new Error('Invalid JSON from upstream: ' + err.message));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
clearTimeout(timeoutHandle);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
req.write(requestBody);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Forward request to Z.AI and stream response
|
||||
*/
|
||||
private async forwardAndStreamUpstream(
|
||||
openaiRequest: OpenAIRequest,
|
||||
_originalHeaders: Record<string, string | undefined>,
|
||||
clientRes: http.ServerResponse,
|
||||
thinkingConfig: ThinkingConfig,
|
||||
startTime: number
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = new URL(this.upstreamUrl);
|
||||
const requestBody = JSON.stringify(openaiRequest);
|
||||
|
||||
const options: https.RequestOptions = {
|
||||
hostname: url.hostname,
|
||||
port: url.port || 443,
|
||||
path: url.pathname || '/api/coding/paas/v4/chat/completions',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(requestBody),
|
||||
'Authorization': process.env.ANTHROPIC_AUTH_TOKEN || '',
|
||||
'User-Agent': 'CCS-GLMT-Proxy/1.0',
|
||||
'Accept': 'text/event-stream'
|
||||
}
|
||||
};
|
||||
|
||||
this.log(`Forwarding streaming request to: ${url.hostname}${url.pathname}`);
|
||||
|
||||
// Apply timeout to streaming requests
|
||||
const timeoutHandle = setTimeout(() => {
|
||||
req.destroy();
|
||||
reject(new Error(`Streaming request timeout after ${this.timeout}ms`));
|
||||
}, this.timeout);
|
||||
|
||||
const req = https.request(options, (upstreamRes) => {
|
||||
clearTimeout(timeoutHandle);
|
||||
if (upstreamRes.statusCode !== 200) {
|
||||
let body = '';
|
||||
upstreamRes.on('data', (chunk: Buffer) => body += chunk.toString());
|
||||
upstreamRes.on('end', () => {
|
||||
reject(new Error(`Upstream error: ${upstreamRes.statusCode}\n${body}`));
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const parser = new SSEParser();
|
||||
const accumulator = new DeltaAccumulator(thinkingConfig);
|
||||
|
||||
upstreamRes.on('data', (chunk: Buffer) => {
|
||||
try {
|
||||
const events = parser.parse(chunk);
|
||||
|
||||
events.forEach(event => {
|
||||
// Transform OpenAI delta → Anthropic events
|
||||
const anthropicEvents = this.transformer.transformDelta(event, accumulator);
|
||||
|
||||
// Forward to Claude CLI with immediate flush
|
||||
anthropicEvents.forEach(evt => {
|
||||
const eventLine = `event: ${evt.event}\n`;
|
||||
const dataLine = `data: ${JSON.stringify(evt.data)}\n\n`;
|
||||
clientRes.write(eventLine + dataLine);
|
||||
|
||||
// Flush immediately if method available
|
||||
if (typeof (clientRes as unknown as { flush?: () => void }).flush === 'function') {
|
||||
(clientRes as unknown as { flush: () => void }).flush();
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
this.log(`Error processing chunk: ${err.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
upstreamRes.on('end', () => {
|
||||
const duration = Date.now() - startTime;
|
||||
this.log(`Streaming completed in ${duration}ms`);
|
||||
clientRes.end();
|
||||
resolve();
|
||||
});
|
||||
|
||||
upstreamRes.on('error', (error) => {
|
||||
clearTimeout(timeoutHandle);
|
||||
this.log(`Upstream stream error: ${error.message}`);
|
||||
clientRes.write(`event: error\n`);
|
||||
clientRes.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
|
||||
clientRes.end();
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
clearTimeout(timeoutHandle);
|
||||
this.log(`Request error: ${error.message}`);
|
||||
clientRes.write(`event: error\n`);
|
||||
clientRes.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
|
||||
clientRes.end();
|
||||
reject(error);
|
||||
});
|
||||
|
||||
req.write(requestBody);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop proxy server
|
||||
*/
|
||||
stop(): void {
|
||||
if (this.server) {
|
||||
this.log('Stopping proxy server');
|
||||
this.server.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log message if verbose
|
||||
*/
|
||||
private log(message: string): void {
|
||||
if (this.verbose) {
|
||||
console.error(`[glmt-proxy] ${message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Main entry point
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2);
|
||||
const verbose = args.includes('--verbose') || args.includes('-v');
|
||||
|
||||
const proxy = new GlmtProxy({ verbose });
|
||||
|
||||
proxy.start().catch(error => {
|
||||
console.error('[glmt-proxy] Failed to start:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Cleanup on signals
|
||||
process.on('SIGTERM', () => {
|
||||
proxy.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
proxy.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// Keep process alive
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('[glmt-proxy] Uncaught exception:', error);
|
||||
proxy.stop();
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
export default GlmtProxy;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* ReasoningEnforcer - Inject explicit reasoning instructions into prompts
|
||||
*
|
||||
* Purpose: Force GLM models to use structured reasoning output format (<reasoning_content>)
|
||||
* This complements API parameters (reasoning: true) with explicit prompt instructions.
|
||||
*
|
||||
* Strategy:
|
||||
* 1. If system prompt exists: Prepend reasoning instruction
|
||||
* 2. If no system prompt: Prepend to first user message
|
||||
* 3. Select prompt template based on effort level (low/medium/high/max)
|
||||
* 4. Preserve message structure (string vs array content)
|
||||
*/
|
||||
|
||||
type EffortLevel = 'low' | 'medium' | 'high' | 'max';
|
||||
|
||||
interface ContentBlock {
|
||||
type: string;
|
||||
text?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface Message {
|
||||
role: string;
|
||||
content: string | ContentBlock[];
|
||||
}
|
||||
|
||||
interface ThinkingConfig {
|
||||
thinking?: boolean;
|
||||
effort?: string;
|
||||
}
|
||||
|
||||
interface ReasoningEnforcerOptions {
|
||||
enabled?: boolean;
|
||||
prompts?: Record<EffortLevel, string>;
|
||||
}
|
||||
|
||||
export class ReasoningEnforcer {
|
||||
private enabled: boolean;
|
||||
private prompts: Record<EffortLevel, string>;
|
||||
|
||||
constructor(options: ReasoningEnforcerOptions = {}) {
|
||||
this.enabled = options.enabled ?? false; // Opt-in by default
|
||||
this.prompts = options.prompts || this.getDefaultPrompts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject reasoning instruction into messages
|
||||
* @param messages - Messages array to modify
|
||||
* @param thinkingConfig - { thinking: boolean, effort: string }
|
||||
* @returns Modified messages array
|
||||
*/
|
||||
injectInstruction(messages: Message[], thinkingConfig: ThinkingConfig = {}): Message[] {
|
||||
// Only inject if enabled or thinking explicitly requested
|
||||
if (!this.enabled && !thinkingConfig.thinking) {
|
||||
return messages;
|
||||
}
|
||||
|
||||
// Clone messages to avoid mutation
|
||||
const modifiedMessages: Message[] = JSON.parse(JSON.stringify(messages));
|
||||
|
||||
// Select prompt based on effort level
|
||||
const effort = (thinkingConfig.effort?.toLowerCase() || 'medium') as EffortLevel;
|
||||
const prompt = this.selectPrompt(effort);
|
||||
|
||||
// Strategy 1: Inject into system prompt (preferred)
|
||||
const systemIndex = modifiedMessages.findIndex(m => m.role === 'system');
|
||||
if (systemIndex >= 0) {
|
||||
const systemMsg = modifiedMessages[systemIndex];
|
||||
|
||||
if (typeof systemMsg.content === 'string') {
|
||||
systemMsg.content = `${prompt}\n\n${systemMsg.content}`;
|
||||
} else if (Array.isArray(systemMsg.content)) {
|
||||
systemMsg.content.unshift({
|
||||
type: 'text',
|
||||
text: prompt
|
||||
});
|
||||
}
|
||||
|
||||
return modifiedMessages;
|
||||
}
|
||||
|
||||
// Strategy 2: Prepend to first user message
|
||||
const userIndex = modifiedMessages.findIndex(m => m.role === 'user');
|
||||
if (userIndex >= 0) {
|
||||
const userMsg = modifiedMessages[userIndex];
|
||||
|
||||
if (typeof userMsg.content === 'string') {
|
||||
userMsg.content = `${prompt}\n\n${userMsg.content}`;
|
||||
} else if (Array.isArray(userMsg.content)) {
|
||||
userMsg.content.unshift({
|
||||
type: 'text',
|
||||
text: prompt
|
||||
});
|
||||
}
|
||||
|
||||
return modifiedMessages;
|
||||
}
|
||||
|
||||
// No system or user messages found (edge case)
|
||||
return modifiedMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select prompt template based on effort level
|
||||
*/
|
||||
private selectPrompt(effort: EffortLevel): string {
|
||||
return this.prompts[effort] || this.prompts.medium;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default prompt templates
|
||||
*/
|
||||
private getDefaultPrompts(): Record<EffortLevel, string> {
|
||||
return {
|
||||
low: `You are an expert reasoning model using GLM-4.6 architecture.
|
||||
|
||||
CRITICAL: Before answering, write 2-3 sentences of reasoning in <reasoning_content> tags.
|
||||
|
||||
OUTPUT FORMAT:
|
||||
<reasoning_content>
|
||||
(Brief analysis: what is the problem? what's the approach?)
|
||||
</reasoning_content>
|
||||
|
||||
(Write your final answer here)`,
|
||||
|
||||
medium: `You are an expert reasoning model using GLM-4.6 architecture.
|
||||
|
||||
CRITICAL REQUIREMENTS:
|
||||
1. Always think step-by-step before answering
|
||||
2. Write your reasoning process explicitly in <reasoning_content> tags
|
||||
3. Never skip your chain of thought, even for simple problems
|
||||
|
||||
OUTPUT FORMAT:
|
||||
<reasoning_content>
|
||||
(Write your detailed thinking here: analyze the problem, explore approaches,
|
||||
evaluate trade-offs, and arrive at a conclusion)
|
||||
</reasoning_content>
|
||||
|
||||
(Write your final answer here based on your reasoning above)`,
|
||||
|
||||
high: `You are an expert reasoning model using GLM-4.6 architecture.
|
||||
|
||||
CRITICAL REQUIREMENTS:
|
||||
1. Think deeply and systematically before answering
|
||||
2. Write comprehensive reasoning in <reasoning_content> tags
|
||||
3. Explore multiple approaches and evaluate trade-offs
|
||||
4. Show all steps in your problem-solving process
|
||||
|
||||
OUTPUT FORMAT:
|
||||
<reasoning_content>
|
||||
(Write exhaustive analysis here:
|
||||
- Problem decomposition
|
||||
- Multiple approach exploration
|
||||
- Trade-off analysis for each approach
|
||||
- Edge case consideration
|
||||
- Final conclusion with justification)
|
||||
</reasoning_content>
|
||||
|
||||
(Write your final answer here based on your systematic reasoning above)`,
|
||||
|
||||
max: `You are an expert reasoning model using GLM-4.6 architecture.
|
||||
|
||||
CRITICAL REQUIREMENTS:
|
||||
1. Think exhaustively from first principles
|
||||
2. Write extremely detailed reasoning in <reasoning_content> tags
|
||||
3. Analyze ALL possible angles, approaches, and edge cases
|
||||
4. Challenge your own assumptions and explore alternatives
|
||||
5. Provide rigorous justification for every claim
|
||||
|
||||
OUTPUT FORMAT:
|
||||
<reasoning_content>
|
||||
(Write comprehensive analysis here:
|
||||
- First principles breakdown
|
||||
- Exhaustive approach enumeration
|
||||
- Comparative analysis of all approaches
|
||||
- Edge case and failure mode analysis
|
||||
- Assumption validation
|
||||
- Counter-argument consideration
|
||||
- Final conclusion with rigorous justification)
|
||||
</reasoning_content>
|
||||
|
||||
(Write your final answer here based on your exhaustive reasoning above)`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default ReasoningEnforcer;
|
||||
Reference in New Issue
Block a user