Skip to content

feat: add LiteLLM as AI gateway provider#15

Open
RheagalFire wants to merge 1 commit into
framerslab:masterfrom
RheagalFire:feat/add-litellm-provider
Open

feat: add LiteLLM as AI gateway provider#15
RheagalFire wants to merge 1 commit into
framerslab:masterfrom
RheagalFire:feat/add-litellm-provider

Conversation

@RheagalFire
Copy link
Copy Markdown

@RheagalFire RheagalFire commented Jun 6, 2026

Summary

  • Adds LiteLLM as a new AI provider, following the existing GroqProvider delegation pattern. LiteLLMProvider wraps OpenAIProvider with the user's LiteLLM proxy URL, enabling access to 100+ LLM providers (Anthropic, Google, Bedrock, Azure, Ollama, etc.) through a single OpenAI-compatible gateway.
  • Users who self-host a LiteLLM proxy can consolidate multiple provider keys, enable fallback routing, and use any model configured on the proxy without adding new provider packages.

Checklist

  • Tests added/updated
  • Docs updated (README or typedoc)
  • Conventional commit title/body

Changes:

  • src/core/llm/providers/implementations/LiteLLMProvider.ts - new provider (delegates to OpenAIProvider, dynamic model discovery via /v1/models)
  • src/core/llm/providers/AIModelProviderManager.ts - registered 'litellm' case
  • src/core/llm/providers/__tests__/LiteLLMProvider.test.ts - 12 unit tests
$ npx vitest run src/core/llm/providers/__tests__/LiteLLMProvider.test.ts
 ✓ initialization > throws when API key is missing
 ✓ initialization > initializes with valid config
 ✓ initialization > uses custom default model
 ✓ initialization > delegates to OpenAIProvider with proxy baseURL
 ✓ generateCompletion > delegates to OpenAIProvider
 ✓ generateCompletionStream > yields streamed chunks from delegate
 ✓ generateEmbeddings > delegates to OpenAIProvider
 ✓ listAvailableModels > fetches models from proxy /v1/models
 ✓ listAvailableModels > returns empty array on fetch error
 ✓ listAvailableModels > returns empty array on network failure
 ✓ checkHealth > delegates to OpenAIProvider
 ✓ shutdown > delegates and resets state
 12 passed

Live E2E against a real LiteLLM proxy (routing to Claude via Azure Foundry):

$ litellm --config config.yaml --port 4000

const provider = new LiteLLMProvider();
await provider.initialize({
  apiKey: 'sk-test',
  baseURL: 'http://localhost:4000/v1',
});

Models: [ 'anthropic/claude-sonnet-4-6' ]
content: OK
finishReason: stop
usage: { promptTokens: 13, completionTokens: 4, totalTokens: 17 }

Usage:

// In agent config:
{
  providers: [{
    providerId: 'litellm',
    enabled: true,
    config: {
      apiKey: process.env.LITELLM_API_KEY,
      baseURL: 'http://localhost:4000/v1',
      defaultModelId: 'anthropic/claude-sonnet-4-6',
    },
  }],
}

Additive only. No existing providers modified. Zero new dependencies.

Summary by Sourcery

Add a new LiteLLM provider that delegates to the existing OpenAI provider and register it with the AI model provider manager.

New Features:

  • Introduce LiteLLMProvider to access multiple LLM backends via an OpenAI-compatible LiteLLM proxy.
  • Enable dynamic LiteLLM model discovery through the proxy's /v1/models endpoint and integrate LiteLLM into the provider manager under the 'litellm' ID.

Tests:

  • Add unit tests covering LiteLLMProvider initialization, delegation behavior, model listing, health checks, and shutdown.

Summary by CodeRabbit

  • New Features
    • Added support for LiteLLM provider, enabling integration with self-hosted LiteLLM proxy servers using OpenAI-compatible APIs. Users can now configure and connect to self-hosted LiteLLM instances for language model completions and embeddings.

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 6, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces LiteLLMProvider, a new LLM provider implementation that wraps OpenAIProvider to interface with self-hosted LiteLLM proxies. The provider includes configuration, initialization with proxy settings, request delegation, dynamic model discovery, and full test coverage. Integration into the manager's provider registry is minimal.

Changes

LiteLLM Provider Support

Layer / File(s) Summary
Provider Configuration Contract
src/core/llm/providers/implementations/LiteLLMProvider.ts
LiteLLMProviderConfig interface captures API key (required) and optional proxy base URL, default model ID, and request timeout.
Provider Implementation - Core Methods
src/core/llm/providers/implementations/LiteLLMProvider.ts
LiteLLMProvider class initializes by validating the API key and configuring an OpenAIProvider delegate with proxy settings. Completion, streaming, and embedding methods pass through to the delegate.
Provider Implementation - Model Discovery & Lifecycle
src/core/llm/providers/implementations/LiteLLMProvider.ts
listAvailableModels calls the proxy's /v1/models endpoint and parses returned model metadata, with optional capability filtering. getModelInfo, checkHealth, and shutdown complete the lifecycle, delegating where appropriate and resetting state on shutdown.
Manager Integration
src/core/llm/providers/AIModelProviderManager.ts
LiteLLMProvider is imported and instantiated in the provider switch when providerId lowercases to litellm.
Test Suite - Setup & Mocking
src/core/llm/providers/__tests__/LiteLLMProvider.test.ts
Vitest suite with complete mock of OpenAIProvider to isolate unit testing from external HTTP calls.
Test Suite - Initialization & Core Delegation
src/core/llm/providers/__tests__/LiteLLMProvider.test.ts
Tests verify API key validation, successful initialization with proxy configuration, and delegation of completions, streaming completions, and embeddings to the mocked OpenAIProvider.
Test Suite - Model Discovery & Lifecycle
src/core/llm/providers/__tests__/LiteLLMProvider.test.ts
Tests for listAvailableModels success and error modes (HTTP error, network failure) via stubbed fetch, and verification of checkHealth and shutdown delegation with state reset.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

