mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-01-30 06:12:06 +00:00
fix some errors
This commit is contained in:
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,14 +97,52 @@ 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) => {
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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:");
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
3
packages/core/src/types/tokenizer.d.ts
vendored
3
packages/core/src/types/tokenizer.d.ts
vendored
@@ -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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user