feat: Add BYOK (Bring Your Own Key) support for Groq API#145
feat: Add BYOK (Bring Your Own Key) support for Groq API#145Nihallllll wants to merge 2 commits intoAOSSIE-Org:mainfrom
Conversation
- Replace deprecated gemma2-9b-it model with llama-3.3-70b-versatile - Add BYOK support: API key passed via X-BYOK-Api-Key request header - Add groq_api_key and groq_model fields to LangGraph pipeline state - Add SettingsModal component for users to configure their own Groq key - Fix chat: send article_text as context so LLM can actually answer questions - Fix chat: forward BYOK headers on chat requests - Make fact-check failure non-fatal (soft failure continues with empty facts) - Fix results page: add header with working Perspective → /analyze navigation link - Switch frontend API base URL from hardcoded HF Space to localhost:7860
📝 WalkthroughWalkthroughThis PR introduces bring-your-own-key (BYOK) authentication with configurable model selection across the application. It transitions from module-level Groq client initialization using environment variables to per-call client creation with explicit Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 15
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
backend/app/modules/facts_check/llm_processing.py (1)
143-148:⚠️ Potential issue | 🟠 MajorPrevent
UnboundLocalErrorwhen LLM JSON parsing fails.If parsing fails on Line 145,
parsedis undefined at Line 148.Suggested fix
try: parsed = json.loads(content) except Exception as parse_err: logger.error(f"LLM JSON parse error: {parse_err}") + parsed = { + "verdict": "Unknown", + "explanation": "Failed to parse verifier output", + "original_claim": claim, + "source_link": source, + } results_list.append(parsed)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/app/modules/facts_check/llm_processing.py` around lines 143 - 148, The JSON parsing try/except in llm_processing.py can leave `parsed` undefined if json.loads(content) raises; update the except handler in the block around the current try/except (the JSON parse around `parsed = json.loads(content)` and `results_list.append(parsed)`) to either set `parsed` to a safe default (e.g., None or an empty dict) or skip appending when parsing fails, and keep logging the error via `logger.error`; ensure `results_list.append(parsed)` only runs when `parsed` is defined/valid to prevent UnboundLocalError.backend/app/modules/facts_check/web_search.py (1)
35-40:⚠️ Potential issue | 🟠 MajorHandle empty
itemsarrays, not only missing keys.Lines 35-36 avoid
KeyError, butitems: []still raisesIndexErrorat Line 38.Suggested fix
- if "items" not in res: + if not res.get("items"): return []🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/app/modules/facts_check/web_search.py` around lines 35 - 40, The code currently checks only for the presence of the "items" key and then indexes res["items"][0], which will raise IndexError when items is an empty list; replace that logic by reading items = res.get("items", []) and then if not items: return [] to handle empty lists (or None) safely, then populate first using items[0]["title"], items[0]["link"], items[0]["snippet"]; update the block that references res and first accordingly so it never indexes into an empty items array.frontend/app/analyze/results/page.tsx (1)
93-103:⚠️ Potential issue | 🟠 MajorAdd chat request error handling to prevent UI dead-ends.
If
/api/chatreturns 401/network error, this throws and leaves the chat flow without user feedback.Proposed fix
- const res = await axios.post(`${BACKEND}/api/chat`, { - message: message, - article_text: analysisData?.cleaned_text ?? "", - }, { headers: { "X-BYOK-Api-Key": getActiveApiKey() ?? "", "X-BYOK-Model": getActiveModel() ?? "" } }); - const data = res.data; - - console.log(data); - - // 🔹 Step 2: Append LLM’s response - setMessages([...newMessages, { role: "assistant", content: data.answer }]); + try { + const apiKey = getActiveApiKey(); + const model = getActiveModel(); + const headers: Record<string, string> = {}; + if (apiKey) headers["X-BYOK-Api-Key"] = apiKey; + if (model) headers["X-BYOK-Model"] = model; + + const { data } = await axios.post( + `${BACKEND}/api/chat`, + { + message, + article_text: analysisData?.cleaned_text ?? "", + }, + { headers } + ); + setMessages([...newMessages, { role: "assistant", content: data.answer }]); + } catch (error) { + setMessages([ + ...newMessages, + { + role: "assistant", + content: "I couldn’t reach the chat service. Check API settings and try again.", + }, + ]); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/app/analyze/results/page.tsx` around lines 93 - 103, Wrap the axios.post call that sends to `${BACKEND}/api/chat` in a try/catch around the existing code in page.tsx (the block that creates `res` and `data`), detect HTTP 401 specifically via `err.response?.status === 401` and network/other errors via lack of `err.response`, and in each case update UI state instead of letting the exception bubble: call `setMessages` (or a dedicated error state) to append an assistant/system message describing the error (e.g., "Authentication error: please check API key" for 401, "Network error: please try again" for network failures), ensure any loading flags are cleared, and optionally log the full error to console for debugging; keep references to `newMessages`, `setMessages`, `getActiveApiKey`, `getActiveModel`, and `analysisData` when implementing the catch handling so the flow continues gracefully.
🧹 Nitpick comments (2)
backend/app/logging/logging_config.py (1)
28-32: Consider narrowing the exception type.Catching bare
Exceptioncan mask unexpected errors. The likely failure modes here areOSError(iffileno()oropen()fails) andAttributeError(ifsys.stdoutlacksfileno(), e.g., in certain IDEs or test harnesses).♻️ Proposed fix
try: utf8_stream = open(sys.stdout.fileno(), mode="w", encoding="utf-8", closefd=False) console_handler = logging.StreamHandler(utf8_stream) - except Exception: + except (OSError, AttributeError): console_handler = logging.StreamHandler(sys.stdout)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/app/logging/logging_config.py` around lines 28 - 32, Replace the broad "except Exception" with targeted exceptions: catch OSError and AttributeError around the utf8_stream = open(sys.stdout.fileno(), ...) and console_handler = logging.StreamHandler(utf8_stream) block so only expected failures (fileno()/open issues or missing fileno) fall through to the fallback console_handler = logging.StreamHandler(sys.stdout); keep the same fallback behavior and ensure utf8_stream and console_handler symbols are used exactly as in the diff.backend/app/modules/pipeline.py (1)
67-70: Consider centralizing the default model constant.
"llama-3.3-70b-versatile"is duplicated across modules; a shared constant reduces drift and accidental mismatch.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/app/modules/pipeline.py` around lines 67 - 70, Replace the hard-coded model string in run_langgraph_workflow with a shared constant: define a single exported constant (e.g., DEFAULT_GROQ_MODEL or GROQ_MODEL_DEFAULT) in your central config/constants module and import it into this module, then change the function signature of run_langgraph_workflow to use that constant as the default for groq_model and update any other occurrences of "llama-3.3-70b-versatile" to reference that same constant so all modules use the single source of truth.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@backend/app/modules/bias_detection/check_bias.py`:
- Around line 36-38: The Groq client is being constructed before the try block
in check_bias, so initialization/validation errors escape your structured error
handling; move the Groq(api_key=api_key) call inside the try (or add a
surrounding try/except that wraps the instantiation) and handle exceptions there
(using the same error response path you use for subsequent calls), referencing
the check_bias function and the Groq client instantiation to ensure any
instantiation errors are caught and returned in the structured error response.
In `@backend/app/modules/chat/llm_processing.py`:
- Line 46: The current logger.debug call logs the full LLM context in
pinecone_context which can leak sensitive text and create huge logs; modify the
logging in llm_processing.py to avoid printing raw content by replacing the
logger.debug(f"Generated context for LLM:\n{pinecone_context}") call with a
metadata-only log: record the length/size (e.g., len(pinecone_context) or number
of chunks), an optional content hash (e.g., SHA256) or first N characters only
if needed for debugging, and any token/count metrics instead of the full
pinecone_context string; update the code paths that reference logger.debug and
ensure pinecone_context is never logged in full.
In `@backend/app/modules/facts_check/llm_processing.py`:
- Around line 39-40: The code instantiates Groq with state["groq_api_key"]
outside the try block which can raise KeyError before your structured error
handling runs; inside the claim extractor function, move the API key access into
the existing try (or guard it with state.get and raise a controlled error) and
then construct client = Groq(api_key=api_key) within that try so any missing-key
exception is caught and handled (or explicitly check for the key first and call
your error/log path if absent). Ensure you reference the same identifiers
(state, Groq, client) and preserve the existing error logging/handling flow.
In `@backend/app/modules/langgraph_builder.py`:
- Around line 55-56: The store_and_send node is leaking sensitive fields
(groq_api_key and groq_model) by logging the full state and returning it; update
the store_and_send function to (1) remove or redact these keys before logging
(or remove the debug log entirely) and (2) filter them out of the returned state
so the HTTP response from the /process pipeline does not include groq_api_key or
groq_model; locate the store_and_send implementation and replace the return
value with a sanitized dict that excludes "groq_api_key" and "groq_model" (and
ensure pipeline.py/routes.py continue to receive only the sanitized state).
In `@backend/app/modules/langgraph_nodes/fact_check.py`:
- Around line 35-38: The warning currently logs the raw upstream payload via
logger.warning(f"Fact-checking skipped (non-fatal): {error_message}") which can
leak sensitive provider text; change the logging in fact_check.py to emit a
sanitized/structured message instead (e.g., include a fixed description,
optional provider identifier or error code if available, and omit or truncate
the raw error_message) while preserving the soft-failure behavior that sets
verifications = [] so the pipeline continues; update the logger call to use the
sanitized fields rather than logging error_message verbatim.
In `@backend/app/modules/langgraph_nodes/generate_perspective.py`:
- Around line 55-60: The facts_str construction in generate_perspective.py is
using only the first line as an f-string so "{f['verdict']}" and
"{f['explanation']}" are not interpolated; update the list comprehension that
builds facts_str (the expression assigning facts_str) so each item is a single
f-string that includes f['original_claim'], f['verdict'], and f['explanation']
(or alternatively concatenate three f-strings per item) and ensure the
comprehension iterates over (facts or [])—i.e., replace the current mix of plain
and f-strings with a single f"Claim: {f['original_claim']}\nVerdict:
{f['verdict']}\nExplanation: {f['explanation']}" for f in (facts or []).
In `@backend/app/modules/langgraph_nodes/judge.py`:
- Around line 27-33: The ChatGroq instance is created before the try block and
uses direct indexing state["groq_api_key"], which can raise uncaught KeyError;
move the ChatGroq construction (groq_llm = ChatGroq(...)) inside the existing
try block and first validate the key (e.g., check "groq_api_key" in state and
that it is not empty) before instantiation; update references to groq_llm in
this scope so errors are caught and return the node-level error response on
failure.
In `@backend/app/modules/langgraph_nodes/sentiment.py`:
- Around line 28-29: The Groq client instantiation uses state["groq_api_key"]
outside the try block so a missing key raises KeyError before your error
handler; move the API-key access into the existing try/except (or use
state.get("groq_api_key") inside the try) so the client = Groq(api_key=...) call
is executed under the same error handling, and add a clear error/raise path if
the key is None—referencing the Groq constructor, state dict and the current try
block in sentiment.py.
In `@backend/app/routes/routes.py`:
- Around line 69-70: The call passes the entire scraper result dict (content)
into check_bias causing prompt issues; update the route to extract the text
field returned by run_scraper_pipeline (e.g., use content["cleaned_text"] or
content.get("cleaned_text")) and pass that string to check_bias along with
api_key and groq_model so check_bias receives raw article text rather than the
whole dict.
- Around line 94-95: In the /chat route, avoid blocking the event loop by
running the synchronous network calls in the thread pool: replace direct calls
to search_pinecone(query) and ask_llm(query, results, api_key, groq_model,
chat_request.article_text) with asyncio.to_thread(...) calls (i.e., await
asyncio.to_thread(search_pinecone, query) and await asyncio.to_thread(ask_llm,
query, results, api_key, groq_model, chat_request.article_text)); ensure asyncio
is imported if not already and preserve returned values as before.
In `@backend/pyproject.toml`:
- Line 16: Remove the unused dependency string "langchain-google-genai>=2.1.12"
from the project dependencies (the entry shown in the diff) and update the
project's dependency lockfile (poetry.lock or equivalent) so the removed package
is no longer pinned; ensure no import or usage of langchain_google_genai remains
in the codebase before committing the change.
In `@frontend/app/analyze/loading/page.tsx`:
- Line 20: The BACKEND constant in page.tsx is hardcoded to
"http://localhost:7860"; change it to read from a configurable client-side
environment variable (e.g. NEXT_PUBLIC_BACKEND) with a safe fallback to the
current origin/relative path so it works in staging/production and during local
dev; update the const BACKEND declaration to use process.env.NEXT_PUBLIC_BACKEND
(or equivalent runtime config) and ensure code that uses BACKEND (in this file)
handles undefined by building a URL from window.location.origin or using a
relative path so no hardcoded origin remains.
In `@frontend/app/analyze/results/page.tsx`:
- Line 25: Replace the hardcoded BACKEND constant in
frontend/app/analyze/results/page.tsx with an environment-configured value: read
the client-facing env var (e.g. process.env.NEXT_PUBLIC_BACKEND_URL or
NEXT_PUBLIC_API_URL) and use that for BACKEND, falling back to a sensible
default (such as a relative path) when the env var is absent; update any code
that imports/uses BACKEND to rely on this env-driven value so deployments can
configure the backend URL without changing source.
In `@frontend/components/settings-modal.tsx`:
- Around line 167-169: The privacy copy inside the settings-modal component (the
rounded info div in frontend/components/settings-modal.tsx) is incorrect; update
the text to accurately state that the API key is stored in the browser but may
be forwarded to selected AI providers via requests (for example via the
X-BYOK-Api-Key header) rather than claiming it is “never sent to our servers”;
locate the div rendering the message and replace the sentence with a concise,
factual line noting local storage of the key and that requests to providers may
include the key in headers so users understand it can be transmitted.
- Around line 109-114: The effect that runs on provider change unconditionally
calls setModel(current.defaultModel), which can overwrite a previously saved
activeModel in config; update the useEffect to first check for an existing saved
model (e.g., config.activeModel or a provider-scoped saved model) and only call
setModel(current.defaultModel) when no saved model exists or when the saved
model belongs to a different provider, leaving the saved activeModel intact;
keep the existing setShowKey and setError resets but do not clobber model if a
saved activeModel is present.
---
Outside diff comments:
In `@backend/app/modules/facts_check/llm_processing.py`:
- Around line 143-148: The JSON parsing try/except in llm_processing.py can
leave `parsed` undefined if json.loads(content) raises; update the except
handler in the block around the current try/except (the JSON parse around
`parsed = json.loads(content)` and `results_list.append(parsed)`) to either set
`parsed` to a safe default (e.g., None or an empty dict) or skip appending when
parsing fails, and keep logging the error via `logger.error`; ensure
`results_list.append(parsed)` only runs when `parsed` is defined/valid to
prevent UnboundLocalError.
In `@backend/app/modules/facts_check/web_search.py`:
- Around line 35-40: The code currently checks only for the presence of the
"items" key and then indexes res["items"][0], which will raise IndexError when
items is an empty list; replace that logic by reading items = res.get("items",
[]) and then if not items: return [] to handle empty lists (or None) safely,
then populate first using items[0]["title"], items[0]["link"],
items[0]["snippet"]; update the block that references res and first accordingly
so it never indexes into an empty items array.
In `@frontend/app/analyze/results/page.tsx`:
- Around line 93-103: Wrap the axios.post call that sends to
`${BACKEND}/api/chat` in a try/catch around the existing code in page.tsx (the
block that creates `res` and `data`), detect HTTP 401 specifically via
`err.response?.status === 401` and network/other errors via lack of
`err.response`, and in each case update UI state instead of letting the
exception bubble: call `setMessages` (or a dedicated error state) to append an
assistant/system message describing the error (e.g., "Authentication error:
please check API key" for 401, "Network error: please try again" for network
failures), ensure any loading flags are cleared, and optionally log the full
error to console for debugging; keep references to `newMessages`, `setMessages`,
`getActiveApiKey`, `getActiveModel`, and `analysisData` when implementing the
catch handling so the flow continues gracefully.
---
Nitpick comments:
In `@backend/app/logging/logging_config.py`:
- Around line 28-32: Replace the broad "except Exception" with targeted
exceptions: catch OSError and AttributeError around the utf8_stream =
open(sys.stdout.fileno(), ...) and console_handler =
logging.StreamHandler(utf8_stream) block so only expected failures
(fileno()/open issues or missing fileno) fall through to the fallback
console_handler = logging.StreamHandler(sys.stdout); keep the same fallback
behavior and ensure utf8_stream and console_handler symbols are used exactly as
in the diff.
In `@backend/app/modules/pipeline.py`:
- Around line 67-70: Replace the hard-coded model string in
run_langgraph_workflow with a shared constant: define a single exported constant
(e.g., DEFAULT_GROQ_MODEL or GROQ_MODEL_DEFAULT) in your central
config/constants module and import it into this module, then change the function
signature of run_langgraph_workflow to use that constant as the default for
groq_model and update any other occurrences of "llama-3.3-70b-versatile" to
reference that same constant so all modules use the single source of truth.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
backend/uv.lockis excluded by!**/*.lock
📒 Files selected for processing (20)
backend/app/.gitignorebackend/app/logging/logging_config.pybackend/app/modules/bias_detection/check_bias.pybackend/app/modules/chat/llm_processing.pybackend/app/modules/facts_check/llm_processing.pybackend/app/modules/facts_check/web_search.pybackend/app/modules/langgraph_builder.pybackend/app/modules/langgraph_nodes/fact_check.pybackend/app/modules/langgraph_nodes/generate_perspective.pybackend/app/modules/langgraph_nodes/judge.pybackend/app/modules/langgraph_nodes/sentiment.pybackend/app/modules/pipeline.pybackend/app/routes/routes.pybackend/app/utils/fact_check_utils.pybackend/pyproject.tomlfrontend/app/analyze/loading/page.tsxfrontend/app/analyze/page.tsxfrontend/app/analyze/results/page.tsxfrontend/app/page.tsxfrontend/components/settings-modal.tsx
| def check_bias(text, api_key: str, groq_model: str = "llama-3.3-70b-versatile"): | ||
| client = Groq(api_key=api_key) | ||
| try: |
There was a problem hiding this comment.
Guard client initialization inside error-handling path.
At Line 37, Groq(api_key=api_key) executes before the try, so init/validation errors bypass your structured error response.
Proposed fix
def check_bias(text, api_key: str, groq_model: str = "llama-3.3-70b-versatile"):
- client = Groq(api_key=api_key)
try:
+ if not api_key:
+ raise ValueError("Missing Groq API key")
+ client = Groq(api_key=api_key)
logger.debug(f"Raw article text: {text}")
logger.debug(f"JSON dump of text: {json.dumps(text)}")📝 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.
| def check_bias(text, api_key: str, groq_model: str = "llama-3.3-70b-versatile"): | |
| client = Groq(api_key=api_key) | |
| try: | |
| def check_bias(text, api_key: str, groq_model: str = "llama-3.3-70b-versatile"): | |
| try: | |
| if not api_key: | |
| raise ValueError("Missing Groq API key") | |
| client = Groq(api_key=api_key) | |
| logger.debug(f"Raw article text: {text}") | |
| logger.debug(f"JSON dump of text: {json.dumps(text)}") |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/app/modules/bias_detection/check_bias.py` around lines 36 - 38, The
Groq client is being constructed before the try block in check_bias, so
initialization/validation errors escape your structured error handling; move the
Groq(api_key=api_key) call inside the try (or add a surrounding try/except that
wraps the instantiation) and handle exceptions there (using the same error
response path you use for subsequent calls), referencing the check_bias function
and the Groq client instantiation to ensure any instantiation errors are caught
and returned in the structured error response.
| def ask_llm(question, docs, api_key: str, groq_model: str = "llama-3.3-70b-versatile", article_text: str = ""): | ||
| client = Groq(api_key=api_key) | ||
| pinecone_context = build_context(docs) | ||
| logger.debug(f"Generated context for LLM:\n{pinecone_context}") |
There was a problem hiding this comment.
Avoid logging full LLM context payloads.
Line 46 logs raw context text, which can leak user/article content into logs and create very large log entries. Log size/metadata instead of full text.
🔧 Suggested change
- logger.debug(f"Generated context for LLM:\n{pinecone_context}")
+ logger.debug("Generated LLM context from Pinecone notes (chars=%d)", len(pinecone_context))📝 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.
| logger.debug(f"Generated context for LLM:\n{pinecone_context}") | |
| logger.debug("Generated LLM context from Pinecone notes (chars=%d)", len(pinecone_context)) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/app/modules/chat/llm_processing.py` at line 46, The current
logger.debug call logs the full LLM context in pinecone_context which can leak
sensitive text and create huge logs; modify the logging in llm_processing.py to
avoid printing raw content by replacing the logger.debug(f"Generated context for
LLM:\n{pinecone_context}") call with a metadata-only log: record the length/size
(e.g., len(pinecone_context) or number of chunks), an optional content hash
(e.g., SHA256) or first N characters only if needed for debugging, and any
token/count metrics instead of the full pinecone_context string; update the code
paths that reference logger.debug and ensure pinecone_context is never logged in
full.
| client = Groq(api_key=state["groq_api_key"]) | ||
| try: |
There was a problem hiding this comment.
Guard API key access inside try in claim extractor.
Line 39 can throw KeyError before structured error handling starts.
Suggested fix
def run_claim_extractor_sdk(state):
- client = Groq(api_key=state["groq_api_key"])
try:
+ api_key = state.get("groq_api_key")
+ if not api_key:
+ raise ValueError("Missing 'groq_api_key' in state")
+ client = Groq(api_key=api_key)
text = state.get("cleaned_text")🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/app/modules/facts_check/llm_processing.py` around lines 39 - 40, The
code instantiates Groq with state["groq_api_key"] outside the try block which
can raise KeyError before your structured error handling runs; inside the claim
extractor function, move the API key access into the existing try (or guard it
with state.get and raise a controlled error) and then construct client =
Groq(api_key=api_key) within that try so any missing-key exception is caught and
handled (or explicitly check for the key first and call your error/log path if
absent). Ensure you reference the same identifiers (state, Groq, client) and
preserve the existing error logging/handling flow.
| groq_api_key: str | ||
| groq_model: str |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, find and read the langgraph_builder.py file
fd langgraph_builder.pyRepository: AOSSIE-Org/Perspective
Length of output: 106
🏁 Script executed:
# Once we have the file, let's examine the context around lines 55-56
head -80 backend/app/modules/langgraph_builder.py | tail -40Repository: AOSSIE-Org/Perspective
Length of output: 1105
🏁 Script executed:
# Search for groq_api_key usage throughout the codebase
rg -n 'groq_api_key' --type py -B2 -A2Repository: AOSSIE-Org/Perspective
Length of output: 2957
🏁 Script executed:
# Search for groq_model usage
rg -n 'groq_model' --type py -B2 -A2Repository: AOSSIE-Org/Perspective
Length of output: 7629
🏁 Script executed:
# Search for potential persistence/storage operations
rg -n 'store_and_send|JSONResponse|return\s+\{|insert|upsert|save|persist|log' backend/app/modules/langgraph_builder.py -B2 -A2Repository: AOSSIE-Org/Perspective
Length of output: 1230
🏁 Script executed:
# Check for any JSON/dict serialization that might expose the key
rg -n 'json\.|dumps\|to_dict\|serialize' backend/app/modules/langgraph_builder.py -B2 -A2Repository: AOSSIE-Org/Perspective
Length of output: 48
🏁 Script executed:
# Find store_and_send.py
fd store_and_send.pyRepository: AOSSIE-Org/Perspective
Length of output: 119
🏁 Script executed:
# Read the store_and_send implementation
cat backend/app/modules/langgraph_nodes/store_and_send.pyRepository: AOSSIE-Org/Perspective
Length of output: 1815
🏁 Script executed:
# Check routes.py to see what data is returned from API endpoints
cat backend/app/routes/routes.pyRepository: AOSSIE-Org/Perspective
Length of output: 3748
🏁 Script executed:
# Search for any logging statements that might expose groq_api_key
rg -n 'logger\.|print\(' backend/app/modules/langgraph_builder.py backend/app/modules/langgraph_nodes/ backend/app/modules/pipeline.py -A2Repository: AOSSIE-Org/Perspective
Length of output: 3450
🏁 Script executed:
# Search for return statements that might include the entire state
rg -n 'return\s+\{|return\s+result|return\s+data' backend/app/routes/routes.py -B3 -A3Repository: AOSSIE-Org/Perspective
Length of output: 750
🏁 Script executed:
# Find chunk_rag_data.py
fd chunk_rag_data.pyRepository: AOSSIE-Org/Perspective
Length of output: 116
🏁 Script executed:
# Read the chunk_rag_data implementation
cat backend/app/modules/vector_store/chunk_rag_data.pyRepository: AOSSIE-Org/Perspective
Length of output: 3488
🏁 Script executed:
# Check what fields are actually needed by chunk_rag_data
rg -n 'state\[|state\.get' backend/app/modules/vector_store/chunk_rag_data.pyRepository: AOSSIE-Org/Perspective
Length of output: 48
Strip groq_api_key and groq_model from state before returning from store_and_send node.
The API key is currently exposed in two critical ways:
-
Logged in store_and_send.py (line 29):
logger.debug(f"Received state for vector storage: {state}")logs the entire state including the API key to application logs. -
Returned to clients in HTTP response: The
/processendpoint returns the entire workflow state (viapipeline.py→routes.pyline 84), which includesgroq_api_keyin the JSON response body. This violates BYOK principles by leaking user credentials to clients.
Fix: In store_and_send.py line 53, filter the returned state before returning:
return {k: v for k, v in state.items() if k not in ("groq_api_key", "groq_model")}, "status": "success"}Also remove the debug log at line 29 or exclude sensitive fields from it.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/app/modules/langgraph_builder.py` around lines 55 - 56, The
store_and_send node is leaking sensitive fields (groq_api_key and groq_model) by
logging the full state and returning it; update the store_and_send function to
(1) remove or redact these keys before logging (or remove the debug log
entirely) and (2) filter them out of the returned state so the HTTP response
from the /process pipeline does not include groq_api_key or groq_model; locate
the store_and_send implementation and replace the return value with a sanitized
dict that excludes "groq_api_key" and "groq_model" (and ensure
pipeline.py/routes.py continue to receive only the sanitized state).
| # Soft failure — web search quota/key issue. Continue with empty facts | ||
| # so the rest of the pipeline (generate_perspective, store_and_send) still runs. | ||
| logger.warning(f"Fact-checking skipped (non-fatal): {error_message}") | ||
| verifications = [] |
There was a problem hiding this comment.
Avoid logging raw fact-check error payloads.
Line 37 logs error_message verbatim. This can leak sensitive upstream details into logs. Prefer a sanitized/structured message without raw provider text.
Suggested fix
- logger.warning(f"Fact-checking skipped (non-fatal): {error_message}")
+ logger.warning("Fact-checking skipped (non-fatal). reason=%s", "upstream_fact_check_failure")📝 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.
| # Soft failure — web search quota/key issue. Continue with empty facts | |
| # so the rest of the pipeline (generate_perspective, store_and_send) still runs. | |
| logger.warning(f"Fact-checking skipped (non-fatal): {error_message}") | |
| verifications = [] | |
| # Soft failure — web search quota/key issue. Continue with empty facts | |
| # so the rest of the pipeline (generate_perspective, store_and_send) still runs. | |
| logger.warning("Fact-checking skipped (non-fatal). reason=%s", "upstream_fact_check_failure") | |
| verifications = [] |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/app/modules/langgraph_nodes/fact_check.py` around lines 35 - 38, The
warning currently logs the raw upstream payload via
logger.warning(f"Fact-checking skipped (non-fatal): {error_message}") which can
leak sensitive provider text; change the logging in fact_check.py to emit a
sanitized/structured message instead (e.g., include a fixed description,
optional provider identifier or error code if available, and omit or truncate
the raw error_message) while preserving the soft-failure behavior that sets
verifications = [] so the pipeline continues; update the logger call to use the
sanitized fields rather than logging error_message verbatim.
| "groq>=0.28.0", | ||
| "langchain>=0.3.25", | ||
| "langchain-community>=0.3.25", | ||
| "langchain-google-genai>=2.1.12", |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for actual usages of langchain-google-genai in Python sources
rg -n --type=py -C2 'langchain_google_genai|ChatGoogleGenerativeAI|GoogleGenerativeAI'Repository: AOSSIE-Org/Perspective
Length of output: 48
🏁 Script executed:
#!/bin/bash
# Also search for imports from langchain packages to get broader context
rg -n --type=py 'from langchain.*import|import langchain' | head -50Repository: AOSSIE-Org/Perspective
Length of output: 428
🏁 Script executed:
#!/bin/bash
# Check the pyproject.toml to see all dependencies and any optional groups
cat backend/pyproject.tomlRepository: AOSSIE-Org/Perspective
Length of output: 780
🏁 Script executed:
#!/bin/bash
# Check for any references to google or genai (case-insensitive) in Python files
# to catch indirect references or commented code
rg -in --type=py 'google.*genai|genai.*google'Repository: AOSSIE-Org/Perspective
Length of output: 48
🏁 Script executed:
#!/bin/bash
# Check if there are any imports in __init__.py or similar that might re-export it
fd -t f '\.py$' | xargs grep -l 'google_genai\|GoogleGenerativeAI' 2>/dev/null || echo "No matches found"Repository: AOSSIE-Org/Perspective
Length of output: 82
Remove the unused langchain-google-genai dependency from backend/pyproject.toml.
The langchain-google-genai package (line 16) is not imported or used anywhere in the codebase. Since the PR's focus is on Groq BYOK integration (which uses langchain_groq), this dependency should be removed to reduce unnecessary supply-chain surface area.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/pyproject.toml` at line 16, Remove the unused dependency string
"langchain-google-genai>=2.1.12" from the project dependencies (the entry shown
in the diff) and update the project's dependency lockfile (poetry.lock or
equivalent) so the removed package is no longer pinned; ensure no import or
usage of langchain_google_genai remains in the codebase before committing the
change.
| import { getActiveApiKey, getActiveModel } from "@/components/settings-modal"; | ||
|
|
||
| // const backend_url = process.env.NEXT_PUBLIC_API_URL; | ||
| const BACKEND = "http://localhost:7860"; |
There was a problem hiding this comment.
Avoid hardcoded backend origin in client code.
http://localhost:7860 at Line 20 will fail outside local dev and is likely to break production/staging.
Proposed fix
- const BACKEND = "http://localhost:7860";
+ const BACKEND =
+ process.env.NEXT_PUBLIC_BACKEND_URL ?? "http://localhost:7860";📝 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.
| const BACKEND = "http://localhost:7860"; | |
| const BACKEND = | |
| process.env.NEXT_PUBLIC_BACKEND_URL ?? "http://localhost:7860"; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/app/analyze/loading/page.tsx` at line 20, The BACKEND constant in
page.tsx is hardcoded to "http://localhost:7860"; change it to read from a
configurable client-side environment variable (e.g. NEXT_PUBLIC_BACKEND) with a
safe fallback to the current origin/relative path so it works in
staging/production and during local dev; update the const BACKEND declaration to
use process.env.NEXT_PUBLIC_BACKEND (or equivalent runtime config) and ensure
code that uses BACKEND (in this file) handles undefined by building a URL from
window.location.origin or using a relative path so no hardcoded origin remains.
| import axios from "axios"; | ||
| import { getActiveApiKey, getActiveModel } from "@/components/settings-modal"; | ||
|
|
||
| const BACKEND = "http://localhost:7860"; |
There was a problem hiding this comment.
Use environment-configured backend URL instead of localhost literal.
Hardcoding http://localhost:7860 in client code creates immediate deployment fragility.
Proposed fix
- const BACKEND = "http://localhost:7860";
+ const BACKEND =
+ process.env.NEXT_PUBLIC_BACKEND_URL ?? "http://localhost:7860";📝 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.
| const BACKEND = "http://localhost:7860"; | |
| const BACKEND = | |
| process.env.NEXT_PUBLIC_BACKEND_URL ?? "http://localhost:7860"; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/app/analyze/results/page.tsx` at line 25, Replace the hardcoded
BACKEND constant in frontend/app/analyze/results/page.tsx with an
environment-configured value: read the client-facing env var (e.g.
process.env.NEXT_PUBLIC_BACKEND_URL or NEXT_PUBLIC_API_URL) and use that for
BACKEND, falling back to a sensible default (such as a relative path) when the
env var is absent; update any code that imports/uses BACKEND to rely on this
env-driven value so deployments can configure the backend URL without changing
source.
| useEffect(() => { | ||
| const current = PROVIDERS[provider as keyof typeof PROVIDERS]; | ||
| setModel(current.defaultModel); | ||
| setShowKey(false); | ||
| setError(""); | ||
| }, [provider]); |
There was a problem hiding this comment.
Saved model can be overwritten during initialization.
The provider effect resets model to default unconditionally; this can wipe a previously saved activeModel from config on mount.
Proposed fix
+ const [configLoaded, setConfigLoaded] = useState(false);
useEffect(() => {
const config = loadApiConfig();
if (config) {
const savedProvider = config.activeProvider in PROVIDERS ? config.activeProvider : "groq";
setProvider(savedProvider);
setModel(config.activeModel || PROVIDERS.groq.defaultModel);
setKeys((prev) => ({ ...prev, ...config.keys }));
}
+ setConfigLoaded(true);
if (!isConfigured() || forceOpen) {
setFirstRun(!isConfigured());
setOpen(true);
}
}, [forceOpen]);
useEffect(() => {
+ if (!configLoaded) return;
const current = PROVIDERS[provider as keyof typeof PROVIDERS];
setModel(current.defaultModel);
setShowKey(false);
setError("");
- }, [provider]);
+ }, [provider, configLoaded]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/components/settings-modal.tsx` around lines 109 - 114, The effect
that runs on provider change unconditionally calls
setModel(current.defaultModel), which can overwrite a previously saved
activeModel in config; update the useEffect to first check for an existing saved
model (e.g., config.activeModel or a provider-scoped saved model) and only call
setModel(current.defaultModel) when no saved model exists or when the saved
model belongs to a different provider, leaving the saved activeModel intact;
keep the existing setShowKey and setError resets but do not clobber model if a
saved activeModel is present.
| <div className="rounded-lg bg-blue-50 dark:bg-blue-950/40 border border-blue-200 dark:border-blue-800 p-3 text-sm text-blue-800 dark:text-blue-200"> | ||
| To use Perspective, select a free AI provider and paste your API key. Your key is stored only in your browser — never sent to our servers. | ||
| </div> |
There was a problem hiding this comment.
First-run privacy copy is factually incorrect.
Line 168 says the key is “never sent to our servers”, but requests explicitly forward it in X-BYOK-Api-Key. This is a compliance/privacy messaging risk.
Suggested wording
- To use Perspective, select a free AI provider and paste your API key. Your key is stored only in your browser — never sent to our servers.
+ To use Perspective, select a free AI provider and paste your API key. Your key is stored in your browser and only forwarded with your analysis requests; it is not persisted server-side.📝 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.
| <div className="rounded-lg bg-blue-50 dark:bg-blue-950/40 border border-blue-200 dark:border-blue-800 p-3 text-sm text-blue-800 dark:text-blue-200"> | |
| To use Perspective, select a free AI provider and paste your API key. Your key is stored only in your browser — never sent to our servers. | |
| </div> | |
| <div className="rounded-lg bg-blue-50 dark:bg-blue-950/40 border border-blue-200 dark:border-blue-800 p-3 text-sm text-blue-800 dark:text-blue-200"> | |
| To use Perspective, select a free AI provider and paste your API key. Your key is stored in your browser and only forwarded with your analysis requests; it is not persisted server-side. | |
| </div> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/components/settings-modal.tsx` around lines 167 - 169, The privacy
copy inside the settings-modal component (the rounded info div in
frontend/components/settings-modal.tsx) is incorrect; update the text to
accurately state that the API key is stored in the browser but may be forwarded
to selected AI providers via requests (for example via the X-BYOK-Api-Key
header) rather than claiming it is “never sent to our servers”; locate the div
rendering the message and replace the sentence with a concise, factual line
noting local storage of the key and that requests to providers may include the
key in headers so users understand it can be transmitted.
What this PR does
This PR removes the dependency on a server-side
GROQ_API_KEYenvironment variableand replaces it with a BYOK (Bring Your Own Key) pattern — users supply their own
Groq API key through the frontend, which is sent securely with each request.
Screen.Recording.2026-02-26.224451.mp4
Why this is needed
Previously, the app required the developer/host to provide a single Groq API key
on the server. This meant:
With BYOK, each user uses their own free Groq key. The server never stores any key.
Changes
Backend
routes.py— All 3 endpoints (/bias,/process,/chat) now readX-BYOK-Api-KeyandX-BYOK-Modelfrom request headers. Returns 401if the key is missing.
pipeline.py—run_langgraph_workflow()acceptsapi_keyandgroq_modeland injects them into the LangGraph state.
langgraph_builder.py—MyStateTypedDict extended withgroq_api_keyandgroq_modelfields.sentiment,generate_perspective,judge) — removedmodule-level hardcoded Groq clients; now create a per-request client from
state["groq_api_key"].fact_check.py— Fact-check failures are now soft/non-fatal. Pipelinecontinues with empty facts instead of aborting the whole analysis.
chat/llm_processing.py— Chat endpoint now accepts the full article textas additional context alongside Pinecone RAG results.
Frontend
components/settings-modal.tsx(new file) — Settings dialog for users toenter their Groq API key and choose a model. Key stored in
localStorage.Auto-opens on first visit.
app/analyze/page.tsx— Added<SettingsModal />to the header.app/analyze/loading/page.tsx— Reads key/model from localStorage and attachesthem as
X-BYOK-Api-Key/X-BYOK-Modelheaders on API requests.app/analyze/results/page.tsx— Chat requests also include BYOK headers andsend full article text for richer answers.
Testing
Tested manually:
gsk_)Notes
BACKENDURL inloading/page.tsxandresults/page.tsxis currentlyhardcoded to
http://localhost:7860. A follow-up can move this toNEXT_PUBLIC_API_URLenv var for cleaner deployments.intentional for now as embedding is infrastructure, not user AI calls.
Summary by CodeRabbit
New Features
Bug Fixes
Chores