A rabbit hops through proxies fair,
LiteLLM magic in the air,
Completions flow, models align,
OpenAI wrapped in perfect design.
Tests stand guard, all passing bright,
New provider shining in the light! 🐰✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding LiteLLM as a new AI gateway provider, which is the primary objective of the PR.
Description check ✅ Passed The description comprehensively covers the summary, rationale, implementation details, test results, usage example, and checklist status, matching the template structure and requirements.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


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.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Jun 6, 2026

Reviewer's Guide

Adds a new LiteLLMProvider that wraps the existing OpenAIProvider, wires it into AIModelProviderManager under the litellm providerId, and introduces unit tests covering initialization, delegation behavior, dynamic model discovery via the LiteLLM /v1/models endpoint, and shutdown/health flows.

Sequence diagram for LiteLLMProvider model discovery via /v1/models

sequenceDiagram
actor Client
participant LiteLLMProvider
participant LiteLLMProxy

Client->>LiteLLMProvider: listAvailableModels(filter)
LiteLLMProvider->>LiteLLMProxy: fetch /v1/models
LiteLLMProxy-->>LiteLLMProvider: models JSON
LiteLLMProvider-->>Client: ModelInfo[]
Loading

File-Level Changes

Change Details Files
Introduce LiteLLMProvider as a thin wrapper around OpenAIProvider configured to talk to a LiteLLM proxy, including dynamic model discovery via the proxy's /v1/models endpoint.
  • Define LiteLLMProviderConfig with apiKey, baseURL, defaultModelId, and requestTimeout options and defaults.
  • Implement LiteLLMProvider class that satisfies IProvider, stores proxy URL/key, and delegates completions, streaming, embeddings, health checks, and shutdown to an internal OpenAIProvider instance.
  • Implement initialize() to validate apiKey, set default model/base URL/timeout, configure the OpenAIProvider delegate with the proxy baseURL, and mark the provider as initialized.
  • Add listAvailableModels() to call the proxy's /v1/models endpoint using fetch, map the returned models into ModelInfo objects, support optional capability filtering, and gracefully return an empty list on HTTP or network errors.
  • Add getModelInfo() helper that resolves a single model by ID using listAvailableModels().
src/core/llm/providers/implementations/LiteLLMProvider.ts
Register the new LiteLLMProvider in the AIModelProviderManager so it can be instantiated from configuration via providerId litellm.
  • Extend the AIModelProviderManager providerId switch to handle the 'litellm' case by constructing a LiteLLMProvider instance.
src/core/llm/providers/AIModelProviderManager.ts
Add unit tests to validate LiteLLMProvider behavior, including configuration validation, delegation, model listing, error handling, and lifecycle methods.
  • Test initialization error when apiKey is missing and successful initialization with valid configuration and custom default model.
  • Verify delegation of generateCompletion, generateCompletionStream, generateEmbeddings, checkHealth, and shutdown to the OpenAIProvider delegate.
  • Test listAvailableModels against mocked /v1/models responses, including happy path, HTTP error, and network failure, ensuring correct mapping to ModelInfo and empty-array fallbacks.
src/core/llm/providers/__tests__/LiteLLMProvider.test.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

Review Summary by Qodo

Add LiteLLM as AI gateway provider with dynamic model discovery

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Adds LiteLLMProvider as new AI gateway supporting 100+ LLM providers
• Delegates to OpenAIProvider with custom proxy baseURL configuration
• Implements dynamic model discovery via proxy /v1/models endpoint
• Includes 12 comprehensive unit tests covering initialization and delegation
Diagram
flowchart LR
  A["LiteLLMProvider"] -->|"delegates to"| B["OpenAIProvider"]
  A -->|"queries"| C["LiteLLM Proxy /v1/models"]
  C -->|"returns"| D["Available Models"]
  B -->|"routes requests"| E["100+ LLM Providers"]
  A -->|"registered in"| F["AIModelProviderManager"]

Loading

Grey Divider

File Changes

1. src/core/llm/providers/implementations/LiteLLMProvider.ts ✨ Enhancement +209/-0

LiteLLMProvider implementation with proxy delegation

• New provider implementation wrapping OpenAIProvider with LiteLLM proxy configuration
• Implements IProvider interface with delegation pattern for all core methods
• Dynamic model discovery via /v1/models endpoint with error handling
• Supports custom baseURL, API key, and default model configuration

