Skip to content

Conversation

sairampillai
Copy link

@sairampillai sairampillai commented Sep 22, 2025

Implements Issue #100

  • Updated dependencies
  • Updated llm_helper.py
  • Updated chat components
  • Updated function calls
  • Tested independent functions using Cursor
  • Tested end-to-end with multiple followup and chats for Ollama local mode and Openrouter models
  • Yet to test end-to-end on multiple LLM providers
  • Updated documentation

Summary by CodeRabbit

  • New Features

    • Streamed AI responses for smoother chat.
    • “New Chat” button to clear history quickly.
    • Clearer provider/model selection with user-facing validation and errors.
    • Improved PDF upload handling with consistent session storage.
    • Simple prompt templating; continued support for local/offline models.
  • Refactor

    • Unified AI backend and streaming implementation for more consistent behavior.
  • Documentation

    • Notes on supported providers and feature parity.
  • Chores

    • Reduced and consolidated dependencies.
  • Style

    • .gitignore updated to cover macOS .DS_Store files.

Copy link

coderabbitai bot commented Sep 22, 2025

Walkthrough

Project migrates from LangChain to LiteLLM: replaces LangChain dependencies and integrations with LiteLLM, adds lightweight in-repo chat utilities, updates app wiring for provider/model parsing, streaming, session-state chat history, PDF handling, and a reset utility; updates .gitignore and adds a migration summary doc.

Changes

Cohort / File(s) Summary
LiteLLM migration in LLM helper
helpers/llm_helper.py
Replaced LangChain logic with LiteLLM: guarded litellm import, added get_litellm_model_name, stream_litellm_completion, get_litellm_llm, provider/model validation, API key/azure handling; aliased get_langchain_llm = get_litellm_llm.
In-house chat utilities
helpers/chat_helper.py
New lightweight chat primitives: ChatMessage, HumanMessage, AIMessage, StreamlitChatMessageHistory (session_state-backed), and ChatPromptTemplate with from_template and format.
App wiring and UI updates
app.py
Removed LangChain usage; integrated chat_helper and llm_helper.get_litellm_llm, added provider/model regex parsing and validation, streaming/error handling updates, PDF_FILE_KEY, reset_chat_history, New Chat button, and consistent session-state handling for PDFs and messages.
Dependencies
requirements.txt
Removed LangChain-related packages; added/required litellm>=1.55.0; preserved google-generativeai and other existing pins.
Migration docs
LITELLM_MIGRATION_SUMMARY.md
Added migration summary documenting new APIs, supported providers, and compatibility notes.
Ignore rules
.gitignore
Replaced .idea.DS_Store with .DS_Store and .idea/**/.DS_Store.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor U as User
    participant S as Streamlit app (`app.py`)
    participant C as ChatHelper (`helpers/chat_helper.py`)
    participant H as LLM Helper (`helpers/llm_helper.py`)
    participant L as LiteLLM Provider

    U->>S: Submit message / upload PDF
    S->>C: Add `HumanMessage` to history
    S->>H: validate provider/model → get_litellm_llm(...)
    alt provider/model valid
        S->>H: stream_litellm_completion(messages, max_tokens, api_key...)
        H->>L: issue streaming request
        loop stream tokens
            L-->>H: token chunk (delta/content)
            H-->>S: yield chunk
            S->>C: append/aggregate `AIMessage` chunks
        end
    else invalid provider/model
        H-->>S: validation error
        S-->>U: show error and halt
    end
    S-->>U: render final AI response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • Migrate to LiteLLM #100 — Directly related: same LiteLLM migration and replacement of LangChain chat/history abstractions described in this PR.

Poem

A rabbit hops in, code in paw,
Swapped long chains for LiteLLM's law.
I stash each chat in cozy state,
Stream tokens nibbling at the gate.
New Chat pressed—my whiskers cheer, hooray! 🥕

Pre-merge checks and finishing touches

✅ 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 succinctly captures the core change of replacing LangChain with LiteLLM for LLM interactions, clearly highlighting both integration and deprecation aspects without unnecessary detail or noise.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


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.

@sairampillai sairampillai marked this pull request as ready for review September 22, 2025 13:28
Copy link

@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 (6)
helpers/llm_helper.py (2)

39-45: Retry config bug: Retry has no backoff_jitter.

This raises a TypeError at import. Remove it and enable respect_retry_after_header.

Apply:

 retries = Retry(
-    total=5,
-    backoff_factor=0.25,
-    backoff_jitter=0.3,
-    status_forcelist=[502, 503, 504],
-    allowed_methods={'POST'},
+    total=5,
+    backoff_factor=0.25,
+    status_forcelist=[502, 503, 504],
+    allowed_methods=frozenset({'POST'}),
+    respect_retry_after_header=True,
 )

100-116: Azure validation too lenient.

str(valid_url) is always truthy; invalid URLs pass. Validate scheme/host explicitly.

-    if provider == GlobalConfig.PROVIDER_AZURE_OPENAI:
-        valid_url = urllib3.util.parse_url(azure_endpoint_url)
-        all_status = all(
-            [azure_api_version, azure_deployment_name, str(valid_url)]
-        )
-        return all_status
+    if provider == GlobalConfig.PROVIDER_AZURE_OPENAI:
+        parsed = urllib3.util.parse_url(azure_endpoint_url or "")
+        if not (parsed and parsed.scheme in ("https", "http") and parsed.host):
+            return False
+        return all([azure_api_version, azure_deployment_name])
app.py (4)

101-121: Azure arg names/order are confusing here.

Names suggest URL then deployment, but later you pass them swapped. Standardize to azure_endpoint_url, azure_deployment_name and use keyword args.

