mirror of
https://github.com/THU-MAIC/OpenMAIC.git
synced 2026-10-16 05:19:50 +08:00
fix(tts): surface provider rate-limit errors as HTTP 429 (#644)
TTS providers throw a plain `Error` on any non-OK response, and the `/api/generate/tts` route catches everything as `GENERATION_FAILED` (500), so an upstream 429 (quota/concurrency) was indistinguishable from a real failure — clients had no signal to retry/back off. `TTSRateLimitError` already existed for exactly this (with a TODO noting the route collapses everything uniformly), but only Doubao threw it and the route never mapped it. - Add a `RATE_LIMITED` API error code. - Map upstream HTTP 429 to `TTSRateLimitError` at every provider's `!response.ok` site via a shared `throwIfTtsRateLimited` helper. - Return `429 RATE_LIMITED` from the TTS route when a `TTSRateLimitError` is caught (all other errors unchanged). Purely additive: non-429 responses keep their existing error messages. Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
This commit is contained in:
co-authored by
ly-wang19
Claude Opus 4.8
wyuc
parent
4da3a3a8a0
commit
2586cb5e05
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import { NextRequest } from 'next/server';
|
||||
import { generateTTS } from '@/lib/audio/tts-providers';
|
||||
import { generateTTS, TTSRateLimitError } from '@/lib/audio/tts-providers';
|
||||
import {
|
||||
isServerConfiguredProvider,
|
||||
resolveTTSApiKey,
|
||||
@@ -113,6 +113,9 @@ export async function POST(req: NextRequest) {
|
||||
`TTS generation failed [provider=${ttsProviderId ?? 'unknown'}, voice=${ttsVoice ?? 'unknown'}, audioId=${audioId ?? 'unknown'}]:`,
|
||||
error,
|
||||
);
|
||||
if (error instanceof TTSRateLimitError) {
|
||||
return apiError('RATE_LIMITED', 429, error.message);
|
||||
}
|
||||
return apiError(
|
||||
'GENERATION_FAILED',
|
||||
500,
|
||||
|
||||
@@ -127,6 +127,17 @@ export class TTSRateLimitError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map an upstream HTTP 429 to a typed {@link TTSRateLimitError} so the API route
|
||||
* can surface it as 429 instead of a generic 500. Call right after an
|
||||
* `!response.ok` check, before building the provider-specific error message.
|
||||
*/
|
||||
export function throwIfTtsRateLimited(provider: string, status: number): void {
|
||||
if (status === 429) {
|
||||
throw new TTSRateLimitError(provider, `${provider} TTS rate limit exceeded (HTTP 429)`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate speech using specified TTS provider
|
||||
*/
|
||||
@@ -205,6 +216,7 @@ async function generateOpenAITTS(
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throwIfTtsRateLimited('OpenAI', response.status);
|
||||
const error = await response.json().catch(() => ({ error: response.statusText }));
|
||||
throw new Error(`OpenAI TTS API error: ${error.error?.message || response.statusText}`);
|
||||
}
|
||||
@@ -248,6 +260,7 @@ async function generateLemonadeTTS(
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throwIfTtsRateLimited('Lemonade', response.status);
|
||||
throw new Error(`Lemonade TTS API error: ${await readTTSApiError(response)}`);
|
||||
}
|
||||
|
||||
@@ -314,6 +327,7 @@ async function generateVoxCPMTTS(
|
||||
: await postVoxCPMVLLMOmni(baseUrl, request, config);
|
||||
|
||||
if (!response.ok) {
|
||||
throwIfTtsRateLimited('VoxCPM', response.status);
|
||||
throw new Error(`VoxCPM TTS API error: ${await readTTSApiError(response)}`);
|
||||
}
|
||||
|
||||
@@ -559,6 +573,7 @@ async function generateAzureTTS(
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throwIfTtsRateLimited('Azure', response.status);
|
||||
throw new Error(`Azure TTS API error: ${response.statusText}`);
|
||||
}
|
||||
|
||||
@@ -592,6 +607,7 @@ async function generateGLMTTS(config: TTSModelConfig, text: string): Promise<TTS
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throwIfTtsRateLimited('GLM', response.status);
|
||||
const errorText = await response.text().catch(() => response.statusText);
|
||||
let errorMessage = `GLM TTS API error: ${errorText}`;
|
||||
try {
|
||||
@@ -642,6 +658,7 @@ async function generateQwenTTS(config: TTSModelConfig, text: string): Promise<TT
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throwIfTtsRateLimited('Qwen', response.status);
|
||||
const errorText = await response.text().catch(() => response.statusText);
|
||||
throw new Error(`Qwen TTS API error: ${errorText}`);
|
||||
}
|
||||
@@ -708,6 +725,7 @@ async function generateMiniMaxTTS(
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throwIfTtsRateLimited('MiniMax', response.status);
|
||||
const errorText = await response.text().catch(() => response.statusText);
|
||||
throw new Error(`MiniMax TTS API error: ${errorText}`);
|
||||
}
|
||||
@@ -773,6 +791,7 @@ async function generateElevenLabsTTS(
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throwIfTtsRateLimited('ElevenLabs', response.status);
|
||||
const errorText = await response.text().catch(() => response.statusText);
|
||||
throw new Error(`ElevenLabs TTS API error: ${errorText || response.statusText}`);
|
||||
}
|
||||
@@ -853,6 +872,7 @@ async function generateDoubaoTTS(
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throwIfTtsRateLimited('Doubao', response.status);
|
||||
const errorText = await response.text().catch(() => response.statusText);
|
||||
throw new Error(`Doubao TTS API error (${response.status}): ${errorText}`);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ export const API_ERROR_CODES = {
|
||||
TOO_MANY_REDIRECTS: 'TOO_MANY_REDIRECTS',
|
||||
CONTENT_SENSITIVE: 'CONTENT_SENSITIVE',
|
||||
UPSTREAM_ERROR: 'UPSTREAM_ERROR',
|
||||
RATE_LIMITED: 'RATE_LIMITED',
|
||||
GENERATION_FAILED: 'GENERATION_FAILED',
|
||||
TRANSCRIPTION_FAILED: 'TRANSCRIPTION_FAILED',
|
||||
PARSE_FAILED: 'PARSE_FAILED',
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { throwIfTtsRateLimited, TTSRateLimitError } from '@/lib/audio/tts-providers';
|
||||
|
||||
describe('throwIfTtsRateLimited', () => {
|
||||
it('throws a typed TTSRateLimitError on HTTP 429', () => {
|
||||
expect(() => throwIfTtsRateLimited('OpenAI', 429)).toThrow(TTSRateLimitError);
|
||||
try {
|
||||
throwIfTtsRateLimited('OpenAI', 429);
|
||||
throw new Error('should have thrown');
|
||||
} catch (e) {
|
||||
expect(e).toBeInstanceOf(TTSRateLimitError);
|
||||
expect((e as TTSRateLimitError).provider).toBe('OpenAI');
|
||||
}
|
||||
});
|
||||
|
||||
it('does not throw on non-429 statuses', () => {
|
||||
expect(() => throwIfTtsRateLimited('OpenAI', 200)).not.toThrow();
|
||||
expect(() => throwIfTtsRateLimited('OpenAI', 401)).not.toThrow();
|
||||
expect(() => throwIfTtsRateLimited('OpenAI', 500)).not.toThrow();
|
||||
expect(() => throwIfTtsRateLimited('OpenAI', 503)).not.toThrow();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user