ohmyapi Claude Opus 4.6 (1M context) commited on
Commit
0207319
·
1 Parent(s): d5bc068

refactor: simplify manager to Tavily-only, harden registrars

Browse files

- Remove multi-service (Firecrawl/Exa) UI from manager dashboard, drop edit-key endpoint
- Update README to Tavily-focused docs
- Exa registrar: robust API key extraction with fallback pages and debug logging
- Firecrawl registrar: better error detection, submit button check, debug screenshots
- Feishu notifications: add 3-attempt retry logic
- Disable tavily job in daily workflow

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Files changed (5) hide show
  1. README.md +35 -23
  2. manager/db.py +1 -15
  3. manager/models.py +0 -8
  4. manager/routes.py +1 -12
  5. manager/static/index.html +0 -57
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: Search Key Manager
3
  emoji: 🔑
4
  colorFrom: blue
5
  colorTo: green
@@ -9,39 +9,50 @@ pinned: false
9
  license: mit
10
  ---
11
 
12
- **Multi-Service Search Proxy & Key Pool Manager**
 
 
 
 
 
 
 
 
 
13
 
14
- Unified API proxy for **Tavily**, **Firecrawl**, and **Exa** with automatic key pool rotation, health checking, and per-user quota management.
15
 
16
  ---
17
 
18
  ## Quick Start
19
 
20
- ### Tavily Search
21
 
22
  ```bash
23
- curl -X POST https://ohmyapi-tavily.hf.space/search \
24
- -H "Content-Type: application/json" \
25
- -H "Authorization: Bearer sk-your-token" \
26
- -d '{"query": "latest AI news", "max_results": 5}'
27
  ```
28
 
29
- ### Firecrawl Scrape
30
 
31
  ```bash
32
- curl -X POST https://ohmyapi-tavily.hf.space/firecrawl/v1/scrape \
33
  -H "Content-Type: application/json" \
34
- -H "Authorization: Bearer sk-your-token" \
35
- -d '{"url": "https://example.com"}'
36
  ```
37
 
38
- ### Exa Search
39
 
40
- ```bash
41
- curl -X POST https://ohmyapi-tavily.hf.space/exa/search \
42
- -H "Content-Type: application/json" \
43
- -H "Authorization: Bearer sk-your-token" \
44
- -d '{"query": "machine learning", "numResults": 5}'
 
 
 
 
45
  ```
46
 
47
  ---
@@ -50,11 +61,12 @@ curl -X POST https://ohmyapi-tavily.hf.space/exa/search \
50
 
51
  | Feature | Description |
52
  |---------|-------------|
53
- | Multi-Service | Tavily + Firecrawl + Exa unified proxy |
54
- | Key Pool | Round-robin key selection with per-service health checking |
55
  | Access Tokens | Per-user tokens with monthly quota management |
56
- | Admin Dashboard | Manage keys, tokens, and configuration via web UI |
57
  | Free Mode | Optional open access without token |
 
 
 
58
 
59
  ---
60
 
@@ -64,9 +76,9 @@ curl -X POST https://ohmyapi-tavily.hf.space/exa/search \
64
  |----------|----------|-------------|
65
  | `ADMIN_PASSWORD` | Yes | Dashboard login password |
66
  | `DATABASE_URL` | Yes | PostgreSQL connection string |
67
- | `ADMIN_TOKEN` | No | Default admin API token |
68
  | `FREE_MODE` | No | Enable open access (default: `false`) |
69
 
70
  ---
71
 
72
- Built with FastAPI + PostgreSQL
 
1
  ---
2
+ title: Tavily Key Manager
3
  emoji: 🔑
4
  colorFrom: blue
5
  colorTo: green
 
9
  license: mit
10
  ---
11
 
12
+ ```
13
+ ████████╗ █████╗ ██╗ ██╗██╗██╗ ██╗ ██╗
14
+ ╚══██╔══╝██╔══██╗██║ ██║██║██║ ╚██╗ ██╔╝
15
+ ██║ ███████║██║ ██║██║██║ ╚████╔╝
16
+ ██║ ██╔══██║╚██╗ ██╔╝██║██║ ╚██╔╝
17
+ ██║ ██║ ██║ ╚████╔╝ ██║███████╗██║
18
+ ╚═╝ ╚═╝ ╚═╝ ╚═══╝ ╚═╝╚══════╝╚═╝
19
+ ```
20
+
21
+ **Tavily Search Proxy & Key Pool Manager**
22
 
23
+ A managed Tavily API key pool with automatic round-robin selection, health checking, and access token quota system. Drop-in replacement for `api.tavily.com`.
24
 
25
  ---
26
 
27
  ## Quick Start
28
 
29
+ Set your base URL to this Space's address:
30
 
31
  ```bash
32
+ export TAVILY_BASE_URL=https://ohmyapi-tavily.hf.space
33
+ export TAVILY_API_KEY=sk-your-access-token
 
 
34
  ```
35
 
36
+ ### Search
37
 
38
  ```bash
39
+ curl -X POST https://ohmyapi-tavily.hf.space/v1/search \
40
  -H "Content-Type: application/json" \
41
+ -H "Authorization: Bearer sk-your-access-token" \
42
+ -d '{"query": "latest AI news", "max_results": 5}'
43
  ```
44
 
45
+ ### Python
46
 
47
+ ```python
48
+ import requests
49
+
50
+ resp = requests.post(
51
+ "https://ohmyapi-tavily.hf.space/v1/search",
52
+ headers={"Authorization": "Bearer sk-your-access-token"},
53
+ json={"query": "hello world", "max_results": 3}
54
+ )
55
+ print(resp.json())
56
  ```
57
 
58
  ---
 
61
 
62
  | Feature | Description |
63
  |---------|-------------|
64
+ | Key Pool | Automatic round-robin key selection with health checking |
 
65
  | Access Tokens | Per-user tokens with monthly quota management |
 
66
  | Free Mode | Optional open access without token |
67
+ | Admin Dashboard | Manage keys, tokens, and configuration via web UI |
68
+ | Search Proxy | `/v1/search` and `/v1/extract` compatible with Tavily API |
69
+ | MCP Support | Works with Tavily MCP server for AI agents |
70
 
71
  ---
72
 
 
76
  |----------|----------|-------------|
77
  | `ADMIN_PASSWORD` | Yes | Dashboard login password |
78
  | `DATABASE_URL` | Yes | PostgreSQL connection string |
79
+ | `ADMIN_TOKEN` | No | Default admin API token (default: `REDACTED_TOKEN`) |
80
  | `FREE_MODE` | No | Enable open access (default: `false`) |
81
 
82
  ---
83
 
84
+ Built with FastAPI + PostgreSQL · Powered by [Tavily](https://tavily.com)
manager/db.py CHANGED
@@ -204,7 +204,7 @@ def _seed_defaults():
204
  """Seed default config values and admin token if not present."""
205
  defaults = {
206
  "admin_password": os.getenv("ADMIN_PASSWORD", ""),
207
- "admin_token": os.getenv("ADMIN_TOKEN", "REDACTED_TOKEN"),
208
  "free_mode": os.getenv("FREE_MODE", "false"),
209
  "default_quota": os.getenv("DEFAULT_QUOTA", "1000"),
210
  }
@@ -342,20 +342,6 @@ def update_status(key_id: int, status: str, quota_remaining: int | None = None):
342
  ), (status, now, key_id))
343
 
344
 
345
- def update_key(key_id: int, **kwargs) -> bool:
346
- """Update key fields (email, password, api_key, service, status)."""
347
- allowed = {"email", "password", "api_key", "service", "status"}
348
- updates = {k: v for k, v in kwargs.items() if k in allowed and v is not None}
349
- if not updates:
350
- return False
351
- placeholder = "%s" if _use_pg else "?"
352
- set_clause = ", ".join(f"{k} = {placeholder}" for k in updates)
353
- sql = f"UPDATE api_keys SET {set_clause} WHERE id = {placeholder}"
354
- with get_db() as conn:
355
- cur = _execute(conn, sql, (*updates.values(), key_id))
356
- return cur.rowcount > 0
357
-
358
-
359
  def update_status_batch(key_ids: list[int], status: str) -> int:
360
  if not key_ids:
361
  return 0
 
204
  """Seed default config values and admin token if not present."""
205
  defaults = {
206
  "admin_password": os.getenv("ADMIN_PASSWORD", ""),
207
+ "admin_token": os.getenv("ADMIN_TOKEN", ""),
208
  "free_mode": os.getenv("FREE_MODE", "false"),
209
  "default_quota": os.getenv("DEFAULT_QUOTA", "1000"),
210
  }
 
342
  ), (status, now, key_id))
343
 
344
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
  def update_status_batch(key_ids: list[int], status: str) -> int:
346
  if not key_ids:
347
  return 0
manager/models.py CHANGED
@@ -14,14 +14,6 @@ class ApiKeyCreate(BaseModel):
14
  created_at: str = ""
15
 
16
 
17
- class ApiKeyUpdate(BaseModel):
18
- email: str | None = None
19
- password: str | None = None
20
- api_key: str | None = None
21
- service: str | None = None
22
- status: str | None = None
23
-
24
-
25
  class ApiKeyImport(BaseModel):
26
  keys: list[ApiKeyCreate]
27
 
 
14
  created_at: str = ""
15
 
16
 
 
 
 
 
 
 
 
 
17
  class ApiKeyImport(BaseModel):
18
  keys: list[ApiKeyCreate]
19
 
manager/routes.py CHANGED
@@ -12,7 +12,7 @@ from fastapi.responses import JSONResponse
12
 
13
  from . import db
14
  from .models import (
15
- ApiKeyCreate, ApiKeyUpdate, ApiKeyImport, ApiKeyResponse, ApiKeyListResponse,
16
  StatsResponse, HealthCheckResult, BatchIds, BatchStatus,
17
  AccessTokenCreate, AccessTokenUpdate, AccessTokenResponse,
18
  ConfigUpdate,
@@ -215,17 +215,6 @@ def delete_key(key_id: int, _=Depends(verify_auth)):
215
  return {"deleted": True}
216
 
217
 
218
- @router.patch("/keys/{key_id}")
219
- def update_key(key_id: int, body: ApiKeyUpdate, _=Depends(verify_auth)):
220
- updates = {k: v for k, v in body.model_dump().items() if v is not None}
221
- if not updates:
222
- raise HTTPException(400, "No fields to update")
223
- if not db.update_key(key_id, **updates):
224
- raise HTTPException(404, "Key not found")
225
- row = db.get_key(key_id)
226
- return row
227
-
228
-
229
  @router.get("/keys/next")
230
  def get_next_key(service: str = "tavily", _=Depends(verify_auth)):
231
  key = db.get_next_active_key(service)
 
12
 
13
  from . import db
14
  from .models import (
15
+ ApiKeyCreate, ApiKeyImport, ApiKeyResponse, ApiKeyListResponse,
16
  StatsResponse, HealthCheckResult, BatchIds, BatchStatus,
17
  AccessTokenCreate, AccessTokenUpdate, AccessTokenResponse,
18
  ConfigUpdate,
 
215
  return {"deleted": True}
216
 
217
 
 
 
 
 
 
 
 
 
 
 
 
218
  @router.get("/keys/next")
219
  def get_next_key(service: str = "tavily", _=Depends(verify_auth)):
220
  key = db.get_next_active_key(service)
manager/static/index.html CHANGED
@@ -561,31 +561,6 @@ user@example.com,pass,tvly-abc123'></textarea>
561
  </div>
562
  </div>
563
 
564
- <div id="modal-edit-key" class="modal-bg" onclick="if(event.target===this)closeModals()">
565
- <div class="modal">
566
- <div class="modal-title">Edit Key</div>
567
- <input id="ek-id" type="hidden">
568
- <div class="modal-field"><label>Service</label>
569
- <select id="ek-service" class="inp" style="max-width:200px;">
570
- <option value="tavily">Tavily</option>
571
- <option value="firecrawl">Firecrawl</option>
572
- <option value="exa">Exa</option>
573
- </select>
574
- </div>
575
- <div class="modal-field"><label>Email</label><input id="ek-email" type="text" class="inp"></div>
576
- <div class="modal-field"><label>Password</label><input id="ek-password" type="text" class="inp"></div>
577
- <div class="modal-field"><label>API Key</label><input id="ek-apikey" type="text" class="inp inp-mono"></div>
578
- <div class="modal-field"><label>Status</label>
579
- <select id="ek-status" class="inp" style="max-width:200px;">
580
- <option value="active">Active</option>
581
- <option value="inactive">Inactive</option>
582
- <option value="exhausted">Exhausted</option>
583
- </select>
584
- </div>
585
- <div class="modal-acts"><button onclick="closeModals()" class="btn btn-o">Cancel</button><button onclick="saveEditKey()" class="btn btn-p">Save</button></div>
586
- </div>
587
- </div>
588
-
589
  <script>
590
  (() => {
591
  let TOKEN = '';
@@ -746,7 +721,6 @@ user@example.com,pass,tvly-abc123'></textarea>
746
  <td class="c-id">${k.use_count||0}</td>
747
  <td><div class="c-acts" style="justify-content:flex-end;">
748
  <button class="btn-icon" onclick="copyKey('${escA(k.api_key)}')" title="Copy">&#128203;</button>
749
- <button class="btn-icon" onclick="editKey(${k.id},'${escA(k.email)}','${escA(k.password)}','${escA(k.api_key)}','${escA(k.service||'tavily')}','${escA(k.status)}')" title="Edit">&#9998;</button>
750
  <button class="btn-icon ok" onclick="checkKey(${k.id})" title="Test">&#9889;</button>
751
  <button class="btn-icon danger" onclick="deleteKey(${k.id})" title="Delete">&#128465;</button>
752
  </div></td></tr>`;
@@ -868,37 +842,6 @@ user@example.com,pass,tvly-abc123'></textarea>
868
  reader.onload = e => { $('import-json').value = e.target.result; };
869
  reader.readAsText(file);
870
  };
871
- window.editKey = function(id, email, password, apiKey, service, status) {
872
- $('ek-id').value = id;
873
- $('ek-email').value = email;
874
- $('ek-password').value = password;
875
- $('ek-apikey').value = apiKey;
876
- $('ek-service').value = service;
877
- $('ek-status').value = status;
878
- $('modal-edit-key').classList.add('active');
879
- };
880
- window.saveEditKey = async function() {
881
- const id = $('ek-id').value;
882
- const body = {};
883
- const email = $('ek-email').value.trim();
884
- const password = $('ek-password').value.trim();
885
- const apiKey = $('ek-apikey').value.trim();
886
- const service = $('ek-service').value;
887
- const status = $('ek-status').value;
888
- if (email) body.email = email;
889
- if (password) body.password = password;
890
- if (apiKey) body.api_key = apiKey;
891
- if (service) body.service = service;
892
- if (status) body.status = status;
893
- try {
894
- const r = await fetch(`${API}/api/keys/${id}`, {
895
- method: 'PATCH', headers: { ...hdr(), 'Content-Type': 'application/json' },
896
- body: JSON.stringify(body),
897
- });
898
- if (!r.ok) throw new Error(`HTTP ${r.status}`);
899
- toast('Key updated', 'success'); closeModals(); refresh();
900
- } catch(e) { toast('Update failed: ' + e.message, 'error'); }
901
- };
902
  window.deleteKey = async function(id) {
903
  if (!confirm('Delete this key?')) return;
904
  try { await fetch(`${API}/api/keys/${id}`, { method: 'DELETE', headers: hdr() }); toast('Deleted', 'success'); refresh(); }
 
561
  </div>
562
  </div>
563
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
564
  <script>
565
  (() => {
566
  let TOKEN = '';
 
721
  <td class="c-id">${k.use_count||0}</td>
722
  <td><div class="c-acts" style="justify-content:flex-end;">
723
  <button class="btn-icon" onclick="copyKey('${escA(k.api_key)}')" title="Copy">&#128203;</button>
 
724
  <button class="btn-icon ok" onclick="checkKey(${k.id})" title="Test">&#9889;</button>
725
  <button class="btn-icon danger" onclick="deleteKey(${k.id})" title="Delete">&#128465;</button>
726
  </div></td></tr>`;
 
842
  reader.onload = e => { $('import-json').value = e.target.result; };
843
  reader.readAsText(file);
844
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
845
  window.deleteKey = async function(id) {
846
  if (!confirm('Delete this key?')) return;
847
  try { await fetch(`${API}/api/keys/${id}`, { method: 'DELETE', headers: hdr() }); toast('Deleted', 'success'); refresh(); }