src/core/llm/providers/implementations/LiteLLMProvider.ts


2. src/core/llm/providers/__tests__/LiteLLMProvider.test.ts 🧪 Tests +225/-0

Comprehensive test suite for LiteLLMProvider

• 12 unit tests covering initialization, delegation, and error scenarios
• Tests verify API key validation, config handling, and OpenAIProvider delegation
• Tests for dynamic model discovery with success and failure cases
• Tests for streaming, embeddings, health checks, and shutdown

src/core/llm/providers/tests/LiteLLMProvider.test.ts


3. src/core/llm/providers/AIModelProviderManager.ts ✨ Enhancement +4/-0

Register LiteLLMProvider in provider manager

• Added import for LiteLLMProvider and LiteLLMProviderConfig
• Registered 'litellm' case in provider factory switch statement
• Instantiates LiteLLMProvider when providerId matches 'litellm'

src/core/llm/providers/AIModelProviderManager.ts


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented Jun 6, 2026

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0)

Grey Divider


Action required

1. Unbounded model fetch 🐞 Bug ☼ Reliability
Description
LiteLLMProvider.listAvailableModels() calls fetch() without any timeout/AbortController, so
AIModelProviderManager.initialize() and listAllAvailableModels() can hang indefinitely while waiting
for model discovery.
Code

src/core/llm/providers/implementations/LiteLLMProvider.ts[R161-168]

