Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions src/api/providers/__tests__/claude-code.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,142 @@ describe("ClaudeCodeHandler", () => {
await expect(iterator.next()).rejects.toThrow()
})

test("should suppress verbose 5-hour usage limit errors", async () => {
const systemPrompt = "You are a helpful assistant"
const messages = [{ role: "user" as const, content: "Hello" }]

// Mock async generator that yields a 5-hour usage limit error
const mockGenerator = async function* (): AsyncGenerator<ClaudeCodeMessage | string> {
yield {
type: "assistant" as const,
message: {
id: "msg_123",
type: "message",
role: "assistant",
model: "claude-3-5-sonnet-20241022",
content: [
{
type: "text",
text: 'API Error: 429 {"error":{"message":"Usage limit reached. Please wait 5 hours before trying again."}}',
},
],
stop_reason: "stop_sequence",
stop_sequence: null,
usage: {
input_tokens: 10,
output_tokens: 20,
},
} as any,
session_id: "session_123",
}
}

mockRunClaudeCode.mockReturnValue(mockGenerator())

const stream = handler.createMessage(systemPrompt, messages)
const results = []

// Should not throw an error - the error should be suppressed
for await (const chunk of stream) {
results.push(chunk)
}

// Should have no results since the error was suppressed
expect(results).toHaveLength(0)
})

test("should suppress various 5-hour limit error messages", async () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great test coverage! Consider adding an edge case test where the error message contains "429" and "rate limit" but isn't actually a rate limit error (e.g., if these terms appear in user-generated content). This would help ensure we're not over-suppressing errors.

const systemPrompt = "You are a helpful assistant"
const messages = [{ role: "user" as const, content: "Hello" }]

const errorMessages = [
'API Error: 429 {"error":{"message":"5-hour usage limit exceeded"}}',
'API Error: 429 {"error":{"message":"Five hour rate limit reached"}}',
'API Error: 429 {"error":{"message":"Rate limit: Please wait before making another request"}}',
'API Error: 429 {"error":{"message":"Usage limit has been reached for this period"}}',
]

for (const errorMessage of errorMessages) {
// Mock async generator that yields the error
const mockGenerator = async function* (): AsyncGenerator<ClaudeCodeMessage | string> {
yield {
type: "assistant" as const,
message: {
id: "msg_123",
type: "message",
role: "assistant",
model: "claude-3-5-sonnet-20241022",
content: [
{
type: "text",
text: errorMessage,
},
],
stop_reason: "stop_sequence",
stop_sequence: null,
usage: {
input_tokens: 10,
output_tokens: 20,
},
} as any,
session_id: "session_123",
}
}

mockRunClaudeCode.mockReturnValue(mockGenerator())

const stream = handler.createMessage(systemPrompt, messages)
const results = []

// Should not throw an error - the error should be suppressed
for await (const chunk of stream) {
results.push(chunk)
}

// Should have no results since the error was suppressed
expect(results).toHaveLength(0)
}
})

test("should not suppress non-429 API errors", async () => {
const systemPrompt = "You are a helpful assistant"
const messages = [{ role: "user" as const, content: "Hello" }]

// Mock async generator that yields a non-429 error
const mockGenerator = async function* (): AsyncGenerator<ClaudeCodeMessage | string> {
yield {
type: "assistant" as const,
message: {
id: "msg_123",
type: "message",
role: "assistant",
model: "claude-3-5-sonnet-20241022",
content: [
{
type: "text",
text: 'API Error: 500 {"error":{"message":"Internal server error"}}',
},
],
stop_reason: "stop_sequence",
stop_sequence: null,
usage: {
input_tokens: 10,
output_tokens: 20,
},
} as any,
session_id: "session_123",
}
}

mockRunClaudeCode.mockReturnValue(mockGenerator())

const stream = handler.createMessage(systemPrompt, messages)
const iterator = stream[Symbol.asyncIterator]()

// Should throw an error for non-429 errors
await expect(iterator.next()).rejects.toThrow('{"error":{"message":"Internal server error"}}')
})

test("should log warning for unsupported tool_use content", async () => {
const systemPrompt = "You are a helpful assistant"
const messages = [{ role: "user" as const, content: "Hello" }]
Expand Down
14 changes: 14 additions & 0 deletions src/api/providers/claude-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,20 @@ export class ClaudeCodeHandler extends BaseProvider implements ApiHandler {
throw new Error(content.text)
}

// Check if this is a 5-hour usage limit error (429 status)
// These errors should be handled gracefully without showing verbose details
if (
content.text.includes("429") &&
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this detection logic robust enough? I'm checking for "429" as a string and then looking for keywords, but what if a legitimate non-error response happens to contain these terms? Should we validate the error structure more strictly first (e.g., checking that we're actually in an error object)?

(error.error?.message?.toLowerCase().includes("usage limit") ||
error.error?.message?.toLowerCase().includes("rate limit") ||
error.error?.message?.toLowerCase().includes("5-hour") ||
error.error?.message?.toLowerCase().includes("five hour"))
) {
// Don't throw the verbose error - let the UI handle it with a concise message
// The UI already shows a grey notice for this case
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For future maintainers, it might be worth documenting where this UI handling occurs. Could we add a more specific reference, like: "The UI shows a grey notice via [specific component/file]"?

return
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a debug log here? Silently suppressing the error works for the UI, but it might be helpful for developers to track when rate limits are being hit. Something like:

Suggested change
return
// Don't throw the verbose error - let the UI handle it with a concise message
// The UI already shows a grey notice for this case
console.debug('[ClaudeCode] 5-hour usage limit detected, suppressing verbose error');
return

}

if (error.error.message.includes("Invalid model name")) {
throw new Error(
content.text + `\n\n${t("common:errors.claudeCode.apiKeyModelPlanMismatch")}`,
Expand Down
Loading