fix some errors

This commit is contained in:
musistudio
2025-12-31 13:47:35 +08:00
parent f679670f92
commit e7073790b3
7 changed files with 118 additions and 37 deletions

View File

@@ -62,21 +62,20 @@ export async function executeCodeCommand(
}; };
} }
args.push('--settings', getSettingsPath(`${JSON.stringify(settingsFlag)}`));
console.log(args)
// Non-interactive mode for automation environments // Non-interactive mode for automation environments
if (config.NON_INTERACTIVE_MODE) { if (config.NON_INTERACTIVE_MODE) {
env.CI = "true"; settingsFlag.env = {
env.FORCE_COLOR = "0"; ...settingsFlag.env,
env.NODE_NO_READLINE = "1"; CI: "true",
env.TERM = "dumb"; FORCE_COLOR: "0",
NODE_NO_READLINE: "1",
TERM: "dumb"
}
} }
// Set ANTHROPIC_SMALL_FAST_MODEL if it exists in config const settingsFile = await getSettingsPath(`${JSON.stringify(settingsFlag)}`)
if (config?.ANTHROPIC_SMALL_FAST_MODEL) {
env.ANTHROPIC_SMALL_FAST_MODEL = config.ANTHROPIC_SMALL_FAST_MODEL; args.push('--settings', settingsFile);
}
// Increment reference count when command starts // Increment reference count when command starts
incrementReferenceCount(); incrementReferenceCount();

View File

