Skip to content

refactor: enhance scalability of openrouter client#11

Merged
Simplereally merged 2 commits intomainfrom
refactor/scalable-openrouter-client
Jan 13, 2026
Merged

refactor: enhance scalability of openrouter client#11
Simplereally merged 2 commits intomainfrom
refactor/scalable-openrouter-client

Conversation

@Simplereally
Copy link
Copy Markdown
Owner

@Simplereally Simplereally commented Jan 13, 2026

Summary by CodeRabbit

  • New Features

    • Added cache clearing functionality
    • Added configurable timeout and retry options for requests
    • Introduced standardized error handling
    • Added support for disabling reasoning in requests
  • Tests

    • Added comprehensive test suite covering client behavior and error scenarios

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Jan 13, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
bloomstudio Ready Ready Preview, Comment Jan 13, 2026 11:02am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 13, 2026

Warning

Rate limit exceeded

@Simplereally has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 18 minutes and 56 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 5e88cb6 and 512c248.

📒 Files selected for processing (1)
  • lib/openrouter/openrouter-client.ts
📝 Walkthrough

Walkthrough

This PR introduces a singleton-pattern OpenRouter client with caching, client-side retry logic with exponential backoff, per-request timeouts, and abort signal handling. It extends GenerateOptions with timeoutMs and maxRetries parameters and adds comprehensive test coverage for the new functionality.

Changes

Cohort / File(s) Summary
Core Client Implementation
lib/openrouter/openrouter-client.ts
Implements singleton client creation with caching, HTTP keep-alive, retry infrastructure (exponential backoff with jitter), per-request timeouts, and configurable maxRetries. Adds OpenRouterError class, clearClientCache() method, and extends generate() with retry loop and abort handling; generateStream() gains optional disableReasoning config support.
Public API Export
lib/openrouter/index.ts
Adds clearClientCache to the public export surface.
Test Coverage
lib/openrouter/openrouter-client.test.ts
New comprehensive test suite validating singleton caching behavior, cache invalidation, API key dependency, retry logic with max limits, abort signal handling, disableReasoning flag integration, and OpenRouterError validation across generate/generateStream paths.

Sequence Diagrams

sequenceDiagram
    participant Client as Application
    participant Cache as ClientCache
    participant Gen as generate()
    participant Fetch as highThroughputFetch
    participant API as OpenRouter API

    Client->>Cache: createOpenRouterClient(apiKey)
    Cache->>Cache: Check if cached & apiKey matches
    Cache-->>Client: Return cached client

    Client->>Gen: generate(prompt, options)
    
    loop Retry Loop (maxRetries)
        Gen->>Fetch: streamText(request, {timeout})
        Fetch->>API: HTTP POST
        alt Success
            API-->>Fetch: Stream response
            Fetch-->>Gen: Process stream
            Gen->>Gen: Extract & return text
            Gen-->>Client: Return result
        else Retryable Error
            API-->>Fetch: Error (5xx, etc.)
            Fetch-->>Gen: Throw error
            Gen->>Gen: calculateRetryDelay(exponential backoff)
            Gen->>Gen: sleep(delay)
            Note over Gen: Retry next iteration
        else Non-Retryable or Max Retries Exceeded
            Gen-->>Client: Throw OpenRouterError
        end
    end
Loading
sequenceDiagram
    participant Client as Application
    participant Clear as clearClientCache()
    participant Cache as ClientCache

    Client->>Clear: clearClientCache()
    Clear->>Cache: Reset cachedClient & cachedApiKey
    Cache->>Cache: Clear singleton state
    Clear-->>Client: void

    Note over Client: Next createOpenRouterClient() call<br/>will create fresh instance
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 A rabbit hops through caches spry,
With retries bouncing, oh so high!
Timeouts tick and backoff sways,
Singleton streams through network's maze! 🌳

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main enhancement: adding scalability features (singleton pattern, HTTP keep-alive, exponential backoff retry logic, configurable timeouts, and caching) to the OpenRouter client.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @lib/openrouter/openrouter-client.ts:
- Around line 204-206: The GenerateOptions.timeoutMs field is declared but never
used; update the generate function to read timeoutMs from its options and pass
it into highThroughputFetch (instead of always using DEFAULT_TIMEOUT_MS), or
remove timeoutMs from GenerateOptions if per-request timeouts are not supported;
specifically, modify the generate function to extract timeoutMs (fallback to
DEFAULT_TIMEOUT_MS when undefined) and forward that value to highThroughputFetch
so each request honors the caller-provided timeout.
🧹 Nitpick comments (3)
lib/openrouter/openrouter-client.ts (2)

165-170: Fragile status code detection in error messages.

Checking for status codes by string matching in error messages (e.g., "429") could lead to false positives. An error message like "User 429 not found" or "Connection to port 5000 failed" would incorrectly match.

Consider checking if the error has a status property instead, or use a more specific pattern like matching "status 429" or "HTTP 429".