+  public async listAvailableModels(
+    filter?: { capability?: string },
+  ): Promise<ModelInfo[]> {
+    try {
+      const url = this.proxyBaseURL.replace(/\/+$/, '') + '/models';
+      const response = await fetch(url, {
+        headers: { Authorization: `Bearer ${this.proxyApiKey}` },
+      });
Evidence
LiteLLMProvider performs /models discovery via a bare fetch without any abort/timeout, while
AIModelProviderManager awaits provider.listAvailableModels() during both initialization caching and
aggregate model listing, meaning the whole manager operation can block on an unresponsive proxy.

src/core/llm/providers/implementations/LiteLLMProvider.ts[161-169]
src/core/llm/providers/AIModelProviderManager.ts[175-176]
src/core/llm/providers/AIModelProviderManager.ts[207-216]
src/core/llm/providers/AIModelProviderManager.ts[264-286]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`LiteLLMProvider.listAvailableModels()` uses a bare `fetch()` with no timeout/abort handling. Since `AIModelProviderManager` awaits `listAvailableModels()` during initialization and when listing all models, an unresponsive LiteLLM proxy can stall these flows indefinitely.

## Issue Context
Other providers in this codebase bound network calls with request timeouts (often via `AbortController`). LiteLLMProvider already accepts `requestTimeout` in config, but it is only passed to the OpenAI delegate and not applied to `/models` discovery.

## Fix Focus Areas
- src/core/llm/providers/implementations/LiteLLMProvider.ts[106-122]
- src/core/llm/providers/implementations/LiteLLMProvider.ts[161-190]

## Suggested fix
- Persist a `requestTimeoutMs` field on `LiteLLMProvider` during `initialize()` (default to 60000).
- In `listAvailableModels()`, use `AbortController` + `setTimeout(() => controller.abort(), requestTimeoutMs)` and pass `{ signal: controller.signal }` to `fetch`.
- Ensure the timer is cleared in `finally` to avoid leaks.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Discovery failures misroute models 🐞 Bug ≡ Correctness
Description
LiteLLMProvider.listAvailableModels() returns [] for non-OK responses and all exceptions, preventing
AIModelProviderManager from populating modelToProviderMap and causing getProviderForModel() to fall
back to its '/'-prefix heuristic (e.g., routing "anthropic/..." to the Anthropic provider instead of
LiteLLM).
Code

src/core/llm/providers/implementations/LiteLLMProvider.ts[R169-189]

+      if (!response.ok) {
+        return [];
+      }
+      const data = (await response.json()) as {
+        data?: Array<{ id: string; owned_by?: string }>;
+      };
+      const models: ModelInfo[] = (data.data ?? []).map((m) => ({
+        modelId: m.id,
+        providerId: 'litellm',
+        displayName: m.id,
+        capabilities: ['chat'] as string[],
+        supportsStreaming: true,
+        status: 'active' as const,
+      }));
+      if (filter?.capability) {
+        return models.filter((m) => m.capabilities.includes(filter.capability!));
+      }
+      return models;
+    } catch {
+      return [];
+    }
Evidence
LiteLLMProvider returns [] on HTTP failure and in a blanket catch, so the manager won't map LiteLLM
model IDs. When no mapping exists, the manager routes modelId values containing / by treating
the first segment as a providerId, and the codebase uses model IDs like
anthropic/claude-sonnet-4-6, making wrong-provider selection deterministic in that scenario.

src/core/llm/providers/implementations/LiteLLMProvider.ts[169-189]
src/core/llm/providers/AIModelProviderManager.ts[240-258]
src/core/llm/providers/tests/LiteLLMProvider.test.ts[16-21]
src/core/llm/providers/tests/LiteLLMProvider.test.ts[88-95]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`LiteLLMProvider.listAvailableModels()` silently converts both HTTP failures and runtime errors into an empty model list. This means `AIModelProviderManager` cannot map discovered model IDs to the `litellm` provider; when a modelId contains `/`, the manager may route by prefix instead (treating the first segment as a providerId), which can select the wrong provider.

## Issue Context
- Manager routing prefers `modelToProviderMap`, then falls back to splitting `modelId` on `/`.
- LiteLLM model IDs commonly include provider prefixes like `anthropic/claude-...` (tests and docs).

## Fix Focus Areas
- src/core/llm/providers/implementations/LiteLLMProvider.ts[161-190]
- src/core/llm/providers/AIModelProviderManager.ts[240-258]

## Suggested fix
- Do not fully swallow discovery failures:
 - At minimum: `console.warn` (or debug-gated log) with status code / error when `/models` fails.
 - Preferably: throw an Error on non-OK and on caught exceptions so `AIModelProviderManager`’s existing `catch` paths can log the providerId and context.
- To reduce misrouting when discovery is temporarily unavailable, consider returning a minimal fallback catalog containing at least `defaultModelId` (if set) so the manager can still map the primary configured model to `litellm`.
- Update/extend tests to assert the new behavior (log/throw and/or fallback model inclusion).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

3. Fragile global fetch cleanup 🐞 Bug ☼ Reliability
Description
LiteLLMProvider tests stub the global fetch and manually call vi.unstubAllGlobals() at the end of
each test; if a test throws before cleanup, the stub can leak into subsequent tests and cause
cascading failures.
Code

src/core/llm/providers/tests/LiteLLMProvider.test.ts[R160-205]

+  describe('listAvailableModels', () => {
+    it('fetches models from proxy /v1/models', async () => {
+      const mockFetch = vi.fn().mockResolvedValue({
+        ok: true,
+        json: async () => ({
+          data: [
+            { id: 'gpt-4o-mini' },
+            { id: 'anthropic/claude-sonnet-4-6' },
+          ],
+        }),
+      });
+      vi.stubGlobal('fetch', mockFetch);
+
+      await provider.initialize({ apiKey: 'sk-test' });
+      const models = await provider.listAvailableModels();
+
+      expect(models).toHaveLength(2);
+      expect(models[0].modelId).toBe('gpt-4o-mini');
+      expect(models[1].modelId).toBe('anthropic/claude-sonnet-4-6');
+
+      vi.unstubAllGlobals();
+    });
+
+    it('returns empty array on fetch error', async () => {
+      const mockFetch = vi.fn().mockResolvedValue({ ok: false });
+      vi.stubGlobal('fetch', mockFetch);
+
+      await provider.initialize({ apiKey: 'sk-test' });
+      const models = await provider.listAvailableModels();
+
+      expect(models).toEqual([]);
+
+      vi.unstubAllGlobals();
+    });
+
+    it('returns empty array on network failure', async () => {
+      const mockFetch = vi.fn().mockRejectedValue(new Error('ECONNREFUSED'));
+      vi.stubGlobal('fetch', mockFetch);
+
+      await provider.initialize({ apiKey: 'sk-test' });
+      const models = await provider.listAvailableModels();
+
+      expect(models).toEqual([]);
+
+      vi.unstubAllGlobals();
+    });
Evidence
The tests stub globalThis.fetch and only unstub it at the end of each test body, which is not
guaranteed to execute if the test fails mid-way.

src/core/llm/providers/tests/LiteLLMProvider.test.ts[160-205]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The tests manually call `vi.unstubAllGlobals()` within each test. If an assertion fails earlier, cleanup won’t run and the stubbed global `fetch` may persist into later tests.

## Issue Context
This is test hygiene; it makes failures harder to debug when one test contaminates others.

## Fix Focus Areas
- src/core/llm/providers/__tests__/LiteLLMProvider.test.ts[62-69]
- src/core/llm/providers/__tests__/LiteLLMProvider.test.ts[160-205]

## Suggested fix
- Add `afterEach(() => vi.unstubAllGlobals())` (and import `afterEach`) and remove per-test unstub calls.
- Alternatively, wrap stubbing in `try/finally` blocks to guarantee cleanup.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • Consider replacing the console.log calls in initialize and shutdown with the project's existing logging/telemetry mechanism so provider lifecycle events are captured consistently and are easier to control in production.
  • The error message in initialize tightly couples the config-based API key to a specific env var name (LITELLM_API_KEY); either accept the env var directly here or rephrase the message to reflect that the key is provided via the config object rather than requiring a specific environment variable.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider replacing the `console.log` calls in `initialize` and `shutdown` with the project's existing logging/telemetry mechanism so provider lifecycle events are captured consistently and are easier to control in production.
- The error message in `initialize` tightly couples the config-based API key to a specific env var name (`LITELLM_API_KEY`); either accept the env var directly here or rephrase the message to reflect that the key is provided via the config object rather than requiring a specific environment variable.

## Individual Comments

### Comment 1
<location path="src/core/llm/providers/implementations/LiteLLMProvider.ts" line_range="96-98" />
<code_context>
+  /** @inheritdoc */
+  public defaultModelId?: string;
+
+  private delegate = new OpenAIProvider();
+  private proxyBaseURL: string = 'http://localhost:4000/v1';
+  private proxyApiKey: string = '';
+
+  constructor() {}
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Consider guarding public methods that rely on initialization to avoid subtle failures when used before initialize() is called.

Public methods like `generateCompletion`, `generateCompletionStream`, `generateEmbeddings`, and `listAvailableModels` assume `delegate`, `proxyApiKey`, and `proxyBaseURL` have been set by `initialize()`, but they don’t verify `isInitialized`. If `initialize()` is skipped, some calls (e.g., `listAvailableModels`) will run with defaults and an empty API key. Please add a guard (e.g., throw when `!this.isInitialized`) so incorrect usage fails fast rather than producing silent, hard-to-debug behavior.

Suggested implementation:

```typescript
  constructor() {}

  /**
   * Throws if the provider has not been initialized.
   */
  private ensureInitialized(): void {
    if (!this.isInitialized) {
      throw new Error('LiteLLMProvider must be initialized before use. Call initialize() first.');
    }
  }

  /**
   * Initializes the provider by configuring the underlying OpenAI delegate
   * with the LiteLLM proxy URL and API key.
   */

```

```typescript
  public async generateCompletion(
    params: ProviderGenerateCompletionParams,
  ): Promise<ProviderGenerateCompletionResult> {
    this.ensureInitialized();

```

```typescript
  public async generateCompletionStream(
    params: ProviderGenerateCompletionStreamParams,
  ): Promise<ProviderGenerateCompletionStreamResult> {
    this.ensureInitialized();

```

```typescript
  public async generateEmbeddings(
    params: ProviderGenerateEmbeddingsParams,
  ): Promise<ProviderGenerateEmbeddingsResult> {
    this.ensureInitialized();

```

```typescript
  public async listAvailableModels(): Promise<ProviderModelInfo[]> {
    this.ensureInitialized();

```

If any of the method signatures (`generateCompletion`, `generateCompletionStream`, `generateEmbeddings`, `listAvailableModels`) differ slightly (e.g., different parameter type names or return types), adjust the SEARCH patterns to exactly match the existing code blocks and keep only the added `this.ensureInitialized();` call as the first statement in each method body. The new `ensureInitialized` helper can be reused by any future public methods that also require initialization.
</issue_to_address>

### Comment 2
<location path="src/core/llm/providers/implementations/LiteLLMProvider.ts" line_range="166-171" />
<code_context>
+      const response = await fetch(url, {
+        headers: { Authorization: `Bearer ${this.proxyApiKey}` },
+      });
+      if (!response.ok) {
+        return [];
+      }
+      const data = (await response.json()) as {
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Swallowing non-OK responses and errors may make diagnosing configuration issues difficult.

With the current behavior, any non-OK status or thrown error causes `listAvailableModels` to return `[]`, making misconfigurations (bad URL, invalid key, auth failures) indistinguishable from a valid “no models” response. Consider at least logging the HTTP status / error, or exposing a richer error signal, so proxy/config issues are debuggable while still allowing callers to handle an empty list when it’s genuine.

```suggestion
      const response = await fetch(url, {
        headers: { Authorization: `Bearer ${this.proxyApiKey}` },
      });
      if (!response.ok) {
        // Log non-OK responses so misconfigurations (bad URL, invalid key, auth failures, etc.)
        // are visible during debugging, while still returning an empty list to callers.
        console.error(
          `[LiteLLMProvider] listAvailableModels: request to ${url} failed with status ${response.status} ${response.statusText}`,
        );
        return [];
      }
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +96 to +98
private delegate = new OpenAIProvider();
private proxyBaseURL: string = 'http://localhost:4000/v1';
private proxyApiKey: string = '';
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): Consider guarding public methods that rely on initialization to avoid subtle failures when used before initialize() is called.

Public methods like generateCompletion, generateCompletionStream, generateEmbeddings, and listAvailableModels assume delegate, proxyApiKey, and proxyBaseURL have been set by initialize(), but they don’t verify isInitialized. If initialize() is skipped, some calls (e.g., listAvailableModels) will run with defaults and an empty API key. Please add a guard (e.g., throw when !this.isInitialized) so incorrect usage fails fast rather than producing silent, hard-to-debug behavior.

Suggested implementation:

  constructor() {}

  /**
   * Throws if the provider has not been initialized.
   */
  private ensureInitialized(): void {
    if (!this.isInitialized) {
      throw new Error('LiteLLMProvider must be initialized before use. Call initialize() first.');
    }
  }

  /**
   * Initializes the provider by configuring the underlying OpenAI delegate
   * with the LiteLLM proxy URL and API key.
   */
  public async generateCompletion(
    params: ProviderGenerateCompletionParams,
  ): Promise<ProviderGenerateCompletionResult> {
    this.ensureInitialized();
  public async generateCompletionStream(
    params: ProviderGenerateCompletionStreamParams,
  ): Promise<ProviderGenerateCompletionStreamResult> {
    this.ensureInitialized();
  public async generateEmbeddings(
    params: ProviderGenerateEmbeddingsParams,
  ): Promise<ProviderGenerateEmbeddingsResult> {
    this.ensureInitialized();
  public async listAvailableModels(): Promise<ProviderModelInfo[]> {
    this.ensureInitialized();

If any of the method signatures (generateCompletion, generateCompletionStream, generateEmbeddings, listAvailableModels) differ slightly (e.g., different parameter type names or return types), adjust the SEARCH patterns to exactly match the existing code blocks and keep only the added this.ensureInitialized(); call as the first statement in each method body. The new ensureInitialized helper can be reused by any future public methods that also require initialization.

Comment on lines +166 to +171
const response = await fetch(url, {
headers: { Authorization: `Bearer ${this.proxyApiKey}` },
});
if (!response.ok) {
return [];
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): Swallowing non-OK responses and errors may make diagnosing configuration issues difficult.

With the current behavior, any non-OK status or thrown error causes listAvailableModels to return [], making misconfigurations (bad URL, invalid key, auth failures) indistinguishable from a valid “no models” response. Consider at least logging the HTTP status / error, or exposing a richer error signal, so proxy/config issues are debuggable while still allowing callers to handle an empty list when it’s genuine.

Suggested change
const response = await fetch(url, {
headers: { Authorization: `Bearer ${this.proxyApiKey}` },
});
if (!response.ok) {
return [];
}
const response = await fetch(url, {
headers: { Authorization: `Bearer ${this.proxyApiKey}` },
});
if (!response.ok) {
// Log non-OK responses so misconfigurations (bad URL, invalid key, auth failures, etc.)
// are visible during debugging, while still returning an empty list to callers.
console.error(
`[LiteLLMProvider] listAvailableModels: request to ${url} failed with status ${response.status} ${response.statusText}`,
);
return [];
}

Comment on lines +161 to +168
public async listAvailableModels(
filter?: { capability?: string },
): Promise<ModelInfo[]> {
try {
const url = this.proxyBaseURL.replace(/\/+$/, '') + '/models';
const response = await fetch(url, {
headers: { Authorization: `Bearer ${this.proxyApiKey}` },
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Unbounded model fetch 🐞 Bug ☼ Reliability

LiteLLMProvider.listAvailableModels() calls fetch() without any timeout/AbortController, so
AIModelProviderManager.initialize() and listAllAvailableModels() can hang indefinitely while waiting
for model discovery.
Agent Prompt
## Issue description
`LiteLLMProvider.listAvailableModels()` uses a bare `fetch()` with no timeout/abort handling. Since `AIModelProviderManager` awaits `listAvailableModels()` during initialization and when listing all models, an unresponsive LiteLLM proxy can stall these flows indefinitely.

## Issue Context
Other providers in this codebase bound network calls with request timeouts (often via `AbortController`). LiteLLMProvider already accepts `requestTimeout` in config, but it is only passed to the OpenAI delegate and not applied to `/models` discovery.

## Fix Focus Areas
- src/core/llm/providers/implementations/LiteLLMProvider.ts[106-122]
- src/core/llm/providers/implementations/LiteLLMProvider.ts[161-190]

## Suggested fix
- Persist a `requestTimeoutMs` field on `LiteLLMProvider` during `initialize()` (default to 60000).
- In `listAvailableModels()`, use `AbortController` + `setTimeout(() => controller.abort(), requestTimeoutMs)` and pass `{ signal: controller.signal }` to `fetch`.
- Ensure the timer is cleared in `finally` to avoid leaks.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +169 to +189
if (!response.ok) {
return [];
}
const data = (await response.json()) as {
data?: Array<{ id: string; owned_by?: string }>;
};
const models: ModelInfo[] = (data.data ?? []).map((m) => ({
modelId: m.id,
providerId: 'litellm',
displayName: m.id,
capabilities: ['chat'] as string[],
supportsStreaming: true,
status: 'active' as const,
}));
if (filter?.capability) {
return models.filter((m) => m.capabilities.includes(filter.capability!));
}
return models;
} catch {
return [];
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Discovery failures misroute models 🐞 Bug ≡ Correctness

LiteLLMProvider.listAvailableModels() returns [] for non-OK responses and all exceptions, preventing
AIModelProviderManager from populating modelToProviderMap and causing getProviderForModel() to fall
back to its '/'-prefix heuristic (e.g., routing "anthropic/..." to the Anthropic provider instead of
LiteLLM).
Agent Prompt
## Issue description
`LiteLLMProvider.listAvailableModels()` silently converts both HTTP failures and runtime errors into an empty model list. This means `AIModelProviderManager` cannot map discovered model IDs to the `litellm` provider; when a modelId contains `/`, the manager may route by prefix instead (treating the first segment as a providerId), which can select the wrong provider.

## Issue Context
- Manager routing prefers `modelToProviderMap`, then falls back to splitting `modelId` on `/`.
- LiteLLM model IDs commonly include provider prefixes like `anthropic/claude-...` (tests and docs).

## Fix Focus Areas
- src/core/llm/providers/implementations/LiteLLMProvider.ts[161-190]
- src/core/llm/providers/AIModelProviderManager.ts[240-258]

## Suggested fix
- Do not fully swallow discovery failures:
  - At minimum: `console.warn` (or debug-gated log) with status code / error when `/models` fails.
  - Preferably: throw an Error on non-OK and on caught exceptions so `AIModelProviderManager`’s existing `catch` paths can log the providerId and context.
- To reduce misrouting when discovery is temporarily unavailable, consider returning a minimal fallback catalog containing at least `defaultModelId` (if set) so the manager can still map the primary configured model to `litellm`.
- Update/extend tests to assert the new behavior (log/throw and/or fallback model inclusion).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/core/llm/providers/AIModelProviderManager.ts (1)

33-44: ⚠️ Potential issue | 🟡 Minor

Add LiteLLMProviderConfig to ProviderConfigEntry.config union

providerId: 'litellm' is wired to LiteLLMProvider, and LiteLLMProviderConfig is imported, but ProviderConfigEntry.config omits it—so LiteLLM entries don’t get typed config support.

Suggested fix
 export interface ProviderConfigEntry {
   providerId: string;
   enabled: boolean;
-  config: Partial<OpenAIProviderConfig | OpenRouterProviderConfig | OllamaProviderConfig | AnthropicProviderConfig | GroqProviderConfig | TogetherProviderConfig | MistralProviderConfig | XAIProviderConfig | GeminiProviderConfig | ClaudeCodeProviderConfig | GeminiCLIProviderConfig | Record<string, any>>;
+  config: Partial<OpenAIProviderConfig | OpenRouterProviderConfig | OllamaProviderConfig | AnthropicProviderConfig | GroqProviderConfig | TogetherProviderConfig | MistralProviderConfig | XAIProviderConfig | GeminiProviderConfig | ClaudeCodeProviderConfig | GeminiCLIProviderConfig | LiteLLMProviderConfig | Record<string, any>>;
   isDefault?: boolean;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/llm/providers/AIModelProviderManager.ts` around lines 33 - 44,
ProviderConfigEntry.config currently omits LiteLLMProviderConfig even though
LiteLLMProvider is wired to providerId 'litellm' and LiteLLMProviderConfig is
already imported; update the ProviderConfigEntry interface to include
LiteLLMProviderConfig in the union for the config property so entries with
providerId 'litellm' get proper typings, i.e., add LiteLLMProviderConfig
alongside the other provider config types referenced in
ProviderConfigEntry.config.
🧹 Nitpick comments (1)
src/core/llm/providers/__tests__/LiteLLMProvider.test.ts (1)

161-178: ⚡ Quick win

Assert the request contract for model discovery, not just the parsed response.

The test verifies parsed models, but it doesn’t verify that listAvailableModels() called the expected endpoint and auth. Add assertions that fetch was called with /v1/models and an Authorization header (derived from apiKey) so proxy-routing regressions are caught.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/llm/providers/__tests__/LiteLLMProvider.test.ts` around lines 161 -
178, The test currently checks parsed models but not the request sent; after
calling provider.initialize({ apiKey: 'sk-test' }) and await
provider.listAvailableModels(), add assertions on the mockFetch call (mockFetch
or global fetch stub) to ensure it was invoked with the '/v1/models' URL and
that the request options include an Authorization header derived from the apiKey
(e.g., 'Authorization: Bearer sk-test'); use the recorded call arguments from
mockFetch to assert the URL and headers so proxy-routing/auth regressions are
caught.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/core/llm/providers/__tests__/LiteLLMProvider.test.ts`:
- Around line 160-205: The tests stub the global fetch inside each it block
using vi.stubGlobal but call vi.unstubAllGlobals only inside the happy path,
which can leak fetch if an assertion fails; refactor the
describe('listAvailableModels') suite to move vi.stubGlobal usage into each test
as-is but add an afterEach block that calls vi.unstubAllGlobals() (or
vi.restoreAllMocks()/appropriate Vitest cleanup) to ensure fetch is always
restored; reference the test suite, the provider.initialize(...) and
provider.listAvailableModels() usages to locate where stubbing happens and add
the afterEach cleanup for all cases.

In `@src/core/llm/providers/implementations/LiteLLMProvider.ts`:
- Around line 161-168: listAvailableModels() performs a direct fetch to the
proxy /models without honoring the configured requestTimeout, so a stalled proxy
can hang discovery; update listAvailableModels (in LiteLLMProvider) to create an
AbortController, start a timer using this.requestTimeout that calls
controller.abort(), pass controller.signal to fetch (using this.proxyBaseURL and
this.proxyApiKey as before), and ensure the timeout timer is cleared in a
finally block so it is always cleaned up even on errors; mirror the
timeout-handling pattern used in initialize() or other methods that forward
requestTimeout to the delegate.

---

Outside diff comments:
In `@src/core/llm/providers/AIModelProviderManager.ts`:
- Around line 33-44: ProviderConfigEntry.config currently omits
LiteLLMProviderConfig even though LiteLLMProvider is wired to providerId
'litellm' and LiteLLMProviderConfig is already imported; update the
ProviderConfigEntry interface to include LiteLLMProviderConfig in the union for
the config property so entries with providerId 'litellm' get proper typings,
i.e., add LiteLLMProviderConfig alongside the other provider config types
referenced in ProviderConfigEntry.config.

---

Nitpick comments:
In `@src/core/llm/providers/__tests__/LiteLLMProvider.test.ts`:
- Around line 161-178: The test currently checks parsed models but not the
request sent; after calling provider.initialize({ apiKey: 'sk-test' }) and await
provider.listAvailableModels(), add assertions on the mockFetch call (mockFetch
or global fetch stub) to ensure it was invoked with the '/v1/models' URL and
that the request options include an Authorization header derived from the apiKey
(e.g., 'Authorization: Bearer sk-test'); use the recorded call arguments from
mockFetch to assert the URL and headers so proxy-routing/auth regressions are
caught.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 38ac08b5-c708-434f-8443-1eb4577dbf8d

📥 Commits

Reviewing files that changed from the base of the PR and between 606b158 and 149da1a.

📒 Files selected for processing (3)
  • src/core/llm/providers/AIModelProviderManager.ts
  • src/core/llm/providers/__tests__/LiteLLMProvider.test.ts
  • src/core/llm/providers/implementations/LiteLLMProvider.ts

Comment on lines +160 to +205
describe('listAvailableModels', () => {
it('fetches models from proxy /v1/models', async () => {
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
data: [
{ id: 'gpt-4o-mini' },
{ id: 'anthropic/claude-sonnet-4-6' },
],
}),
});
vi.stubGlobal('fetch', mockFetch);

await provider.initialize({ apiKey: 'sk-test' });
const models = await provider.listAvailableModels();

expect(models).toHaveLength(2);
expect(models[0].modelId).toBe('gpt-4o-mini');
expect(models[1].modelId).toBe('anthropic/claude-sonnet-4-6');

vi.unstubAllGlobals();
});

it('returns empty array on fetch error', async () => {
const mockFetch = vi.fn().mockResolvedValue({ ok: false });
vi.stubGlobal('fetch', mockFetch);

await provider.initialize({ apiKey: 'sk-test' });
const models = await provider.listAvailableModels();

expect(models).toEqual([]);

vi.unstubAllGlobals();
});

it('returns empty array on network failure', async () => {
const mockFetch = vi.fn().mockRejectedValue(new Error('ECONNREFUSED'));
vi.stubGlobal('fetch', mockFetch);

await provider.initialize({ apiKey: 'sk-test' });
const models = await provider.listAvailableModels();

expect(models).toEqual([]);

vi.unstubAllGlobals();
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Ensure global fetch cleanup runs even when assertions fail.

In Line 180, Line 192, and Line 204, vi.unstubAllGlobals() is only reached on the happy path. If an assertion throws earlier, fetch can leak into later tests and create order-dependent failures. Move cleanup to an afterEach for this suite.

Suggested fix
-import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
@@
 describe('LiteLLMProvider', () => {
   let provider: LiteLLMProvider;
 
   beforeEach(() => {
     vi.clearAllMocks();
     provider = new LiteLLMProvider();
   });
+  
+  afterEach(() => {
+    vi.unstubAllGlobals();
+  });
@@
-      vi.unstubAllGlobals();
@@
-      vi.unstubAllGlobals();
@@
-      vi.unstubAllGlobals();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/llm/providers/__tests__/LiteLLMProvider.test.ts` around lines 160 -
205, The tests stub the global fetch inside each it block using vi.stubGlobal
but call vi.unstubAllGlobals only inside the happy path, which can leak fetch if
an assertion fails; refactor the describe('listAvailableModels') suite to move
vi.stubGlobal usage into each test as-is but add an afterEach block that calls
vi.unstubAllGlobals() (or vi.restoreAllMocks()/appropriate Vitest cleanup) to
ensure fetch is always restored; reference the test suite, the
provider.initialize(...) and provider.listAvailableModels() usages to locate
where stubbing happens and add the afterEach cleanup for all cases.

Comment on lines +161 to +168
public async listAvailableModels(
filter?: { capability?: string },
): Promise<ModelInfo[]> {
try {
const url = this.proxyBaseURL.replace(/\/+$/, '') + '/models';
const response = await fetch(url, {
headers: { Authorization: `Bearer ${this.proxyApiKey}` },
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C2 "requestTimeout|listAvailableModels|fetch\\(" src/core/llm/providers/implementations/LiteLLMProvider.ts

Repository: framerslab/agentos

Length of output: 1138


Add requestTimeout/AbortController to LiteLLMProvider.listAvailableModels() proxy /models fetch (src/core/llm/providers/implementations/LiteLLMProvider.ts:161-168).

initialize() forwards requestTimeout to the delegate, but listAvailableModels() performs a direct fetch(.../models) with no signal/timeout handling, so a stalled proxy can hang model discovery. Use the configured timeout for this fetch (AbortController) and ensure any timer is cleared even on errors.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/llm/providers/implementations/LiteLLMProvider.ts` around lines 161 -
168, listAvailableModels() performs a direct fetch to the proxy /models without
honoring the configured requestTimeout, so a stalled proxy can hang discovery;
update listAvailableModels (in LiteLLMProvider) to create an AbortController,
start a timer using this.requestTimeout that calls controller.abort(), pass
controller.signal to fetch (using this.proxyBaseURL and this.proxyApiKey as
before), and ensure the timeout timer is cleared in a finally block so it is
always cleaned up even on errors; mirror the timeout-handling pattern used in
initialize() or other methods that forward requestTimeout to the delegate.

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