feat(llm): return structured rate-limit info on 429 responses
Add RateLimitInfo interface and CompletionResult type union so callers can distinguish 429 rate limits from other errors. Includes isRateLimited type guard and per-request model override in CompletionRequest. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -62,11 +62,63 @@ describe('openRouterClient', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns null on HTTP error', async () => {
|
||||
it('returns rateLimited result with reset timestamp on 429', async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 429,
|
||||
statusText: 'Too Many Requests',
|
||||
json: () => Promise.resolve({
|
||||
error: {
|
||||
code: 429,
|
||||
message: 'Rate limit exceeded',
|
||||
metadata: {
|
||||
headers: {
|
||||
'X-RateLimit-Reset': '1741305600000',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const client = createOpenRouterClient(mockConfig);
|
||||
const result = await client.complete({
|
||||
system: 'sys',
|
||||
user: 'usr',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
rateLimited: true,
|
||||
resetAt: 1741305600000,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns rateLimited result with null resetAt when 429 has no reset header', async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 429,
|
||||
statusText: 'Too Many Requests',
|
||||
json: () => Promise.resolve({
|
||||
error: { code: 429, message: 'Rate limit exceeded' },
|
||||
}),
|
||||
});
|
||||
|
||||
const client = createOpenRouterClient(mockConfig);
|
||||
const result = await client.complete({
|
||||
system: 'sys',
|
||||
user: 'usr',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
rateLimited: true,
|
||||
resetAt: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null on non-429 HTTP errors', async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error',
|
||||
});
|
||||
|
||||
const client = createOpenRouterClient(mockConfig);
|
||||
|
||||
@@ -6,15 +6,27 @@ const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
||||
export interface CompletionRequest extends RenderedPrompt {
|
||||
maxTokens?: number;
|
||||
temperature?: number;
|
||||
model?: string;
|
||||
}
|
||||
|
||||
export interface RateLimitInfo {
|
||||
rateLimited: true;
|
||||
resetAt: number | null;
|
||||
}
|
||||
|
||||
export type CompletionResult = string | null | RateLimitInfo;
|
||||
|
||||
export function isRateLimited(result: CompletionResult): result is RateLimitInfo {
|
||||
return result != null && typeof result === 'object' && 'rateLimited' in result;
|
||||
}
|
||||
|
||||
export interface OpenRouterClient {
|
||||
complete(request: CompletionRequest): Promise<string | null>;
|
||||
complete(request: CompletionRequest): Promise<CompletionResult>;
|
||||
}
|
||||
|
||||
export function createOpenRouterClient(config: LlmConfig): OpenRouterClient {
|
||||
return {
|
||||
async complete(request: CompletionRequest): Promise<string | null> {
|
||||
async complete(request: CompletionRequest): Promise<CompletionResult> {
|
||||
try {
|
||||
const response = await fetch(OPENROUTER_URL, {
|
||||
method: 'POST',
|
||||
@@ -24,7 +36,7 @@ export function createOpenRouterClient(config: LlmConfig): OpenRouterClient {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: config.model,
|
||||
model: request.model ?? config.model,
|
||||
max_tokens: request.maxTokens ?? config.maxTokens,
|
||||
temperature: request.temperature ?? config.temperature,
|
||||
messages: [
|
||||
@@ -35,6 +47,16 @@ export function createOpenRouterClient(config: LlmConfig): OpenRouterClient {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 429) {
|
||||
try {
|
||||
const data = await response.json();
|
||||
const resetStr = data?.error?.metadata?.headers?.['X-RateLimit-Reset'];
|
||||
const resetAt = resetStr ? Number(resetStr) : null;
|
||||
return { rateLimited: true, resetAt };
|
||||
} catch {
|
||||
return { rateLimited: true, resetAt: null };
|
||||
}
|
||||
}
|
||||
console.warn(`OpenRouter API error: ${response.status} ${response.statusText}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user