From 2877c94ba64cc679bc671639dff67ffab976b633 Mon Sep 17 00:00:00 2001 From: Andreas Bichinger Date: Sun, 14 Sep 2025 10:37:03 +0200 Subject: [PATCH 1/2] feat(typechat) Add rate-limiting logic Introduced `TYPECHAT_RPM` environment variable to control the maximum requests per minute. Related: #273 --- src/services/typechat.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/services/typechat.ts b/src/services/typechat.ts index 42c0e5d1..a6affc0f 100644 --- a/src/services/typechat.ts +++ b/src/services/typechat.ts @@ -16,6 +16,8 @@ import { TString, } from "./service-definitions"; +const MINUTE_MS = 60*1000; + function generateSchema( batch: TString[], name: string, @@ -132,6 +134,7 @@ export class TypeChatTranslate implements TService { } async translateStrings(args: TServiceArgs) { + const rpm = parseInt(process.env.TYPECHAT_RPM ?? "") const batchSize = parseInt(process.env.OPEN_AI_BATCH_SIZE ?? ""); const batches: TString[][] = chunk( args.strings, @@ -141,9 +144,21 @@ export class TypeChatTranslate implements TService { const model = this.manual ? createManualModel() : createLanguageModel(process.env); - for (const batch of batches) { + for(var i = 0; i < batches.length; i++) { + const batch = batches[i] + const start = new Date() const result = await translateBatch(model, batch, args, process.env); results.push(result); + + // Sleep to not exceeded the specified requests per minute (RPM) + if (!this.manual && !isNaN(rpm) && rpm > 0 && i < batches.length - 1) { + const requestDuration = new Date().getTime() - start.getTime() + const sleepDuration = (MINUTE_MS/rpm)-requestDuration + console.log( + `Going to sleep for ${sleepDuration} ms` + ); + await sleep(sleepDuration) + } } return flatten(results); } @@ -220,7 +235,7 @@ function isTransientHttpError(code: number): boolean { * Sleeps for the given number of milliseconds. */ function sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); + return new Promise((resolve) => setTimeout(resolve, ms > 0 ? ms : 0)); } function missingEnvironmentVariable(name: string): never { From eefabc608e3c33f50f7cdf6743543d7b2f7ede9a Mon Sep 17 00:00:00 2001 From: Andreas Bichinger Date: Sun, 14 Sep 2025 11:40:16 +0200 Subject: [PATCH 2/2] feat(typechat): add decorator to sanitize keys --- src/services/typechat.ts | 35 +++++++++++++++++++++++++++++++++++ tsconfig.json | 3 ++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/services/typechat.ts b/src/services/typechat.ts index a6affc0f..7f9bfc3f 100644 --- a/src/services/typechat.ts +++ b/src/services/typechat.ts @@ -126,6 +126,40 @@ function createManualModel(): TypeChatLanguageModel { } } +function sanitize_keys( + target: any, + propertyKey: string, + descriptor: PropertyDescriptor +) { + const originalMethod = descriptor.value; + + descriptor.value = async function (args: TServiceArgs) { + const sanitizedKeys = args.strings.map(({ key }) => { + return { + new: key.replace(/[^a-zA-Z0-9_]+/g, "_"), + old: key, + }; + }); + + // Replace keys with sanitized versions + for (let i = 0; i < sanitizedKeys.length; i++) { + args.strings[i].key = sanitizedKeys[i].new; + } + + // Call the original method + const results: TResult[] = await originalMethod.apply(this, [args]); + + // Restore original keys in the results + for (let i = 0; i < sanitizedKeys.length; i++) { + results[i].key = sanitizedKeys[i].old; + } + + return results; + }; + + return descriptor; +} + export class TypeChatTranslate implements TService { manual: boolean; @@ -133,6 +167,7 @@ export class TypeChatTranslate implements TService { this.manual = manual ?? false; } + @sanitize_keys async translateStrings(args: TServiceArgs) { const rpm = parseInt(process.env.TYPECHAT_RPM ?? "") const batchSize = parseInt(process.env.OPEN_AI_BATCH_SIZE ?? ""); diff --git a/tsconfig.json b/tsconfig.json index 8f844797..5abcbeee 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,7 +20,8 @@ "strictNullChecks": true, "strictFunctionTypes": true, "strictPropertyInitialization": true, - "strictBindCallApply": true + "strictBindCallApply": true, + "experimentalDecorators": true, }, "files": [ "src/index.ts"