-def are_all_inputs_valid(
+def are_all_inputs_valid(
         user_prompt: str,
         provider: str,
         selected_model: str,
         user_key: str,
-        azure_deployment_url: str = '',
-        azure_endpoint_name: str = '',
+        azure_endpoint_url: str = '',
+        azure_deployment_name: str = '',
         azure_api_version: str = '',
 ) -> bool:
@@
-    if not llm_helper.is_valid_llm_provider_model(
-            provider, selected_model, user_key,
-            azure_endpoint_name, azure_deployment_url, azure_api_version
-    ):
+    if not llm_helper.is_valid_llm_provider_model(
+        provider=provider,
+        model=selected_model,
+        api_key=user_key,
+        azure_endpoint_url=azure_endpoint_url,
+        azure_deployment_name=azure_deployment_name,
+        azure_api_version=azure_api_version,
+    ):

136-149: Fix swapped Azure args at call site.

They’re passed as deployment, endpoint, version; switch to keywords to avoid mistakes.

-        if not are_all_inputs_valid(
-                prompt_text, provider, llm_name, user_key,
-                az_deployment, az_endpoint, api_ver
-        ):
+        if not are_all_inputs_valid(
+            user_prompt=prompt_text,
+            provider=provider,
+            selected_model=llm_name,
+            user_key=user_key,
+            azure_endpoint_url=az_endpoint,
+            azure_deployment_name=az_deployment,
+            azure_api_version=api_ver,
+        ):

386-389: Avoid logging full user prompts (PII risk).

Log length/metadata only.

-        logger.info(
-            'User input: %s | #characters: %d | LLM: %s',
-            prompt_text, len(prompt_text), llm_name
-        )
+        logger.info('Prompt length: %d | LLM: %s', len(prompt_text), llm_name)

416-425: Fix Azure argument order in legacy LLM helper call sites

  • app.py:136-138 – llm_helper.is_valid_llm_provider_model(...) still passes azure_endpoint_name, azure_deployment_url; swap to azure_endpoint_url, azure_deployment_name.
  • app.py:380-382 – are_all_inputs_valid(...) call uses az_deployment, az_endpoint; reorder or use named args to match the updated signature.
🧹 Nitpick comments (9)
requirements.txt (2)

11-11: Unpinned deps risk non‑reproducible installs.

Pin or constrain:

  • google-generativeai
  • numpy
  • huggingface-hub

Apply this diff to pin conservatively (adjust as needed):

-google-generativeai  # ~=0.8.3
+google-generativeai~=0.8.3
-numpy
+numpy~=1.26
-huggingface-hub  #~=0.24.5
+huggingface-hub~=0.24.5

Also applies to: 24-24, 33-33


32-33: httpx/anyio alignment with LiteLLM.

LiteLLM uses httpx; ensure httpx/anyio pins remain compatible across provider backends. Consider a constraints.txt to centralize versions.

LITELLM_MIGRATION_SUMMARY.md (2)

96-103: Azure mapping likely inaccurate.

For LiteLLM, Azure typically expects model as azure/{deployment_name}, not azure/{model}. Update docs to avoid misconfiguration.


123-126: “No breaking changes” claim is optimistic.

You replaced LangChain chat components with custom classes. While internal, call‑sites may need small adjustments. Reword to “minimal breaking changes; app updated accordingly.”

helpers/llm_helper.py (4)

46-49: Unused requests Session/adapter.

You build http_session but never use it (LiteLLM uses httpx). Remove to reduce confusion.

-adapter = HTTPAdapter(max_retries=retries)
-http_session = requests.Session()
-http_session.mount('https://', adapter)
-http_session.mount('http://', adapter)
+# NOTE: LiteLLM uses httpx; requests Session not required and removed.

209-221: Stream parsing: add dict fallback.

Some providers return dicts. Yield text robustly.

-        for chunk in response:
-            if hasattr(chunk, 'choices') and chunk.choices:
+        for chunk in response:
+            if isinstance(chunk, str):
+                yield chunk
+                continue
+            if isinstance(chunk, dict) and "choices" in chunk and chunk["choices"]:
+                choice = chunk["choices"][0]
+                text = (choice.get("delta", {}) or {}).get("content") or (choice.get("message", {}) or {}).get("content")
+                if text:
+                    yield text
+                continue
+            if hasattr(chunk, 'choices') and chunk.choices:
                 choice = chunk.choices[0]
                 if hasattr(choice, 'delta') and hasattr(choice.delta, 'content'):
                     if choice.delta.content:
                         yield choice.delta.content
                 elif hasattr(choice, 'message') and hasattr(choice.message, 'content'):
                     if choice.message.content:
                         yield choice.message.content

222-224: Log stacktrace on failure.

Use logger.exception to preserve traceback.

-    except Exception as e:
-        logger.error(f"Error in LiteLLM completion: {e}")
-        raise
+    except Exception:
+        logger.exception("Error in LiteLLM completion")
+        raise

249-252: Surface missing dependency early.

Raising instead of returning None prevents silent failures downstream; app already catches exceptions. Optional, but clearer.

-    if litellm is None:
-        logger.error("LiteLLM is not installed")
-        return None
+    if litellm is None:
+        raise ImportError("LiteLLM is not installed. pip install litellm")
app.py (1)

592-595: Preserve traceback on generation errors.

Use logger.exception for easier debugging.

-        st.error(APP_TEXT['content_generation_error'])
-        logger.error('Caught a generic exception: %s', str(ex))
+        st.error(APP_TEXT['content_generation_error'])
+        logger.exception('Caught a generic exception')
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 18733f2 and 038ba10.

📒 Files selected for processing (4)
  • LITELLM_MIGRATION_SUMMARY.md (1 hunks)
  • app.py (2 hunks)
  • helpers/llm_helper.py (3 hunks)
  • requirements.txt (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
helpers/llm_helper.py (1)
global_config.py (1)
  • GlobalConfig (16-198)
app.py (1)
helpers/llm_helper.py (1)
  • get_litellm_llm (227-286)
🪛 Ruff (0.13.1)
helpers/llm_helper.py

183-183: Avoid specifying long messages outside the exception class

(TRY003)


223-223: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

🔇 Additional comments (3)
helpers/llm_helper.py (1)

290-291: BC alias retained.

Keeping get_langchain_llm alias is a good migration aid.

app.py (1)

19-60: Custom chat classes look good.

Simple, compatible with Streamlit chat types. No issues spotted.

requirements.txt (1)

10-10: Pin LiteLLM minor version (avoid surprise breaks). Update requirements.txt (line 10) from litellm>=1.55.0 to a reproducible constraint such as litellm~=1.55.0 (locks to 1.55.x) or litellm>=1.55.0,<2.0 (allows minor bumps but blocks v2). Latest stable is v1.76.1 (Aug 30, 2025); no major-version bump since 1.55.x but minor releases can add config/API changes—scan the changelog before upgrading.

Copy link

@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: 1

Caution

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

⚠️ Outside diff range comments (2)
helpers/llm_helper.py (1)

123-129: Fix Azure endpoint validation (str(None) is truthy).

parse_url() may return None, and str(valid_url) is always truthy, so invalid URLs pass. Validate the parsed object and required fields.

Apply:

-    if provider == GlobalConfig.PROVIDER_AZURE_OPENAI:
-        valid_url = urllib3.util.parse_url(azure_endpoint_url)
-        all_status = all(
-            [azure_api_version, azure_deployment_name, str(valid_url)]
-        )
-        return all_status
+    if provider == GlobalConfig.PROVIDER_AZURE_OPENAI:
+        parsed = urllib3.util.parse_url(azure_endpoint_url or "")
+        if not (parsed and parsed.scheme in {"https"} and parsed.host):
+            return False
+        return bool(azure_api_version and azure_deployment_name)
app.py (1)

447-456: Stop mutating os.environ in helpers/llm_helper — pass keys per-request.
helpers/llm_helper.py still assigns os.environ["OPENROUTER_API_KEY"] and AZURE_* (helpers/llm_helper.py:220–227); remove those assignments and refactor to configure provider clients with the supplied api_key/azure_endpoint/azure_api_version instead of mutating process-wide env.

🧹 Nitpick comments (7)
helpers/llm_helper.py (2)

30-31: Relax API key validation to avoid rejecting valid keys (dot and longer lengths).

Keys like hf_.../sk-or-v1-.../some provider tokens can include . and exceed 94 chars. Current regex is too strict.

Apply:

-# 94 characters long, only containing alphanumeric characters, hyphens, and underscores
-API_KEY_REGEX = re.compile(r'^[a-zA-Z0-9_-]{6,94}$')
+# 6–256 printable token chars commonly used by providers
+API_KEY_REGEX = re.compile(r'^[A-Za-z0-9._-]{6,256}$')

And keep only the regex check (no extra length checks needed).

Also applies to: 120-121


39-49: Remove unused requests Retry/session (dead code).

http_session and adapter are never used; LiteLLM uses httpx internally.

Apply:

-retries = Retry(
-    total=5,
-    backoff_factor=0.25,
-    backoff_jitter=0.3,
-    status_forcelist=[502, 503, 504],
-    allowed_methods={'POST'},
-)
-adapter = HTTPAdapter(max_retries=retries)
-http_session = requests.Session()
-http_session.mount('https://', adapter)
-http_session.mount('http://', adapter)
+# NOTE: LiteLLM uses httpx internally; requests retry adapter not needed here.
app.py (5)

101-109: Rename Azure args for clarity; keep ordering consistent.

The parameter names in are_all_inputs_valid are swapped (URL vs name), which is confusing even though the call flips them back. Rename for correctness.

Apply:

-def are_all_inputs_valid(
+def are_all_inputs_valid(
         user_prompt: str,
         provider: str,
         selected_model: str,
         user_key: str,
-        azure_deployment_url: str = '',
-        azure_endpoint_name: str = '',
+        azure_endpoint_url: str = '',
+        azure_deployment_name: str = '',
         azure_api_version: str = '',
 ) -> bool:
@@
-    if not llm_helper.is_valid_llm_provider_model(
-            provider, selected_model, user_key,
-            azure_endpoint_name, azure_deployment_url, azure_api_version
-    ):
+    if not llm_helper.is_valid_llm_provider_model(
+            provider, selected_model, user_key,
+            azure_endpoint_url, azure_deployment_name, azure_api_version
+    ):

And in the caller:

-        if not are_all_inputs_valid(
-                prompt_text, provider, llm_name, user_key,
-                az_deployment, az_endpoint, api_ver
-        ):
+        if not are_all_inputs_valid(
+                prompt_text, provider, llm_name, user_key,
+                az_endpoint, az_deployment, api_ver
+        ):

Also applies to: 411-414


345-347: Don’t render user messages as JSON code blocks.

This formats plain text oddly. Render user messages with write() and AI messages that are JSON with code().

Apply:

-    for msg in history.messages:
-        st.chat_message(msg.type).code(msg.content, language='json')
+    for msg in history.messages:
+        if msg.type == "ai":
+            st.chat_message("ai").code(msg.content, language="json")
+        else:
+            st.chat_message("user").write(msg.content)

241-245: Avoid double error display on invalid provider.

handle_error() already calls st.error(). The second st.error() duplicates output.

Apply:

-            handle_error(f"Invalid provider selected: {selected_provider}", True)
-            st.error(f"Invalid provider selected: {selected_provider}")
+            handle_error(f"Invalid provider selected: {selected_provider}", True)
             st.stop()

141-146: Sync UI hint with relaxed API key rule (length and charset).

Update the message to match the new validation (6–256 chars; allow dot).

Apply:

-            ' is provided if the selected LLM requires it. An API key should be 6-94 characters'
-            ' long, only containing alphanumeric characters, hyphens, and underscores.\n\n'
+            ' is provided if the selected LLM requires it. An API key should be 6–256 characters'
+            ' long, using typical token characters (letters, digits, dot, hyphen, underscore).\n\n'

512-521: Make “payment required” guidance provider‑agnostic.

This path now covers multiple providers (not just HF). Suggest neutral wording with next steps.

Apply:

-                    'The available inference quota has exhausted.'
-                    ' Please use your own Hugging Face access token. Paste your token in'
-                    ' the input field on the sidebar to the left.'
-                    '\n\nDon\'t have a token? Get your free'
-                    ' [HF access token](https://huggingface.co/settings/tokens) now'
-                    ' and start creating your slide deck! For gated models, you may need to'
-                    ' visit the model\'s page and accept the terms or service.'
-                    '\n\nAlternatively, choose a different LLM and provider from the list.',
+                    'The model endpoint reported “payment required” or insufficient credits.'
+                    ' Please supply a valid API key with sufficient quota on the sidebar,'
+                    ' or choose a different LLM/provider. For gated models, accept the'
+                    ' provider’s terms on the model page before retrying.',
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 038ba10 and 15a4e19.

📒 Files selected for processing (2)
  • app.py (4 hunks)
  • helpers/llm_helper.py (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
app.py (2)
global_config.py (1)
  • GlobalConfig (16-198)
helpers/llm_helper.py (1)
  • get_litellm_llm (253-312)
helpers/llm_helper.py (1)
global_config.py (1)
  • GlobalConfig (16-198)
🪛 Ruff (0.13.1)
app.py

400-401: Possible SQL injection vector through string-based query construction

(S608)

helpers/llm_helper.py

195-195: Avoid specifying long messages outside the exception class

(TRY003)


202-202: Avoid specifying long messages outside the exception class

(TRY003)


249-249: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

🔇 Additional comments (3)
helpers/llm_helper.py (3)

133-150: Harden Azure model-name handling to prevent azure/azure double-prefix.

Even though you special-case Azure later, this helper remains a foot‑gun if used elsewhere.

Apply:

 def get_litellm_model_name(provider: str, model: str) -> str:
@@
-    prefix = provider_prefix_map.get(provider)
-    if prefix:
-        return f"{prefix}/{model}"
+    prefix = provider_prefix_map.get(provider)
+    if prefix:
+        if provider == GlobalConfig.PROVIDER_AZURE_OPENAI and model.startswith("azure/"):
+            return model
+        return f"{prefix}/{model}"
     return model

253-313: Approve — LiteLLM wrapper is a pragmatic bridge.

Lightweight wrapper with stream() keeps app changes minimal and is fine given the migration. Repo-wide search found no runtime references to langchain or get_langchain_llm; only mentions are in LITELLM_MIGRATION_SUMMARY.md (lines 39–41, 89).


197-206: Avoid mutating process env; pass provider-specific params per call and set timeout

File: helpers/llm_helper.py — lines 197–250

os.environ changes are process-global (Streamlit = multi‑user risk). LiteLLM supports per-call params for Azure/OpenRouter (api_key, api_base/azure_endpoint, api_version, base_url) and stream=True — add REQUEST_TIMEOUT to request params and use logger.exception for full stack traces.

Apply:

@@
-    # Prepare the request parameters
-    request_params = {
+    # Prepare the request parameters
+    request_params = {
         "model": litellm_model,
         "messages": messages,
         "max_tokens": max_tokens,
         "temperature": GlobalConfig.LLM_MODEL_TEMPERATURE,
         "stream": True,
+        "timeout": REQUEST_TIMEOUT,
     }
@@
-    # Set API key and any provider-specific params
-    if provider != GlobalConfig.PROVIDER_OLLAMA:
-        # For OpenRouter, set environment variable as per documentation
-        if provider == GlobalConfig.PROVIDER_OPENROUTER:
-            os.environ["OPENROUTER_API_KEY"] = api_key
-            # Optional: Set base URL if different from default
-            # os.environ["OPENROUTER_API_BASE"] = "https://openrouter.ai/api/v1"
-        elif provider == GlobalConfig.PROVIDER_AZURE_OPENAI:
-            # For Azure OpenAI, set environment variables as per documentation
-            os.environ["AZURE_API_KEY"] = api_key
-            os.environ["AZURE_API_BASE"] = azure_endpoint_url
-            os.environ["AZURE_API_VERSION"] = azure_api_version
-        else:
-            # For other providers, pass API key as parameter
-            api_key_to_use = get_litellm_api_key(provider, api_key)
-            request_params["api_key"] = api_key_to_use
+    # Set API key and any provider-specific params (no global env mutation)
+    if provider != GlobalConfig.PROVIDER_OLLAMA:
+        if provider == GlobalConfig.PROVIDER_OPENROUTER:
+            request_params.update({
+                "base_url": OPENROUTER_BASE_URL,
+                "api_key": api_key,
+                # Optional but recommended by OpenRouter:
+                "headers": {
+                    "HTTP-Referer": "https://github.com/barun-saha/slide-deck-ai",
+                    "X-Title": "SlideDeck AI",
+                },
+            })
+        elif provider == GlobalConfig.PROVIDER_AZURE_OPENAI:
+            request_params.update({
+                "azure_endpoint": azure_endpoint_url,
+                "api_version": azure_api_version,
+                "api_key": api_key,
+                # "azure_deployment": azure_deployment_name,  # optional since model="azure/<deployment>"
+            })
+        else:
+            request_params["api_key"] = get_litellm_api_key(provider, api_key)
@@
-    except Exception as e:
-        logger.error(f"Error in LiteLLM completion: {e}")
+    except Exception:
+        logger.exception("Error in LiteLLM completion")
         raise

Copy link

@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)
helpers/llm_helper.py (1)

124-129: Azure endpoint validation always truthy; enforce scheme and host.

str(parse_url(...)) is non-empty even for empty URL (“None”). Validate scheme/host explicitly.

-    valid_url = urllib3.util.parse_url(azure_endpoint_url)
-    all_status = all(
-        [azure_api_version, azure_deployment_name, str(valid_url)]
-    )
-    return all_status
+    parsed = urllib3.util.parse_url(azure_endpoint_url or "")
+    if not (parsed.scheme in ("http", "https") and parsed.host):
+        return False
+    return all([azure_api_version, azure_deployment_name])
🧹 Nitpick comments (5)
helpers/llm_helper.py (5)

39-50: Remove unused requests Retry/session setup.

http_session and adapter are never used; keep the file lean to avoid confusion.

-retries = Retry(
-    total=5,
-    backoff_factor=0.25,
-    backoff_jitter=0.3,
-    status_forcelist=[502, 503, 504],
-    allowed_methods={'POST'},
-)
-adapter = HTTPAdapter(max_retries=retries)
-http_session = requests.Session()
-http_session.mount('https://', adapter)
-http_session.mount('http://', adapter)
+# NOTE: Requests session removed as LiteLLM uses httpx under the hood.

112-121: API key check contradicts docstring and may reject valid keys.

Doc says “at least five characters,” but regex enforces 6–94 and specific charset. Prefer a simple length check.

-    if api_key and API_KEY_REGEX.match(api_key) is None:
-        return False
+    if len(api_key) < 5:
+        return False

235-247: Harden stream parsing for dict payloads.

LiteLLM stream chunks can be dicts. Add a dict-path fallback to avoid dropping tokens.

         for chunk in response:
-            if hasattr(chunk, 'choices') and chunk.choices:
+            if hasattr(chunk, 'choices') and chunk.choices:
                 choice = chunk.choices[0]
                 if hasattr(choice, 'delta') and hasattr(choice.delta, 'content'):
                     if choice.delta.content:
                         yield choice.delta.content
                 elif hasattr(choice, 'message') and hasattr(choice.message, 'content'):
                     if choice.message.content:
                         yield choice.message.content
+            elif isinstance(chunk, dict):
+                choices = chunk.get("choices") or []
+                if choices:
+                    delta = (choices[0].get("delta") or {})
+                    content = delta.get("content") or (choices[0].get("message") or {}).get("content")
+                    if content:
+                        yield content

247-249: Log exceptions with traceback.

Use logger.exception to retain stack traces.

-    except Exception as e:
-        logger.error(f"Error in LiteLLM completion: {e}")
+    except Exception:
+        logger.exception("Error in LiteLLM completion")
         raise

318-326: Demo input uses [hf]... which currently fails validation.

Either add hf to VALID_PROVIDERS or change the example to a supported provider until then.

Example alternative:

-        '[hf]mistralai/Mistral-7B-Instruct-v0.2',
+        '[or]mistralai/mistral-7b-instruct',
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 15a4e19 and 70de74a.

📒 Files selected for processing (1)
  • helpers/llm_helper.py (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
helpers/llm_helper.py (1)
global_config.py (1)
  • GlobalConfig (16-198)
🪛 Ruff (0.13.1)
helpers/llm_helper.py

195-195: Avoid specifying long messages outside the exception class

(TRY003)


202-202: Avoid specifying long messages outside the exception class

(TRY003)


248-248: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

🔇 Additional comments (4)
helpers/llm_helper.py (4)

133-150: Defensive fix: avoid double “azure/” prefix if used elsewhere.

Even though Azure is special-cased in stream_litellm_completion, harden this utility to prevent accidental azure/azure/... names.

     prefix = provider_prefix_map.get(provider)
     if prefix:
-        return f"{prefix}/{model}"
+        if provider == GlobalConfig.PROVIDER_AZURE_OPENAI and model.startswith("azure/"):
+            return model
+        return f"{prefix}/{model}"
     return model

170-214: Add request timeout.

REQUEST_TIMEOUT is defined but unused. Pass it to LiteLLM for resilience.

     request_params = {
         "model": litellm_model,
         "messages": messages,
         "max_tokens": max_tokens,
         "temperature": GlobalConfig.LLM_MODEL_TEMPERATURE,
         "stream": True,
+        "timeout": REQUEST_TIMEOUT,
     }

278-301: Wrapper looks good; ensure message shape matches callers.

.stream() returns an iterator of strings; aligns with upstream usage. No action.

Please confirm downstream expects a simple Iterator[str] and not LangChain AIMessageChunk.


315-315: Backward-compat alias is helpful.

Alias maintains existing imports. Good.

@barun-saha
Copy link
Owner

Thanks for the PR.

I had a quick glance and will look at again. In the meantime, can you pls check that the following minimal style guidelines are adhered to:

  1. Use single quotes for strings. No particular preference in case of nested quotes.
  2. Lines are max 100 chars long. Split longer lines at brackets first and then other elements.
  3. Use lazy formatting (%s) with logger instead for f-strings.
  4. End modules with a single blank line.
  5. Two line breaks after functions; one after methods.

Copy link
Owner

@barun-saha barun-saha left a comment

Choose a reason for hiding this comment

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

Thanks. I have left a few comments.

Also, PR #131 introduced some changes to app.py that have been merged recently. Could you pls check if you can pull those changes to your local copy and update the PR?

Copy link

@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: 1

🧹 Nitpick comments (9)
helpers/chat_helper.py (1)

38-41: Add return type for messages property (minor typing hygiene).

Helps IDEs and readers. Example change below.

Apply this diff:

-    def messages(self):
+    def messages(self) -> list['ChatMessage']:
         return st.session_state[self.key]

And add this import at the top of the file:

from typing import List  # if you prefer List[ChatMessage] style
helpers/llm_helper.py (3)

233-235: Use logging.exception to preserve stack trace.

Improves debuggability when streaming fails.

Apply this diff:

-    except Exception as e:
-        logger.error('Error in LiteLLM completion: %s', e)
-        raise
+    except Exception:
+        logger.exception('Error in LiteLLM completion')
+        raise

129-135: Stricter Azure endpoint validation (ensure scheme and host).

Current check casts the URL object to str, which can be truthy for invalid inputs.

Apply this diff:

-    if provider == GlobalConfig.PROVIDER_AZURE_OPENAI:
-        valid_url = urllib3.util.parse_url(azure_endpoint_url)
-        all_status = all(
-            [azure_api_version, azure_deployment_name, str(valid_url)]
-        )
-        return all_status
+    if provider == GlobalConfig.PROVIDER_AZURE_OPENAI:
+        parsed = urllib3.util.parse_url(azure_endpoint_url)
+        return all([
+            azure_api_version,
+            azure_deployment_name,
+            getattr(parsed, 'scheme', None) in ('http', 'https'),
+            bool(getattr(parsed, 'host', None)),
+        ])

15-16: Avoid mutating sys.path; use package-relative imports.

sys.path manipulation is fragile. Prefer proper package layout and absolute/relative imports.

app.py (5)

181-188: Use lazy logger formatting instead of f-strings.

Aligns with the requested logging style.

Apply this diff:

-            logger.warning(f"Could not parse provider from selectbox option: {selected_option}")
+            logger.warning('Could not parse provider from selectbox option: %s', selected_option)

191-197: Use lazy logger formatting instead of f-strings.

Aligns with the requested logging style.

Apply this diff:

-            logger.warning(f"Provider regex did not match for: {llm_provider_to_use}, using: {selected_provider}")
+            logger.warning('Provider regex did not match for: %s, using: %s', llm_provider_to_use, selected_provider)

60-79: Fix confusing Azure param names (endpoint vs. deployment).

Names are swapped; clarify to reduce future mistakes. Update signature and docstring accordingly.

Apply this diff:

-def are_all_inputs_valid(
+def are_all_inputs_valid(
         user_prompt: str,
         provider: str,
         selected_model: str,
         user_key: str,
-        azure_deployment_url: str = '',
-        azure_endpoint_name: str = '',
+        azure_endpoint_url: str = '',
+        azure_deployment_name: str = '',
         azure_api_version: str = '',
 ) -> bool:
@@
-    :param azure_deployment_url: Azure OpenAI deployment URL.
-    :param azure_endpoint_name: Azure OpenAI model endpoint.
+    :param azure_endpoint_url: Azure OpenAI endpoint URL.
+    :param azure_deployment_name: Azure OpenAI deployment name.
     :param azure_api_version: Azure OpenAI API version.

95-101: Update internal validation call to use corrected Azure param order.

Apply this diff:

-    if not llm_helper.is_valid_llm_provider_model(
-            provider, selected_model, user_key,
-            azure_endpoint_name, azure_deployment_url, azure_api_version
-    ):
+    if not llm_helper.is_valid_llm_provider_model(
+            provider, selected_model, user_key,
+            azure_endpoint_url, azure_deployment_name, azure_api_version
+    ):

370-373: Swap args at callsite to match corrected Azure param names.

Current order passes name before endpoint; reverse them.

Apply this diff:

-        if not are_all_inputs_valid(
-                prompt_text, provider, llm_name, user_key,
-                az_deployment, az_endpoint, api_ver
-        ):
+        if not are_all_inputs_valid(
+                prompt_text, provider, llm_name, user_key,
+                az_endpoint, az_deployment, api_ver
+        ):
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 70de74a and 54db4e6.

📒 Files selected for processing (3)
  • app.py (6 hunks)
  • helpers/chat_helper.py (1 hunks)
  • helpers/llm_helper.py (3 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-23T04:32:09.834Z
Learnt from: barun-saha
PR: barun-saha/slide-deck-ai#139
File: helpers/llm_helper.py:70-84
Timestamp: 2025-09-23T04:32:09.834Z
Learning: Hugging Face model support has been discontinued in the slide-deck-ai project, so PROVIDER_HUGGING_FACE should remain commented out in GlobalConfig.VALID_PROVIDERS and validation should continue rejecting 'hf' providers.

Applied to files:

  • helpers/llm_helper.py
  • app.py
🧬 Code graph analysis (2)
helpers/llm_helper.py (1)
global_config.py (1)
  • GlobalConfig (16-198)
app.py (3)
global_config.py (1)
  • GlobalConfig (16-198)
helpers/chat_helper.py (4)
  • StreamlitChatMessageHistory (30-46)
  • ChatPromptTemplate (49-60)
  • from_template (56-57)
  • HumanMessage (16-20)
helpers/llm_helper.py (1)
  • get_litellm_llm (238-300)
🪛 Ruff (0.13.1)
helpers/llm_helper.py

183-183: Avoid specifying long messages outside the exception class

(TRY003)


190-190: Avoid specifying long messages outside the exception class

(TRY003)


234-234: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

app.py

359-360: Possible SQL injection vector through string-based query construction

(S608)

🔇 Additional comments (3)
helpers/chat_helper.py (1)

23-27: Confirm Streamlit role name 'ai' is accepted; consider 'assistant' if not.

Streamlit typically recognizes 'user' and 'assistant'. If 'ai' is unsupported, switch AIMessage to use role='assistant' while keeping compatibility with your history.

app.py (1)

295-306: LGTM on the chat-helper integration.

Switch to StreamlitChatMessageHistory and template formatting is clean and minimal.

Please confirm st.chat_message(msg.type) works with 'ai' role in your deployed Streamlit version.

helpers/llm_helper.py (1)

195-217: Add timeout and OpenRouter base_url to LiteLLM request (prevents hangs, fixes OR routing).

Currently no timeout is set and OpenRouter base URL isn’t provided.

Apply this diff:

     # Prepare the request parameters
     request_params = {
         'model': litellm_model,
         'messages': messages,
         'max_tokens': max_tokens,
         'temperature': GlobalConfig.LLM_MODEL_TEMPERATURE,
         'stream': True,
+        'timeout': REQUEST_TIMEOUT,
     }
@@
-    if provider != GlobalConfig.PROVIDER_OLLAMA:
+    if provider != GlobalConfig.PROVIDER_OLLAMA:
         # For OpenRouter, pass API key as parameter
         if provider == GlobalConfig.PROVIDER_OPENROUTER:
-            request_params['api_key'] = api_key
+            request_params['api_key'] = api_key
+            request_params['base_url'] = OPENROUTER_BASE_URL
         elif provider == GlobalConfig.PROVIDER_AZURE_OPENAI:
             # For Azure OpenAI, pass credentials as parameters
             request_params['api_key'] = api_key
             request_params['azure_api_base'] = azure_endpoint_url
             request_params['azure_api_version'] = azure_api_version
         else:
             # For other providers, pass API key as parameter
             request_params['api_key'] = api_key

@sairampillai
Copy link
Author

@barun-saha Rebased the latest changes and fixed review comments. Also tested along with the New Chat feature end-to-end.

Copy link

@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: 3

🧹 Nitpick comments (6)
app.py (2)

134-151: Style: add a second blank line after top‑level function.

The project’s style guide asks for two blank lines after top‑level functions. Add one more after reset_chat_history().

 def reset_chat_history():
@@
             pptx_path.unlink()
 
-
+ 

365-373: Use the PDF_FILE_KEY constant instead of the magic string 'pdf_file'.

This keeps keys consistent and avoids typos.

-                        f' {st.session_state["pdf_file"].name}'
+                        f' {st.session_state[PDF_FILE_KEY].name}'
@@
-                        f' {st.session_state["pdf_file"].name}'
+                        f' {st.session_state[PDF_FILE_KEY].name}'
helpers/llm_helper.py (4)

17-23: Remove unused completion import and variable.

completion isn’t used (you call litellm.completion(...)), and keeping it complicates fallback logic.

-try:
-    import litellm
-    from litellm import completion
-except ImportError:
-    litellm = None
-    completion = None
+try:
+    import litellm
+except ImportError:
+    litellm = None

103-121: Azure validation: strengthen endpoint checks.

str(valid_url) will be truthy even for malformed URLs. Verify scheme and host, and keep other checks.

-    if provider == GlobalConfig.PROVIDER_AZURE_OPENAI:
-        valid_url = urllib3.util.parse_url(azure_endpoint_url)
-        all_status = all(
-            [azure_api_version, azure_deployment_name, str(valid_url)]
-        )
-        return all_status
+    if provider == GlobalConfig.PROVIDER_AZURE_OPENAI:
+        valid_url = urllib3.util.parse_url(azure_endpoint_url)
+        if not (getattr(valid_url, 'scheme', None) and getattr(valid_url, 'host', None)):
+            return False
+        return all([azure_api_version, azure_deployment_name])

203-221: Use logger.exception to capture traceback.

Ensures stack traces are preserved on streaming failures.

-    except Exception as e:
-        logger.error('Error in LiteLLM completion: %s', e)
-        raise
+    except Exception:
+        logger.exception('Error in LiteLLM completion')
+        raise

245-248: Consistent quotes per style guide.

Project prefers single quotes. Minor, but change for consistency.

-    if litellm is None:
-        logger.error("LiteLLM is not installed")
+    if litellm is None:
+        logger.error('LiteLLM is not installed')
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 54db4e6 and 4f28ec0.

📒 Files selected for processing (4)
  • .github/workflows/codeql.yml (1 hunks)
  • .gitignore (1 hunks)
  • app.py (8 hunks)
  • helpers/llm_helper.py (3 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-23T04:32:09.834Z
Learnt from: barun-saha
PR: barun-saha/slide-deck-ai#139
File: helpers/llm_helper.py:70-84
Timestamp: 2025-09-23T04:32:09.834Z
Learning: Hugging Face model support has been discontinued in the slide-deck-ai project, so PROVIDER_HUGGING_FACE should remain commented out in GlobalConfig.VALID_PROVIDERS and validation should continue rejecting 'hf' providers.

Applied to files:

  • app.py
  • helpers/llm_helper.py
🧬 Code graph analysis (2)
app.py (4)
global_config.py (1)
  • GlobalConfig (16-198)
helpers/chat_helper.py (4)
  • StreamlitChatMessageHistory (30-46)
  • ChatPromptTemplate (49-60)
  • from_template (56-57)
  • HumanMessage (16-20)
helpers/file_manager.py (1)
  • validate_page_range (48-73)
helpers/llm_helper.py (2)
  • get_provider_model (37-78)
  • get_litellm_llm (223-285)
helpers/llm_helper.py (1)
global_config.py (1)
  • GlobalConfig (16-198)
🪛 Ruff (0.13.1)
app.py

388-389: Possible SQL injection vector through string-based query construction

(S608)

helpers/llm_helper.py

168-168: Avoid specifying long messages outside the exception class

(TRY003)


175-175: Avoid specifying long messages outside the exception class

(TRY003)


219-219: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

🔇 Additional comments (3)
.github/workflows/codeql.yml (1)

12-99: LGTM — solid default CodeQL setup.

Matrix, permissions, and steps look correct for Python. No blockers.

helpers/llm_helper.py (1)

55-72: Provider/model validation is good — keeps deprecated HF blocked.

Validation against GlobalConfig.VALID_PROVIDERS and empty-model checks are correct and aligned with the decision to discontinue HF.

app.py (1)

333-341: st.chat_input accept_file usage is valid in Streamlit ≥1.44.0
st.chat_input returns a dict-like object with .text and .files when accept_file=True, so the existing code is correct if you’re on Streamlit 1.44.0 or newer. Pin your Streamlit version accordingly or, for older releases, fall back to st.file_uploader.

Likely an incorrect or invalid review comment.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link

@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: 0

🧹 Nitpick comments (1)
.gitignore (1)

147-149: Remove redundant .idea/**/.DS_Store entry

.DS_Store already ignores this file in all directories, and .idea/ ignores the entire folder. The third rule is redundant.

Apply this diff:

 .DS_Store
 .idea/
-.idea/**/.DS_Store
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4f28ec0 and ff5fef5.

📒 Files selected for processing (1)
  • .gitignore (1 hunks)

@barun-saha
Copy link
Owner

@barun-saha Rebased the latest changes and fixed review comments. Also tested along with the New Chat feature end-to-end.

Thanks, @sairampillai. Have added a few more comments.

Also, I'm not very familiar with git rebase; I see the contributions from another PR appearing in the history here. That's fine I guess. Also, I resolved a merge conflict with .gitignore.

After all review comments are addressed, I'll run the changes locally.

Copy link

@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

🧹 Nitpick comments (10)
helpers/llm_helper.py (4)

124-149: Align return type with behavior and docstring

get_litellm_model_name may return None; update annotation and import Optional for accuracy.

-from typing import Tuple, Union, Iterator
+from typing import Tuple, Union, Iterator, Optional
@@
-def get_litellm_model_name(provider: str, model: str) -> str:
+def get_litellm_model_name(provider: str, model: str) -> Optional[str]:
@@
-    :return: LiteLLM-compatible model name, or None if provider is not supported.
+    :return: LiteLLM-compatible model name, or None if provider is not supported.

226-228: Use logging.exception without explicit exception arg (TRY401)

logging.exception already records the current exception; drop the redundant %s.

-        logger.exception('Error in LiteLLM completion: %s', e)
+        logger.exception('Error in LiteLLM completion')

256-281: Annotate stream return type for clarity

Small readability win: declare that stream yields strings.

-        def stream(self, prompt: str):
+        def stream(self, prompt: str) -> Iterator[str]:

299-308: Remove HF sample to avoid confusion

HF LLM support is discontinued; drop the '[hf]...' example from main.

     inputs = [
         '[co]Cohere',
-        '[hf]mistralai/Mistral-7B-Instruct-v0.2',
         '[gg]gemini-1.5-flash-002'
     ]

Based on learnings

app.py (6)

215-223: Simplify provider extraction and use lazy logging

Minor cleanup; avoids duplicate parsing and aligns with logging style.

-        provider_match = GlobalConfig.PROVIDER_REGEX.match(llm_provider_to_use)
-        if provider_match:
-            selected_provider = provider_match.group(1)
-        else:
-            # If regex doesn't match, try to extract provider from the beginning
-            selected_provider = llm_provider_to_use.split(' ')[0] if ' ' in llm_provider_to_use else llm_provider_to_use
-            logger.warning(f"Provider regex did not match for: {llm_provider_to_use}, using: {selected_provider}")
+        if provider_match:
+            selected_provider = provider_match.group(1)
+        else:
+            selected_provider = (
+                llm_provider_to_use.split(' ')[0]
+                if ' ' in llm_provider_to_use else llm_provider_to_use
+            )
+            logger.warning('Provider regex did not match for: %s, using: %s',
+                           llm_provider_to_use, selected_provider)

481-487: Remove HF-specific exception path

Hugging Face LLM support is discontinued; this catch and the import are dead weight here.

-        except huggingface_hub.errors.ValidationError as ve:
-            handle_error(
-                f'An error occurred while trying to generate the content: {ve}'
-                '\nPlease try again with a significantly shorter input text.',
-                True
-            )
-            return

Also remove the unused import at the top:

-import huggingface_hub

Based on learnings


498-509: Make “payment required” message provider-agnostic

This message still instructs users to use a Hugging Face token; replace with a generic provider message.

-            if 'payment required' in _msg.lower():
+            if 'payment required' in _msg.lower():
                 handle_error(
-                    'The available inference quota has exhausted.'
-                    ' Please use your own Hugging Face access token. Paste your token in'
-                    ' the input field on the sidebar to the left.'
-                    '\n\nDon\'t have a token? Get your free'
-                    ' [HF access token](https://huggingface.co/settings/tokens) now'
-                    ' and start creating your slide deck! For gated models, you may need to'
-                    ' visit the model\'s page and accept the terms or service.'
-                    '\n\nAlternatively, choose a different LLM and provider from the list.',
+                    f'Payment or quota issue for the selected provider ({provider}).'
+                    ' Please verify your API key/billing limits for the provider account,'
+                    ' or select a different model/provider and try again.',
                     should_log=True
                 )

364-372: Use the PDF_FILE_KEY constant instead of the hard-coded key

Prevents drift if the constant changes.

-                    st.text(
-                        f'Extracting page {st.session_state["start_page"]} in'
-                        f' {st.session_state["pdf_file"].name}'
-                    )
+                    st.text(
+                        f'Extracting page {st.session_state["start_page"]} in'
+                        f' {st.session_state[PDF_FILE_KEY].name}'
+                    )
@@
-                    st.text(
-                        f'Extracting pages {st.session_state["start_page"]} to'
-                        f' {st.session_state["end_page"]} in {st.session_state["pdf_file"].name}'
-                    )
+                    st.text(
+                        f'Extracting pages {st.session_state["start_page"]} to'
+                        f' {st.session_state["end_page"]} in {st.session_state[PDF_FILE_KEY].name}'
+                    )

612-612: Use logging.exception without explicit exception arg (TRY401)

Remove the redundant %s argument.

-        logger.exception('Caught a generic exception: %s', str(ex))
+        logger.exception('Caught a generic exception while creating PPTX')

60-68: Clarify Azure arg names and ordering

Rename the Azure parameters in are_all_inputs_valid to azure_endpoint_url then azure_deployment_name, and update the sole call site in app.py (lines 398–401):

 def are_all_inputs_valid(
-        user_prompt: str,
-        provider: str,
-        selected_model: str,
-        user_key: str,
-        azure_deployment_url: str = '',
-        azure_endpoint_name: str = '',
+        user_prompt: str,
+        provider: str,
+        selected_model: str,
+        user_key: str,
+        azure_endpoint_url: str = '',
+        azure_deployment_name: str = '',
         azure_api_version: str = '',
 ) -> bool:
@@
     if not llm_helper.is_valid_llm_provider_model(
-            provider, selected_model, user_key,
-            azure_endpoint_name, azure_deployment_url, azure_api_version
+            provider, selected_model, user_key,
+            azure_endpoint_url, azure_deployment_name, azure_api_version
     ):
@@
-        if not are_all_inputs_valid(
-                prompt_text, provider, llm_name, user_key,
-                az_deployment, az_endpoint, api_ver
+        if not are_all_inputs_valid(
+                prompt_text, provider, llm_name, user_key,
+                az_endpoint, az_deployment, api_ver
         ):
             return
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cb43301 and 80f53c9.

📒 Files selected for processing (2)
  • app.py (7 hunks)
  • helpers/llm_helper.py (3 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-23T04:32:09.863Z
Learnt from: barun-saha
PR: barun-saha/slide-deck-ai#139
File: helpers/llm_helper.py:70-84
Timestamp: 2025-09-23T04:32:09.863Z
Learning: Hugging Face model support has been discontinued in the slide-deck-ai project, so PROVIDER_HUGGING_FACE should remain commented out in GlobalConfig.VALID_PROVIDERS and validation should continue rejecting 'hf' providers.

Applied to files:

  • app.py
  • helpers/llm_helper.py
🧬 Code graph analysis (2)
app.py (3)
global_config.py (1)
  • GlobalConfig (16-198)
helpers/chat_helper.py (4)
  • StreamlitChatMessageHistory (30-46)
  • ChatPromptTemplate (49-60)
  • from_template (56-57)
  • HumanMessage (16-20)
helpers/llm_helper.py (1)
  • get_litellm_llm (231-292)
helpers/llm_helper.py (1)
global_config.py (1)
  • GlobalConfig (16-198)
🪛 Ruff (0.13.1)
app.py

387-388: Possible SQL injection vector through string-based query construction

(S608)


612-612: Redundant exception object included in logging.exception call

(TRY401)

helpers/llm_helper.py

176-176: Avoid specifying long messages outside the exception class

(TRY003)


183-183: Avoid specifying long messages outside the exception class

(TRY003)


227-227: Redundant exception object included in logging.exception call

(TRY401)


254-254: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (2)
app.py (1)

224-228: Good: single UI error + stop, lazy logging

This resolves the duplicate error display noted earlier and follows the logging guideline.

helpers/llm_helper.py (1)

189-210: Add timeout and OpenRouter base_url

Add a per-request timeout (e.g. 30s) to request_params and set base_url for PROVIDER_OPENROUTER to "https://openrouter.ai/api/v1".

     request_params = {
         'model': litellm_model,
         'messages': messages,
         'max_tokens': max_tokens,
         'temperature': GlobalConfig.LLM_MODEL_TEMPERATURE,
         'stream': True,
+        'timeout': 30,
     }
@@
-    if provider != GlobalConfig.PROVIDER_OLLAMA:
-        # For OpenRouter, pass API key as parameter
-        if provider == GlobalConfig.PROVIDER_OPENROUTER:
-            request_params['api_key'] = api_key
+    if provider != GlobalConfig.PROVIDER_OLLAMA:
+        if provider == GlobalConfig.PROVIDER_OPENROUTER:
+            request_params['api_key'] = api_key
+            request_params['base_url'] = 'https://openrouter.ai/api/v1'

Comment on lines +198 to 214
selected_option = st.sidebar.selectbox(
label='2: Select a suitable LLM to use:\n\n(Gemini and Mistral-Nemo are recommended)',
options=[f'{k} ({v["description"]})' for k, v in GlobalConfig.VALID_MODELS.items()],
index=GlobalConfig.DEFAULT_MODEL_INDEX,
help=GlobalConfig.LLM_PROVIDER_HELP,
on_change=reset_api_key
).split(' ')[0]
)

# Extract provider key more robustly using regex
provider_match = GlobalConfig.PROVIDER_REGEX.match(selected_option)
if provider_match:
llm_provider_to_use = selected_option # Use full string for get_provider_model
else:
# Fallback: try to extract the key before the first space
llm_provider_to_use = selected_option.split(' ')[0]
logger.warning(f"Could not parse provider from selectbox option: {selected_option}")

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Bug: description suffix leaks into provider/model string

selected_option includes a trailing description (e.g., "[az]azure/open-ai (…)" ). Passing it to get_provider_model yields an invalid model like "azure/open-ai (…)" and can break token limits lookups.

-        selected_option = st.sidebar.selectbox(
+        selected_option = st.sidebar.selectbox(
             label='2: Select a suitable LLM to use:\n\n(Gemini and Mistral-Nemo are recommended)',
             options=[f'{k} ({v["description"]})' for k, v in GlobalConfig.VALID_MODELS.items()],
             index=GlobalConfig.DEFAULT_MODEL_INDEX,
             help=GlobalConfig.LLM_PROVIDER_HELP,
             on_change=reset_api_key
         )
-
-        # Extract provider key more robustly using regex
-        provider_match = GlobalConfig.PROVIDER_REGEX.match(selected_option)
-        if provider_match:
-            llm_provider_to_use = selected_option  # Use full string for get_provider_model
-        else:
-            # Fallback: try to extract the key before the first space
-            llm_provider_to_use = selected_option.split(' ')[0]
-            logger.warning(f"Could not parse provider from selectbox option: {selected_option}")
+        # Always strip the description; keep only the "[xx]model" token
+        llm_provider_to_use = selected_option.split(' ', 1)[0]
+        provider_match = GlobalConfig.PROVIDER_REGEX.match(llm_provider_to_use)
+        if not provider_match:
+            logger.warning('Could not parse provider from selectbox option: %s', selected_option)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
selected_option = st.sidebar.selectbox(
label='2: Select a suitable LLM to use:\n\n(Gemini and Mistral-Nemo are recommended)',
options=[f'{k} ({v["description"]})' for k, v in GlobalConfig.VALID_MODELS.items()],
index=GlobalConfig.DEFAULT_MODEL_INDEX,
help=GlobalConfig.LLM_PROVIDER_HELP,
on_change=reset_api_key
).split(' ')[0]
)
# Extract provider key more robustly using regex
provider_match = GlobalConfig.PROVIDER_REGEX.match(selected_option)
if provider_match:
llm_provider_to_use = selected_option # Use full string for get_provider_model
else:
# Fallback: try to extract the key before the first space
llm_provider_to_use = selected_option.split(' ')[0]
logger.warning(f"Could not parse provider from selectbox option: {selected_option}")
selected_option = st.sidebar.selectbox(
label='2: Select a suitable LLM to use:\n\n(Gemini and Mistral-Nemo are recommended)',
options=[f'{k} ({v["description"]})' for k, v in GlobalConfig.VALID_MODELS.items()],
index=GlobalConfig.DEFAULT_MODEL_INDEX,
help=GlobalConfig.LLM_PROVIDER_HELP,
on_change=reset_api_key
)
# Always strip the description; keep only the "[xx]model" token
llm_provider_to_use = selected_option.split(' ', 1)[0]
provider_match = GlobalConfig.PROVIDER_REGEX.match(llm_provider_to_use)
if not provider_match:
logger.warning('Could not parse provider from selectbox option: %s', selected_option)

Comment on lines +178 to +187
# Convert to LiteLLM model name
if provider == GlobalConfig.PROVIDER_AZURE_OPENAI:
from langchain_openai import AzureChatOpenAI

logger.debug('Getting LLM via Azure OpenAI: %s', model)

# The `model` parameter is not used here; `azure_deployment` points to the desired name
return AzureChatOpenAI(
azure_deployment=azure_deployment_name,
api_version=azure_api_version,
azure_endpoint=azure_endpoint_url,
temperature=GlobalConfig.LLM_MODEL_TEMPERATURE,
# max_tokens=max_new_tokens,
timeout=None,
max_retries=1,
api_key=api_key,
)

if provider == GlobalConfig.PROVIDER_OPENROUTER:
# Use langchain-openai's ChatOpenAI for OpenRouter
from langchain_openai import ChatOpenAI
# For Azure OpenAI, use the deployment name as the model
# This is consistent with Azure OpenAI's requirement to use deployment names
if not azure_deployment_name:
raise ValueError("Azure deployment name is required for Azure OpenAI provider")
litellm_model = f'azure/{azure_deployment_name}'
else:
litellm_model = get_litellm_model_name(provider, model)

Copy link

@coderabbitai coderabbitai bot Sep 29, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard against unsupported provider/model name

If get_litellm_model_name returns None, we currently pass model=None to LiteLLM.

     else:
-        litellm_model = get_litellm_model_name(provider, model)
+        litellm_model = get_litellm_model_name(provider, model)
+        if not litellm_model:
+            raise ValueError(
+                'Unsupported provider/model; select a supported LLM from the list.'
+            )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Convert to LiteLLM model name
if provider == GlobalConfig.PROVIDER_AZURE_OPENAI:
from langchain_openai import AzureChatOpenAI
logger.debug('Getting LLM via Azure OpenAI: %s', model)
# The `model` parameter is not used here; `azure_deployment` points to the desired name
return AzureChatOpenAI(
azure_deployment=azure_deployment_name,
api_version=azure_api_version,
azure_endpoint=azure_endpoint_url,
temperature=GlobalConfig.LLM_MODEL_TEMPERATURE,
# max_tokens=max_new_tokens,
timeout=None,
max_retries=1,
api_key=api_key,
)
if provider == GlobalConfig.PROVIDER_OPENROUTER:
# Use langchain-openai's ChatOpenAI for OpenRouter
from langchain_openai import ChatOpenAI
# For Azure OpenAI, use the deployment name as the model
# This is consistent with Azure OpenAI's requirement to use deployment names
if not azure_deployment_name:
raise ValueError("Azure deployment name is required for Azure OpenAI provider")
litellm_model = f'azure/{azure_deployment_name}'
else:
litellm_model = get_litellm_model_name(provider, model)
# Convert to LiteLLM model name
if provider == GlobalConfig.PROVIDER_AZURE_OPENAI:
# For Azure OpenAI, use the deployment name as the model
# This is consistent with Azure OpenAI's requirement to use deployment names
if not azure_deployment_name:
raise ValueError("Azure deployment name is required for Azure OpenAI provider")
litellm_model = f'azure/{azure_deployment_name}'
else:
litellm_model = get_litellm_model_name(provider, model)
if not litellm_model:
raise ValueError(
'Unsupported provider/model; select a supported LLM from the list.'
)
🧰 Tools
🪛 Ruff (0.13.1)

183-183: Avoid specifying long messages outside the exception class

(TRY003)

🤖 Prompt for AI Agents
In helpers/llm_helper.py around lines 178 to 187, the code may call
get_litellm_model_name and end up with litellm_model being None which then gets
passed to LiteLLM; update the logic so after calling
get_litellm_model_name(provider, model) you check if the return value is None
and if so raise a clear ValueError (including provider and model in the message)
instead of allowing None to propagate; keep the existing Azure OpenAI branch (it
should still validate azure_deployment_name and set litellm_model =
f'azure/{azure_deployment_name}'), otherwise assign the non-None litellm_model
returned by get_litellm_model_name.

Copy link
Owner

Choose a reason for hiding this comment

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

@sairampillai Pls address this concern. If LiteLLM model is None, no further code execution here.

Copy link

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Owner

@barun-saha barun-saha left a comment

Choose a reason for hiding this comment

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

Thanks. Need some bug fixing, redundancy removal, and formatting changes.

help=GlobalConfig.LLM_PROVIDER_HELP,
on_change=reset_api_key
).split(' ')[0]
)
Copy link
Owner

Choose a reason for hiding this comment

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

This copies the description along with the model name, leading to LiteLLM error. Append .split(' ')[0] to this line. See the original code.

Copy link
Owner

@barun-saha barun-saha left a comment

Choose a reason for hiding this comment

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

Thanks. Need a bug fix, redundancy removal, and some formatting changes.

azure_deployment: str = ''
api_version: str = ''
else:
# The online LLMs
Copy link
Owner

Choose a reason for hiding this comment

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

Let's replace selected_option with llm_provider_to_use. LLM model validations would follow, so no problem is somehow a name in wrong format appears here (which would be rejected).

)

# Extract provider key more robustly using regex
provider_match = GlobalConfig.PROVIDER_REGEX.match(selected_option)
Copy link
Owner

Choose a reason for hiding this comment

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

Can you pls inspect lines 207 through 222? Why is PROVIDER_REGEX matched twice? Seems like duplicate steps.

selected_provider = provider_match.group(1)
else:
# If regex doesn't match, try to extract provider from the beginning
selected_provider = llm_provider_to_use.split(' ')[0] if ' ' in llm_provider_to_use else llm_provider_to_use
Copy link
Owner

Choose a reason for hiding this comment

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

Pls split lines longer than 100 chars using ( and ).

else:
# If regex doesn't match, try to extract provider from the beginning
selected_provider = llm_provider_to_use.split(' ')[0] if ' ' in llm_provider_to_use else llm_provider_to_use
logger.warning(f"Provider regex did not match for: {llm_provider_to_use}, using: {selected_provider}")
Copy link
Owner

Choose a reason for hiding this comment

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

Pls use lazy formatting with logs.

Comment on lines +178 to +187
# Convert to LiteLLM model name
if provider == GlobalConfig.PROVIDER_AZURE_OPENAI:
from langchain_openai import AzureChatOpenAI

logger.debug('Getting LLM via Azure OpenAI: %s', model)

# The `model` parameter is not used here; `azure_deployment` points to the desired name
return AzureChatOpenAI(
azure_deployment=azure_deployment_name,
api_version=azure_api_version,
azure_endpoint=azure_endpoint_url,
temperature=GlobalConfig.LLM_MODEL_TEMPERATURE,
# max_tokens=max_new_tokens,
timeout=None,
max_retries=1,
api_key=api_key,
)

if provider == GlobalConfig.PROVIDER_OPENROUTER:
# Use langchain-openai's ChatOpenAI for OpenRouter
from langchain_openai import ChatOpenAI
# For Azure OpenAI, use the deployment name as the model
# This is consistent with Azure OpenAI's requirement to use deployment names
if not azure_deployment_name:
raise ValueError("Azure deployment name is required for Azure OpenAI provider")
litellm_model = f'azure/{azure_deployment_name}'
else:
litellm_model = get_litellm_model_name(provider, model)

Copy link
Owner

Choose a reason for hiding this comment

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

@sairampillai Pls address this concern. If LiteLLM model is None, no further code execution here.



def get_langchain_llm(
def get_litellm_model_name(provider: str, model: str) -> str:
Copy link
Owner

Choose a reason for hiding this comment

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

Pls update the return type to Optional[str].

@sairampillai
Copy link
Author

@barun-saha Will update and test in the next couple of days and push the changes

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.

3 participants