@@ -3,6 +3,7 @@ import readline from "node:readline";
import JSON5 from "json5"; import JSON5 from "json5";
import path from "node:path"; import path from "node:path";
import { createHash } from "node:crypto"; import { createHash } from "node:crypto";
import os from "node:os";
import { import {
CONFIG_FILE, CONFIG_FILE,
HOME_DIR, PID_FILE, HOME_DIR, PID_FILE,
@@ -12,7 +13,7 @@ import {
readPresetFile, readPresetFile,
} from "@CCR/shared"; } from "@CCR/shared";
import { getServer } from "@CCR/server"; import { getServer } from "@CCR/server";
import { writeFileSync, existsSync, readFileSync } from "fs"; import { writeFileSync, existsSync, readFileSync, mkdirSync } from "fs";
import { checkForUpdates, performUpdate } from "./update"; import { checkForUpdates, performUpdate } from "./update";
import { version } from "../../package.json"; import { version } from "../../package.json";
import { spawn } from "child_process"; import { spawn } from "child_process";
@@ -254,25 +255,35 @@ export const restartService = async () => {
/** /**
* 获取设置文件的临时路径 * Get a temporary path for the settings file
* 对内容进行 hash如果临时目录中已存在该 hash 的文件则直接返回,否则创建新文件 * Hash the content and return the file path if it already exists in temp directory,
* @param content 设置内容字符串 * otherwise create a new file with the content
* @returns 临时文件的完整路径 * @param content Settings content string
* @returns Full path to the temporary file
*/ */
export const getSettingsPath = (content: string): string => { export const getSettingsPath = async (content: string): Promise<string> => {
// 对内容进行 hash使用 SHA256 算法) // Hash the content using SHA256 algorithm
const hash = createHash('sha256').update(content, 'utf-8').digest('hex'); const hash = createHash('sha256').update(content, 'utf-8').digest('hex');
// Create claude-code-router directory in system temp folder
const tempDir = path.join(os.tmpdir(), 'claude-code-router');
const fileName = `ccr-settings-${hash}.json`; const fileName = `ccr-settings-${hash}.json`;
const tempFilePath = path.join(HOME_DIR, fileName); const tempFilePath = path.join(tempDir, fileName);
// 检查文件是否已存在 // Ensure the directory exists
if (existsSync(tempFilePath)) { try {
return tempFilePath; await fs.access(tempDir);
} catch {
await fs.mkdir(tempDir, { recursive: true });
} }
// 文件不存在,创建并写入内容 // Check if the file already exists
writeFileSync(tempFilePath, content, 'utf-8'); try {
await fs.access(tempFilePath);
return tempFilePath; return tempFilePath;
} catch {
// File doesn't exist, create and write content
await fs.writeFile(tempFilePath, content, 'utf-8');
return tempFilePath;
}
} }

View File

@@ -1,8 +1,8 @@
import fp from 'fastify-plugin'; import fp from 'fastify-plugin';
import { CCRPlugin, CCRPluginOptions } from './types'; import { CCRPlugin, CCRPluginOptions } from './types';
import { SSEParserTransform } from '../utils/sse'; import { SSEParserTransform } from '../utils/sse';
import { Tiktoken } from 'tiktoken';
import { OutputHandlerConfig, OutputOptions, outputManager } from './output'; import { OutputHandlerConfig, OutputOptions, outputManager } from './output';
import { ITokenizer, TokenizerConfig } from '../types/tokenizer';
/** /**
* Token statistics interface * Token statistics interface
@@ -45,6 +45,9 @@ interface TokenSpeedOptions extends CCRPluginOptions {
// Store request-level statistics // Store request-level statistics
const requestStats = new Map<string, TokenStats>(); const requestStats = new Map<string, TokenStats>();
// Cache tokenizers by provider and model to avoid repeated initialization
const tokenizerCache = new Map<string, ITokenizer>();
// Cross-request statistics // Cross-request statistics
const globalStats = { const globalStats = {
totalRequests: 0, totalRequests: 0,
@@ -94,15 +97,53 @@ export const tokenSpeedPlugin: CCRPlugin = {
outputManager.setDefaultOptions(opts.outputOptions); outputManager.setDefaultOptions(opts.outputOptions);
} }
// Initialize tiktoken encoder /**
let encoding: Tiktoken | null = null; * Get or create tokenizer for a specific provider and model
try { */
const { get_encoding } = await import('tiktoken'); const getTokenizerForRequest = async (request: any): Promise<ITokenizer | null> => {
encoding = get_encoding('cl100k_base'); const tokenizerService = (fastify as any).tokenizerService;
} catch (error) { if (!tokenizerService) {
fastify.log?.warn('Failed to load tiktoken, falling back to estimation'); fastify.log?.warn('TokenizerService not available');
return null;
} }
// Extract provider and model from request
// Format: "provider,model" or just "model"
if (!request.provider || !request.model) {
return null;
}
const providerName = request.provider;
const modelName = request.model;
// Create cache key
const cacheKey = `${providerName}:${modelName}`;
// Check cache first
if (tokenizerCache.has(cacheKey)) {
return tokenizerCache.get(cacheKey)!;
}
// Get tokenizer config for this model
const tokenizerConfig = tokenizerService.getTokenizerConfigForModel(providerName, modelName);
if (!tokenizerConfig) {
// No specific config, use fallback
fastify.log?.debug(`No tokenizer config for ${providerName}:${modelName}, using fallback`);
return null;
}
try {
// Create and cache tokenizer
const tokenizer = await tokenizerService.getTokenizer(tokenizerConfig);
tokenizerCache.set(cacheKey, tokenizer);
fastify.log?.info(`Created tokenizer for ${providerName}:${modelName} - ${tokenizer.name}`);
return tokenizer;
} catch (error: any) {
fastify.log?.warn(`Failed to create tokenizer for ${providerName}:${modelName}: ${error.message}`);
return null;
}
};
// Add onSend hook to intercept streaming responses // Add onSend hook to intercept streaming responses
fastify.addHook('onSend', async (request, reply, payload) => { fastify.addHook('onSend', async (request, reply, payload) => {
// Only handle streaming responses // Only handle streaming responses
@@ -122,6 +163,10 @@ export const tokenSpeedPlugin: CCRPlugin = {
tokensPerSecond: 0, tokensPerSecond: 0,
contentBlocks: [] contentBlocks: []
}); });
// Get tokenizer for this specific request
const tokenizer = await getTokenizerForRequest(request);
// Tee the stream: one for stats, one for the client // Tee the stream: one for stats, one for the client
const [originalStream, statsStream] = payload.tee(); const [originalStream, statsStream] = payload.tee();
@@ -156,8 +201,8 @@ export const tokenSpeedPlugin: CCRPlugin = {
// Detect content_block_delta event (incremental tokens) // Detect content_block_delta event (incremental tokens)
if (data.event === 'content_block_delta' && data.data?.delta?.type === 'text_delta') { if (data.event === 'content_block_delta' && data.data?.delta?.type === 'text_delta') {
const text = data.data.delta.text; const text = data.data.delta.text;
const tokenCount = encoding const tokenCount = tokenizer
? encoding.encode(text).length ? (tokenizer.encodeText ? tokenizer.encodeText(text).length : estimateTokens(text))
: estimateTokens(text); : estimateTokens(text);
stats.tokenCount += tokenCount; stats.tokenCount += tokenCount;

View File

@@ -38,6 +38,7 @@ import { sessionUsageCache } from "./utils/cache";
declare module "fastify" { declare module "fastify" {
interface FastifyRequest { interface FastifyRequest {
provider?: string; provider?: string;
model?: string;
} }
interface FastifyInstance { interface FastifyInstance {
_server?: Server; _server?: Server;
@@ -226,6 +227,7 @@ class Server {
const [provider, ...model] = body.model.split(","); const [provider, ...model] = body.model.split(",");
body.model = model.join(","); body.model = model.join(",");
req.provider = provider; req.provider = provider;
req.model = model;
return; return;
} catch (err) { } catch (err) {
req.log.error({error: err}, "Error in modelProviderMiddleware:"); req.log.error({error: err}, "Error in modelProviderMiddleware:");

View File

@@ -164,6 +164,16 @@ export class HuggingFaceTokenizer implements ITokenizer {
return this.tokenizer !== null; return this.tokenizer !== null;
} }
/**
* Encode text to tokens (for simple text tokenization)
*/
encodeText(text: string): number[] {
if (!this.tokenizer) {
throw new Error("Tokenizer not initialized");
}
return this.tokenizer.encode(text).ids;
}
dispose(): void { dispose(): void {
this.tokenizer = null; this.tokenizer = null;
} }

View File

@@ -99,6 +99,17 @@ export class TiktokenTokenizer implements ITokenizer {
return this.encoding !== undefined; return this.encoding !== undefined;
} }
/**
* Encode text to tokens (for simple text tokenization)
*/
encodeText(text: string): number[] {
const encoding = this.encoding;
if (!encoding) {
throw new Error("Encoding not initialized");
}
return Array.from(encoding.encode(text));
}
dispose(): void { dispose(): void {
if (this.encoding) { if (this.encoding) {
this.encoding.free(); this.encoding.free();

View File

@@ -112,6 +112,9 @@ export interface ITokenizer {
/** Count tokens for a given request */ /** Count tokens for a given request */
countTokens(request: TokenizeRequest): Promise<number>; countTokens(request: TokenizeRequest): Promise<number>;
/** Encode text to tokens (for simple text tokenization) */
encodeText?(text: string): number[];
/** Check if tokenizer is initialized */ /** Check if tokenizer is initialized */
isInitialized(): boolean; isInitialized(): boolean;