Proposed improvement
 function isRetryableError(error: unknown): boolean {
   if (error instanceof Error) {
     const message = error.message.toLowerCase()
     // Network errors
     if (
       message.includes("fetch failed") ||
       message.includes("network") ||
       message.includes("econnreset") ||
       message.includes("etimedout") ||
       message.includes("socket hang up")
     ) {
       return true
     }
-    // Rate limiting or server errors (check status in error message)
-    for (const status of RETRY_CONFIG.retryableStatuses) {
-      if (message.includes(String(status))) {
-        return true
-      }
-    }
+    // Check for status property on error object
+    const errorWithStatus = error as { status?: number }
+    if (
+      typeof errorWithStatus.status === "number" &&
+      RETRY_CONFIG.retryableStatuses.includes(errorWithStatus.status as 408 | 429 | 500 | 502 | 503 | 504)
+    ) {
+      return true
+    }
   }
   return false
 }

297-302: Hardcoded status code loses error context.

The OpenRouterError always uses status 500 regardless of the actual error. If the original error was a 429 (rate limit) or 408 (timeout), this information is lost.

Consider extracting and preserving the original status if available.

Proposed fix
+function extractStatusFromError(error: Error): number | undefined {
+  const errorWithStatus = error as { status?: number }
+  if (typeof errorWithStatus.status === "number") {
+    return errorWithStatus.status
+  }
+  return undefined
+}

   // Wrap in OpenRouterError for consistent error handling
   throw new OpenRouterError(
     lastError?.message ?? "Generation failed after retries",
     "GENERATION_FAILED",
-    500
+    lastError ? extractStatusFromError(lastError) ?? 500 : 500
   )
lib/openrouter/openrouter-client.test.ts (1)

55-58: Unusual semicolon placement and as casts.

The leading semicolons (;(getOpenRouterApiKey as Mock)) are a defensive pattern against ASI but look unusual. Consider using void operator or restructuring.

Per coding guidelines, avoid as casts. Consider using a type-safe mock helper instead.

Suggested improvement
+// Type-safe mock access
+const mockGetOpenRouterApiKey = vi.mocked(getOpenRouterApiKey)
+const mockCreateOpenRouter = vi.mocked(createOpenRouter)
+const mockStreamText = vi.mocked(streamText)

 beforeEach(() => {
     // Reset all mocks and clear cache before each test
     vi.clearAllMocks()
     clearClientCache()

-        // Setup default mocks
-        ; (getOpenRouterApiKey as Mock).mockReturnValue(mockApiKey)
-        ; (createOpenRouter as Mock).mockReturnValue(mockModelFn)
+    // Setup default mocks
+    mockGetOpenRouterApiKey.mockReturnValue(mockApiKey)
+    mockCreateOpenRouter.mockReturnValue(mockModelFn)
     mockOpenRouterInstance.mockReturnValue("model-instance")
 })
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8fcbf45 and 5e88cb6.

📒 Files selected for processing (3)
  • lib/openrouter/index.ts
  • lib/openrouter/openrouter-client.test.ts
  • lib/openrouter/openrouter-client.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Always leverage TypeScript (strict mode); avoid usage of any which defeats the point of using TypeScript.
Let TypeScript inference work; export explicit prop types for shared/public components.
No any type in TypeScript. Use unknown and narrow instead.
Avoid as casts in TypeScript; prefer satisfies, generics, and runtime validation.
@ts-expect-error allowed only with a comment explaining why + link/TODO to remove.

Files:

  • lib/openrouter/index.ts
  • lib/openrouter/openrouter-client.test.ts
  • lib/openrouter/openrouter-client.ts
🔇 Additional comments (10)
lib/openrouter/openrouter-client.ts (4)

98-122: LGTM!

The singleton pattern is correctly implemented with API key validation and cache invalidation when the key changes.


127-130: LGTM!

Simple and effective cache clearing implementation.


313-338: LGTM!

The generateStream function correctly delegates to streamText and the documented lack of retry logic for streaming is appropriate.


347-356: LGTM!

OpenRouterError is a well-structured custom error class.

lib/openrouter/index.ts (1)

7-10: LGTM!

Clean barrel export that correctly exposes the new clearClientCache function alongside existing exports.

lib/openrouter/openrouter-client.test.ts (5)

65-103: LGTM!

Good test coverage for the singleton client creation, including cache reuse and API key change scenarios.


105-115: LGTM!

Correctly validates that clearing the cache forces a new client to be created.


117-258: LGTM!

Comprehensive test coverage for the generate function including retry behavior, abort handling, and custom options. The retry count expectations correctly account for the initial attempt plus retries.


260-294: LGTM!

Good coverage for generateStream including the disableReasoning option propagation.


296-311: LGTM!

Good coverage for the OpenRouterError class construction with and without optional status.

Comment thread lib/openrouter/openrouter-client.ts
Comment thread lib/openrouter/openrouter-client.ts
@Simplereally Simplereally merged commit fba3dc2 into main Jan 13, 2026
1 of 3 checks passed
@Simplereally Simplereally deleted the refactor/scalable-openrouter-client branch January 13, 2026 11:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant