Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "LeetCode EasyRepeat",
"version": "1.0.6",
"version": "1.0.9",
"description": "Automatically capture LeetCode submissions and schedule spaced repetition reviews.",
"icons": {
"16": "src/assets/icons/icon16.png",
Expand All @@ -19,9 +19,15 @@
"host_permissions": [
"https://leetcode.com/*",
"https://leetcode.cn/*",
"https://api.deepseek.com/*",
"https://dashscope.aliyuncs.com/*",
"http://localhost/*",
"http://127.0.0.1/*"
],
"optional_host_permissions": [
"https://*/*",
"http://*/*"
],
"options_ui": {
"page": "dist/src/options/options.html",
"open_in_tab": true
Expand Down
41 changes: 41 additions & 0 deletions mcp-server/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,47 @@ def list_models(req: ModelsRequest):
except Exception as e:
return _provider_validation_error("OpenAI", e, final_key)

case "deepseek":
env_key = current_settings.deepseek_api_key.get_secret_value() if current_settings.deepseek_api_key else None
final_key = req.api_key or env_key
if not final_key:
return {"models": info.fallback_models, "source": "fallback"}
try:
import openai
client = openai.OpenAI(api_key=final_key, base_url=req.base_url or "https://api.deepseek.com")
models = [m.id for m in client.models.list().data]
return {"models": models or info.fallback_models, "source": "dynamic"}
except Exception as e:
return _provider_validation_error("DeepSeek", e, final_key)

case "qwen":
env_key = current_settings.qwen_api_key.get_secret_value() if current_settings.qwen_api_key else None
final_key = req.api_key or env_key
if not final_key:
return {"models": info.fallback_models, "source": "fallback"}
try:
import openai
client = openai.OpenAI(
api_key=final_key,
base_url=req.base_url or "https://dashscope.aliyuncs.com/compatible-mode/v1",
)
models = [m.id for m in client.models.list().data]
return {"models": models or info.fallback_models, "source": "dynamic"}
except Exception:
# DashScope does not guarantee OpenAI-compatible model discovery.
return {"models": info.fallback_models, "source": "fallback"}

case "custom":
if not req.api_key or not req.base_url:
return {"models": [], "source": "fallback", "warning": "API key and Base URL are required"}
try:
import openai
client = openai.OpenAI(api_key=req.api_key, base_url=req.base_url)
models = [m.id for m in client.models.list().data]
return {"models": models, "source": "dynamic"}
except Exception as e:
return _provider_validation_error("OpenAI-compatible provider", e, req.api_key)

case "anthropic":
env_key = current_settings.anthropic_api_key.get_secret_value() if current_settings.anthropic_api_key else None
final_key = req.api_key or env_key
Expand Down
2 changes: 2 additions & 0 deletions mcp-server/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ class Settings(BaseSettings):
# LLM Provider Keys
google_api_key: Optional[SecretStr] = Field(validation_alias="GOOGLE_API_KEY", default=None)
openai_api_key: Optional[SecretStr] = Field(validation_alias="OPENAI_API_KEY", default=None)
deepseek_api_key: Optional[SecretStr] = Field(validation_alias="DEEPSEEK_API_KEY", default=None)
qwen_api_key: Optional[SecretStr] = Field(validation_alias="DASHSCOPE_API_KEY", default=None)
anthropic_api_key: Optional[SecretStr] = Field(validation_alias="ANTHROPIC_API_KEY", default=None)

# LangSmith Tracing Keys
Expand Down
70 changes: 70 additions & 0 deletions mcp-server/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,30 @@ class ProviderInfo:
default_model="gpt-4o-mini",
fallback_models=["gpt-4o", "gpt-4o-mini", "o1-mini", "o3-mini"],
),
"deepseek": ProviderInfo(
name="deepseek",
display_name="DeepSeek",
requires_api_key=True,
default_model="deepseek-v4-flash",
fallback_models=["deepseek-v4-flash", "deepseek-v4-pro", "deepseek-chat", "deepseek-reasoner"],
),
"qwen": ProviderInfo(
name="qwen",
display_name="Qwen (DashScope)",
requires_api_key=True,
default_model="qwen3.7-plus",
fallback_models=[
"qwen3.8-max", "qwen3.7-plus", "qwen3.7-flash",
"qwen3-coder-plus", "qwen3-coder-flash", "qwen3-coder-next",
],
),
"custom": ProviderInfo(
name="custom",
display_name="OpenAI-Compatible",
requires_api_key=True,
default_model="",
fallback_models=[],
),
"anthropic": ProviderInfo(
name="anthropic",
display_name="Anthropic",
Expand Down Expand Up @@ -89,6 +113,52 @@ def get_llm(provider: str, model: str, api_key: str | None = None, base_url: str
temperature=0.4, max_retries=3,
)

case "deepseek":
from langchain_openai import ChatOpenAI

env_key = settings.deepseek_api_key.get_secret_value() if settings.deepseek_api_key else None
final_key = api_key or env_key
if not final_key:
raise ValueError("DeepSeek requires an API key. Check settings or .env.")

return ChatOpenAI(
model=model,
api_key=final_key,
base_url=base_url or "https://api.deepseek.com",
temperature=0.4,
max_retries=3,
)

case "qwen":
from langchain_openai import ChatOpenAI

env_key = settings.qwen_api_key.get_secret_value() if settings.qwen_api_key else None
final_key = api_key or env_key
if not final_key:
raise ValueError("Qwen/DashScope requires an API key.")
return ChatOpenAI(
model=model,
api_key=final_key,
base_url=base_url or "https://dashscope.aliyuncs.com/compatible-mode/v1",
temperature=0.4,
max_retries=3,
)

case "custom":
from langchain_openai import ChatOpenAI

if not api_key:
raise ValueError("The OpenAI-compatible provider requires an API key.")
if not base_url:
raise ValueError("The OpenAI-compatible provider requires a Base URL.")
return ChatOpenAI(
model=model,
api_key=api_key,
base_url=base_url,
temperature=0.4,
max_retries=3,
)

case "anthropic":
from langchain_anthropic import ChatAnthropic

Expand Down
2 changes: 2 additions & 0 deletions mcp-server/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@ jsonpointer==3.0.0
jsonschema==4.26.0
jsonschema-specifications==2025.9.1
langchain==1.2.12
langchain-anthropic==1.2.0
langchain-core==1.2.18
langchain-google-genai==4.2.1
langchain-ollama==1.0.1
langchain-openai==1.1.11
langgraph==1.1.2
langgraph-checkpoint==4.0.1
langgraph-prebuilt==1.0.8
Expand Down
2 changes: 1 addition & 1 deletion mcp-server/tests/test_models_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def test_models_endpoint_ollama_fallback(mock_get):

def test_models_endpoint_unknown_provider():
"""POST /models with unknown provider returns 400."""
response = client.post("/models", json={"provider": "deepseek"})
response = client.post("/models", json={"provider": "unknown"})
assert response.status_code == 400


Expand Down
33 changes: 22 additions & 11 deletions mcp-server/tests/test_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ def test_get_llm_unknown_provider_raises():
"""Unknown provider string → ValueError."""
from providers import get_llm
with pytest.raises(ValueError, match="Unsupported provider"):
get_llm("deepseek", "deepseek-r1")
get_llm("unknown", "some-model")


def test_get_llm_empty_provider_raises():
Expand Down Expand Up @@ -183,10 +183,12 @@ def test_get_llm_google_falls_back_to_env(mock_cls):
# F) PROVIDERS registry structure validation
# ---------------------------------------------------------------------------

def test_providers_registry_has_all_four():
"""PROVIDERS dict has exactly ollama, google, openai, anthropic."""
def test_providers_registry_has_all_supported_providers():
"""PROVIDERS contains every provider exposed by the extension."""
from providers import PROVIDERS
assert set(PROVIDERS.keys()) == {"ollama", "google", "openai", "anthropic"}
assert set(PROVIDERS.keys()) == {
"ollama", "google", "openai", "deepseek", "qwen", "custom", "anthropic"
}


def test_providers_ollama_requires_no_key():
Expand All @@ -198,22 +200,28 @@ def test_providers_ollama_requires_no_key():
def test_providers_cloud_require_keys():
"""All cloud providers require API keys."""
from providers import PROVIDERS
for name in ("google", "openai", "anthropic"):
for name in ("google", "openai", "deepseek", "qwen", "custom", "anthropic"):
assert PROVIDERS[name].requires_api_key is True, f"{name} should require API key"


def test_providers_have_fallback_models():
"""Every provider has a non-empty fallback_models list."""
"""Built-in providers have fallbacks; custom providers require an explicit model ID."""
from providers import PROVIDERS
for name, info in PROVIDERS.items():
assert len(info.fallback_models) > 0, f"{name} missing fallback_models"
if name == "custom":
assert info.fallback_models == []
else:
assert len(info.fallback_models) > 0, f"{name} missing fallback_models"


def test_providers_have_default_model():
"""Every provider has a non-empty default_model."""
"""Built-in providers have defaults; custom providers require an explicit model ID."""
from providers import PROVIDERS
for name, info in PROVIDERS.items():
assert info.default_model, f"{name} missing default_model"
if name == "custom":
assert info.default_model == ""
else:
assert info.default_model, f"{name} missing default_model"


# ---------------------------------------------------------------------------
Expand All @@ -235,6 +243,9 @@ def test_get_providers_endpoint():
assert "ollama" in names
assert "google" in names
assert "openai" in names
assert "deepseek" in names
assert "qwen" in names
assert "custom" in names
assert "anthropic" in names

# Each provider has expected fields
Expand All @@ -255,7 +266,7 @@ def test_post_models_unknown_provider_returns_400():
from api import app
client = TestClient(app)

response = client.post("/models", json={"provider": "deepseek"})
response = client.post("/models", json={"provider": "unknown"})
assert response.status_code == 400


Expand Down Expand Up @@ -347,6 +358,6 @@ def test_autofix_unknown_provider_returns_400():
response = client.post("/autofix", json={
"code": "def foo(): pass",
"test_input": "1",
"provider": "deepseek"
"provider": "unknown"
})
assert response.status_code == 400
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "leetcode-srs-extension",
"version": "1.0.6",
"version": "1.0.9",
"description": "LeetCode SRS Extension",
"scripts": {
"test": "jest --testPathIgnorePatterns=browser_e2e",
Expand Down
83 changes: 83 additions & 0 deletions src/background/deepseek_client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/** DeepSeek API client using its OpenAI-compatible chat endpoint. */
(function (root, factory) {
const exports = factory();
if (typeof self !== 'undefined') self.DeepSeekClient = exports;
if (typeof module === 'object' && module.exports) module.exports = exports;
}(typeof self !== 'undefined' ? self : this, function () {
const API_URL = 'https://api.deepseek.com/chat/completions';
const DEFAULT_MODEL = 'deepseek-chat';

async function getApiKey() {
if (typeof chrome === 'undefined' || !chrome.storage) return null;
const result = await chrome.storage.local.get(['keys']);
return result.keys?.deepseek || null;
}

async function getModelId() {
if (typeof chrome === 'undefined' || !chrome.storage) return DEFAULT_MODEL;
const result = await chrome.storage.local.get(['selectedModelId', 'cloudProvider']);
return result.cloudProvider === 'deepseek' && String(result.selectedModelId || '').trim()
? result.selectedModelId.trim() : DEFAULT_MODEL;
}

function extractJSON(text) {
if (!text) return null;
let value = text.trim();
const fenced = value.match(/```(?:json)?\s*([\s\S]*?)```/);
if (fenced) value = fenced[1].trim();
const start = value.indexOf('{');
const end = value.lastIndexOf('}');
if (start !== -1 && end > start) value = value.slice(start, end + 1);
try { return JSON.parse(value); } catch (_) { return null; }
}

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

async function analyzeSubmissions(prompt, options = {}) {
const apiKey = await getApiKey();
const model = await getModelId();
if (!apiKey) return { error: 'No DeepSeek API key configured' };

const body = {
model,
messages: [
{ role: 'system', content: 'You are a helpful coding assistant. Output JSON when requested.' },
{ role: 'user', content: prompt }
],
temperature: 0.2,
stream: false
};
if (prompt.toLowerCase().includes('json')) body.response_format = { type: 'json_object' };

let lastError = 'Unknown error';
const maxRetries = options.maxRetries || 3;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
body: JSON.stringify(body)
});
if (!response.ok) {
const detail = await response.text();
lastError = `HTTP ${response.status}: ${detail || response.statusText}`;
if (response.status >= 500 && attempt + 1 < maxRetries) {
await sleep(1000 * Math.pow(2, attempt));
continue;
}
return { error: lastError };
}
const data = await response.json();
const content = data.choices?.[0]?.message?.content;
if (!content) return { error: 'Empty response from DeepSeek' };
return extractJSON(content) || { text: content };
} catch (error) {
lastError = error.message;
if (attempt + 1 < maxRetries) await sleep(1000 * Math.pow(2, attempt));
}
}
return { error: lastError };
}

return { analyzeSubmissions, generateContent: analyzeSubmissions, getApiKey };
}));
Loading