From 5fdfae60871eb13c7331cf4dbc701ba931a2553e Mon Sep 17 00:00:00 2001 From: Daniel Salazar Date: Thu, 5 Feb 2026 15:15:27 -0800 Subject: [PATCH] fix: usage limited error handling (#2423) --- src/backend/src/services/ChatAPIService.js | 4 +- .../src/services/ai/chat/AIChatService.ts | 20 ++- .../providers/UsageLimitedChatProvider.ts | 140 ------------------ 3 files changed, 11 insertions(+), 153 deletions(-) delete mode 100644 src/backend/src/services/ai/chat/providers/UsageLimitedChatProvider.ts diff --git a/src/backend/src/services/ChatAPIService.js b/src/backend/src/services/ChatAPIService.js index 70164f7e7..d4784a3b4 100644 --- a/src/backend/src/services/ChatAPIService.js +++ b/src/backend/src/services/ChatAPIService.js @@ -80,7 +80,7 @@ class ChatAPIService extends BaseService { }); // Return the list of models - res.json({ models: models.filter(e => !['costly', 'fake', 'abuse', 'usage-limited', 'model-fallback-test-1'].includes(e)) }); + res.json({ models: models.filter(e => !['costly', 'fake', 'abuse', 'model-fallback-test-1'].includes(e)) }); } catch ( error ) { this.log.error('Error fetching models:', error); throw APIError.create('internal_server_error'); @@ -103,7 +103,7 @@ class ChatAPIService extends BaseService { }); // Return the detailed list of models - res.json({ models: models.filter((e) => !['costly', 'fake', 'abuse', 'usage-limited', 'model-fallback-test-1'].includes(e.id)) }); + res.json({ models: models.filter((e) => !['costly', 'fake', 'abuse', 'model-fallback-test-1'].includes(e.id)) }); } catch ( error ) { this.log.error('Error fetching model details:', error); throw APIError.create('internal_server_error'); diff --git a/src/backend/src/services/ai/chat/AIChatService.ts b/src/backend/src/services/ai/chat/AIChatService.ts index d9adac325..98b50dbc6 100644 --- a/src/backend/src/services/ai/chat/AIChatService.ts +++ b/src/backend/src/services/ai/chat/AIChatService.ts @@ -44,7 +44,6 @@ import { OpenAiResponsesChatProvider } from './providers/OpenAiProvider/OpenAiCh import { OpenRouterProvider } from './providers/OpenRouterProvider/OpenRouterProvider.js'; import { TogetherAIProvider } from './providers/TogetherAiProvider/TogetherAIProvider.js'; import { IChatModel, IChatProvider, ICompleteArguments } from './providers/types.js'; -import { UsageLimitedChatProvider } from './providers/UsageLimitedChatProvider.js'; import { XAIProvider } from './providers/XAIProvider/XAIProvider.js'; import { redisClient } from '../../../clients/redis/redisSingleton.js'; @@ -179,9 +178,8 @@ export class AIChatService extends BaseService { this.#providers['ollama'] = new OllamaChatProvider(ollamaConfig, this.meteringService); } - // fake and usage-limited providers last + // fake providers last this.#providers['fake-chat'] = new FakeChatProvider(); - this.#providers['usage-limited-chat'] = new UsageLimitedChatProvider(); // emit event for extensions to add providers const extensionProviders = {} as Record; @@ -310,7 +308,6 @@ export class AIChatService extends BaseService { } let model = this.getModel({ modelId: parameters.model, provider: intendedProvider }) || await this.getFallbackModel(parameters.model, [], []); const abuseModel = this.getModel({ modelId: 'abuse' }); - const usageLimitedModel = this.getModel({ modelId: 'usage-limited' }); const completionId = cuid2(); const event = { @@ -377,7 +374,10 @@ export class AIChatService extends BaseService { // Handle usage limits reached case if ( ! usageAllowed ) { - model = usageLimitedModel; + throw APIError.create('insufficient_funds', new Error('No usage left for request.'), { + delegate: 'usage-limited-chat', + message: 'No usage left for request.', + }); } // block non subscriber only models for non-subscribers @@ -494,7 +494,10 @@ export class AIChatService extends BaseService { const fallbackUsageAllowed = await this.meteringService.hasEnoughCredits(actor, 1); // we checked earlier, assume same costs if ( ! fallbackUsageAllowed ) { - fallBackModel = usageLimitedModel; + throw APIError.create('insufficient_funds', new Error('No usage left for request.'), { + delegate: 'usage-limited-chat', + message: 'No usage left for request.', + }); } const provider = this.#providers[fallBackModel.provider!]; @@ -519,11 +522,6 @@ export class AIChatService extends BaseService { resMetadata.service_used = model.provider; // legacy field resMetadata.providerUsed = model.id; - // Add flag if we're using the usage-limited service - if ( model.provider === 'usage-limited-chat' ) { - resMetadata.usage_limited = true; - } - const username = actor.type?.user?.username; if ( ! res! ) { diff --git a/src/backend/src/services/ai/chat/providers/UsageLimitedChatProvider.ts b/src/backend/src/services/ai/chat/providers/UsageLimitedChatProvider.ts deleted file mode 100644 index d849e5fc6..000000000 --- a/src/backend/src/services/ai/chat/providers/UsageLimitedChatProvider.ts +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright (C) 2024-present Puter Technologies Inc. - * - * This file is part of Puter. - * - * Puter is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import dedent from 'dedent'; -import { PassThrough } from 'stream'; -import Streaming from '../../utils/Streaming.js'; -import { IChatProvider, ICompleteArguments } from './types.js'; - -/** -* UsageLimitedChatService - A specialized chat service that returns resource exhaustion messages. -* Extends BaseService to provide responses indicating the user has exceeded their usage limits. -* Follows the same response format as real AI providers but with a custom message about upgrading. -* Can handle both streaming and non-streaming requests consistently. -*/ -export class UsageLimitedChatProvider implements IChatProvider { - - models (): ReturnType { - return [{ - id: 'usage-limited', - name: 'Usage Limited', - context: 16384, - costs_currency: 'usd-cents', - input_cost_key: 'input', - output_cost_key: 'output', - max_tokens: 16384, - costs: { - tokens: 1_000_000, - input: 0, - output: 0, - }, - }]; - } - list () { - return ['usage-limited']; - } - async complete ({ stream, customLimitMessage }: ICompleteArguments): ReturnType { - const limitMessage = customLimitMessage || dedent(` - You have reached your AI usage limit for this account. - `); - - // If streaming is requested, return a streaming response - if ( stream ) { - const streamObj = new PassThrough(); - - const chatStream = new Streaming.AIChatStream({ - stream: streamObj, - }); - - // Schedule the streaming response - setTimeout(() => { - chatStream.write({ - type: 'content_block_start', - index: 0, - }); - - chatStream.write({ - type: 'content_block_delta', - index: 0, - delta: { - type: 'text', - text: limitMessage, - }, - }); - - chatStream.write({ - type: 'content_block_stop', - index: 0, - }); - - chatStream.write({ - type: 'message_stop', - stop_reason: 'end_turn', - }); - - chatStream.end(); - }, 10); - - return { - stream: true, - init_chat_stream: async ({ chatStream: cs }) => { - // Copy contents from our stream to the provided one - chatStream.stream.pipe(cs.stream); - }, - finally_fn: async () => { - // No-op - }, - }; - } - - // Non-streaming response - return { - message: { - id: '00000000-0000-0000-0000-000000000000', - type: 'message', - role: 'assistant', - model: 'usage-limited', - content: [ - { - 'type': 'text', - 'text': limitMessage, - }, - ], - stop_reason: 'end_turn', - stop_sequence: null, - usage: { - 'input_tokens': 0, - 'output_tokens': 1, - }, - }, - usage: { - 'input_tokens': 0, - 'output_tokens': 1, - }, - finish_reason: 'stop', - }; - } - checkModeration (_text: string): ReturnType { - throw new Error('Method not implemented.'); - } - - getDefaultModel () { - return 'usage-limited'; - } -} \ No newline at end of file