From 60a1b4537efdc624dc139f48d7952828aceb9296 Mon Sep 17 00:00:00 2001 From: Tanvir Farhad Date: Fri, 29 May 2026 01:10:58 +0100 Subject: [PATCH 1/4] feat: initialise api/services package --- api/services/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 api/services/__init__.py diff --git a/api/services/__init__.py b/api/services/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/api/services/__init__.py @@ -0,0 +1 @@ + From 0d9632f794d4c8723c6e60e506f4630d368c7101 Mon Sep 17 00:00:00 2001 From: Tanvir Farhad Date: Fri, 29 May 2026 01:14:29 +0100 Subject: [PATCH 2/4] feat: add AI provider abstraction layer for Anthropic, Groq and Gemini --- api/services/ai_provider.py | 103 ++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 api/services/ai_provider.py diff --git a/api/services/ai_provider.py b/api/services/ai_provider.py new file mode 100644 index 0000000..f7b7f37 --- /dev/null +++ b/api/services/ai_provider.py @@ -0,0 +1,103 @@ +import logging +import requests + +logger = logging.getLogger(__name__) + +PROVIDERS = ("anthropic", "groq", "gemini") + +ANTHROPIC_MODEL = "claude-3-5-haiku-20241022" +GROQ_MODEL = "llama-3.1-8b-instant" +GEMINI_MODEL = "gemini-1.5-flash" + + +def get_completion(provider: str, api_key: str, prompt: str) -> str: + provider = provider.lower().strip() + if provider not in PROVIDERS: + raise ValueError( + f"Unsupported provider '{provider}'. Choose from: {', '.join(PROVIDERS)}" + ) + if not api_key or not api_key.strip(): + raise ValueError("api_key is required and cannot be empty") + if provider == "anthropic": + return _anthropic(api_key, prompt) + if provider == "groq": + return _groq(api_key, prompt) + return _gemini(api_key, prompt) + + +def _anthropic(api_key: str, prompt: str) -> str: + try: + resp = requests.post( + "https://api.anthropic.com/v1/messages", + headers={ + "x-api-key": api_key, + "anthropic-version": "2023-06-01", + "content-type": "application/json", + }, + json={ + "model": ANTHROPIC_MODEL, + "max_tokens": 1024, + "messages": [{"role": "user", "content": prompt}], + }, + timeout=30, + ) + if resp.status_code == 401: + raise ValueError("Invalid Anthropic API key") + if resp.status_code == 429: + raise RuntimeError("Anthropic rate limit reached, try again later") + resp.raise_for_status() + return resp.json()["content"][0]["text"] + except (ValueError, RuntimeError): + raise + except requests.exceptions.RequestException as exc: + logger.error("Anthropic request failed: %s", exc) + raise RuntimeError(f"Anthropic request failed: {exc}") from exc + + +def _groq(api_key: str, prompt: str) -> str: + try: + resp = requests.post( + "https://api.groq.com/openai/v1/chat/completions", + headers={ + "Authorization": f"Bearer {api_key}", + "content-type": "application/json", + }, + json={ + "model": GROQ_MODEL, + "messages": [{"role": "user", "content": prompt}], + "max_tokens": 1024, + }, + timeout=30, + ) + if resp.status_code == 401: + raise ValueError("Invalid Groq API key") + if resp.status_code == 429: + raise RuntimeError("Groq rate limit reached, try again later") + resp.raise_for_status() + return resp.json()["choices"][0]["message"]["content"] + except (ValueError, RuntimeError): + raise + except requests.exceptions.RequestException as exc: + logger.error("Groq request failed: %s", exc) + raise RuntimeError(f"Groq request failed: {exc}") from exc + + +def _gemini(api_key: str, prompt: str) -> str: + try: + resp = requests.post( + f"https://generativelanguage.googleapis.com/v1beta/models/{GEMINI_MODEL}:generateContent", + params={"key": api_key}, + json={"contents": [{"parts": [{"text": prompt}]}]}, + timeout=30, + ) + if resp.status_code == 400 and "API_KEY_INVALID" in resp.text: + raise ValueError("Invalid Gemini API key") + if resp.status_code == 429: + raise RuntimeError("Gemini rate limit reached, try again later") + resp.raise_for_status() + return resp.json()["candidates"][0]["content"]["parts"][0]["text"] + except (ValueError, RuntimeError): + raise + except requests.exceptions.RequestException as exc: + logger.error("Gemini request failed: %s", exc) + raise RuntimeError(f"Gemini request failed: {exc}") from exc From e425cd91ec2c89a4703a9980870fa3f6a8411baa Mon Sep 17 00:00:00 2001 From: Tanvir Farhad Date: Fri, 29 May 2026 11:53:50 +0100 Subject: [PATCH 3/4] fix: add module docstring to ai_provider.py Added a docstring explaining the purpose of the AI provider abstraction layer. --- api/services/ai_provider.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/services/ai_provider.py b/api/services/ai_provider.py index f7b7f37..4894eca 100644 --- a/api/services/ai_provider.py +++ b/api/services/ai_provider.py @@ -1,3 +1,5 @@ +"""AI provider abstraction layer supporting Anthropic, Groq and Gemini.""" + import logging import requests From 876cbac6af8ba9cd091ced75867db266f84eb6b1 Mon Sep 17 00:00:00 2001 From: Tanvir Farhad Date: Fri, 29 May 2026 12:15:32 +0100 Subject: [PATCH 4/4] fix: make model configurable with sensible defaults per provider --- api/services/ai_provider.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/api/services/ai_provider.py b/api/services/ai_provider.py index 4894eca..70d2380 100644 --- a/api/services/ai_provider.py +++ b/api/services/ai_provider.py @@ -7,12 +7,16 @@ PROVIDERS = ("anthropic", "groq", "gemini") -ANTHROPIC_MODEL = "claude-3-5-haiku-20241022" -GROQ_MODEL = "llama-3.1-8b-instant" -GEMINI_MODEL = "gemini-1.5-flash" +DEFAULT_MODELS = { + "anthropic": "claude-3-5-haiku-20241022", + "groq": "llama-3.1-8b-instant", + "gemini": "gemini-1.5-flash", +} -def get_completion(provider: str, api_key: str, prompt: str) -> str: +def get_completion( + provider: str, api_key: str, prompt: str, model: str = None +) -> str: provider = provider.lower().strip() if provider not in PROVIDERS: raise ValueError( @@ -20,14 +24,17 @@ def get_completion(provider: str, api_key: str, prompt: str) -> str: ) if not api_key or not api_key.strip(): raise ValueError("api_key is required and cannot be empty") + + resolved_model = model or DEFAULT_MODELS[provider] + if provider == "anthropic": - return _anthropic(api_key, prompt) + return _anthropic(api_key, prompt, resolved_model) if provider == "groq": - return _groq(api_key, prompt) - return _gemini(api_key, prompt) + return _groq(api_key, prompt, resolved_model) + return _gemini(api_key, prompt, resolved_model) -def _anthropic(api_key: str, prompt: str) -> str: +def _anthropic(api_key: str, prompt: str, model: str) -> str: try: resp = requests.post( "https://api.anthropic.com/v1/messages", @@ -37,7 +44,7 @@ def _anthropic(api_key: str, prompt: str) -> str: "content-type": "application/json", }, json={ - "model": ANTHROPIC_MODEL, + "model": model, "max_tokens": 1024, "messages": [{"role": "user", "content": prompt}], }, @@ -56,7 +63,7 @@ def _anthropic(api_key: str, prompt: str) -> str: raise RuntimeError(f"Anthropic request failed: {exc}") from exc -def _groq(api_key: str, prompt: str) -> str: +def _groq(api_key: str, prompt: str, model: str) -> str: try: resp = requests.post( "https://api.groq.com/openai/v1/chat/completions", @@ -65,7 +72,7 @@ def _groq(api_key: str, prompt: str) -> str: "content-type": "application/json", }, json={ - "model": GROQ_MODEL, + "model": model, "messages": [{"role": "user", "content": prompt}], "max_tokens": 1024, }, @@ -84,10 +91,10 @@ def _groq(api_key: str, prompt: str) -> str: raise RuntimeError(f"Groq request failed: {exc}") from exc -def _gemini(api_key: str, prompt: str) -> str: +def _gemini(api_key: str, prompt: str, model: str) -> str: try: resp = requests.post( - f"https://generativelanguage.googleapis.com/v1beta/models/{GEMINI_MODEL}:generateContent", + f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent", params={"key": api_key}, json={"contents": [{"parts": [{"text": prompt}]}]}, timeout=30,