khoj 1.37.0__py3-none-any.whl → 1.37.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. khoj/database/migrations/0087_alter_aimodelapi_api_key.py +17 -0
  2. khoj/database/models/__init__.py +1 -1
  3. khoj/interface/compiled/404/index.html +2 -2
  4. khoj/interface/compiled/_next/static/chunks/{2117-ce1f0a4598f5e4fe.js → 2117-1c18aa2098982bf9.js} +1 -1
  5. khoj/interface/compiled/_next/static/chunks/{2327-b21ecded25471e6c.js → 2327-0bbe3ee35f80659f.js} +1 -1
  6. khoj/interface/compiled/_next/static/chunks/{8515-010dd769c584b672.js → 8515-f305779d95dd5780.js} +1 -1
  7. khoj/interface/compiled/_next/static/chunks/app/agents/{layout-5961c3717e1d8813.js → layout-dd7f2b45a9c30bd7.js} +1 -1
  8. khoj/interface/compiled/_next/static/chunks/app/agents/{page-0d31f76257d6ec11.js → page-df5446aa4fb82e1a.js} +1 -1
  9. khoj/interface/compiled/_next/static/chunks/app/automations/{page-c6180af69fc9c766.js → page-0a44416f9183aec0.js} +1 -1
  10. khoj/interface/compiled/_next/static/chunks/app/chat/{layout-3e25af7224d678a0.js → layout-904fbbb3974588da.js} +1 -1
  11. khoj/interface/compiled/_next/static/chunks/app/chat/{page-e3f298848d59803b.js → page-5175e747d3cb4a33.js} +1 -1
  12. khoj/interface/compiled/_next/static/chunks/app/{page-df51e61295c4a9b5.js → page-44ac22beb2619af0.js} +1 -1
  13. khoj/interface/compiled/_next/static/chunks/app/search/{layout-9ccd090dcc2aa58a.js → layout-51d73830842461d5.js} +1 -1
  14. khoj/interface/compiled/_next/static/chunks/app/search/{page-098017fa7f6ba0bf.js → page-1df7b236b30620f7.js} +1 -1
  15. khoj/interface/compiled/_next/static/chunks/app/settings/{page-1ff027f4e0a5c468.js → page-3473bab693ef81b2.js} +1 -1
  16. khoj/interface/compiled/_next/static/chunks/app/share/chat/{layout-9cc742afcea0b421.js → layout-d090bd23befd0207.js} +1 -1
  17. khoj/interface/compiled/_next/static/chunks/app/share/chat/{page-ea29a4633737cb59.js → page-e8f0cc65930b214e.js} +1 -1
  18. khoj/interface/compiled/_next/static/chunks/main-876327ac335776ab.js +1 -0
  19. khoj/interface/compiled/_next/static/chunks/{webpack-2707657083ef5456.js → webpack-d1d79c1576702da7.js} +1 -1
  20. khoj/interface/compiled/_next/static/css/37a73b87f02df402.css +1 -0
  21. khoj/interface/compiled/_next/static/css/440ae0f0f650dc35.css +1 -0
  22. khoj/interface/compiled/_next/static/css/b62829e3bf683b86.css +1 -0
  23. khoj/interface/compiled/_next/static/css/f29752d6e1be7624.css +1 -0
  24. khoj/interface/compiled/agents/index.html +2 -2
  25. khoj/interface/compiled/agents/index.txt +3 -3
  26. khoj/interface/compiled/automations/index.html +2 -2
  27. khoj/interface/compiled/automations/index.txt +3 -3
  28. khoj/interface/compiled/chat/index.html +2 -2
  29. khoj/interface/compiled/chat/index.txt +3 -3
  30. khoj/interface/compiled/index.html +2 -2
  31. khoj/interface/compiled/index.txt +2 -2
  32. khoj/interface/compiled/search/index.html +2 -2
  33. khoj/interface/compiled/search/index.txt +3 -3
  34. khoj/interface/compiled/settings/index.html +2 -2
  35. khoj/interface/compiled/settings/index.txt +5 -5
  36. khoj/interface/compiled/share/chat/index.html +2 -2
  37. khoj/interface/compiled/share/chat/index.txt +3 -3
  38. khoj/processor/conversation/anthropic/anthropic_chat.py +9 -4
  39. khoj/processor/conversation/anthropic/utils.py +26 -11
  40. khoj/processor/conversation/google/gemini_chat.py +12 -5
  41. khoj/processor/conversation/google/utils.py +41 -5
  42. khoj/processor/conversation/openai/gpt.py +1 -5
  43. khoj/processor/conversation/openai/utils.py +6 -5
  44. khoj/routers/api.py +4 -0
  45. khoj/routers/auth.py +2 -5
  46. khoj/routers/helpers.py +13 -3
  47. khoj/utils/constants.py +2 -0
  48. khoj/utils/helpers.py +58 -2
  49. {khoj-1.37.0.dist-info → khoj-1.37.1.dist-info}/METADATA +5 -6
  50. {khoj-1.37.0.dist-info → khoj-1.37.1.dist-info}/RECORD +60 -59
  51. khoj/interface/compiled/_next/static/chunks/main-98eb5932d6b2e3fa.js +0 -1
  52. khoj/interface/compiled/_next/static/css/0db53bacf81896f5.css +0 -1
  53. khoj/interface/compiled/_next/static/css/b15666ef52060cd0.css +0 -1
  54. khoj/interface/compiled/_next/static/css/bb7ea98028b368f3.css +0 -1
  55. khoj/interface/compiled/_next/static/css/ee66643a6a5bf71c.css +0 -1
  56. /khoj/interface/compiled/_next/static/{4luVb2EbBJ977WDHeDuZQ → PzXuumAYUnzr_Egd_JDmj}/_buildManifest.js +0 -0
  57. /khoj/interface/compiled/_next/static/{4luVb2EbBJ977WDHeDuZQ → PzXuumAYUnzr_Egd_JDmj}/_ssgManifest.js +0 -0
  58. /khoj/interface/compiled/_next/static/chunks/{1915-1943ee8a628b893c.js → 1915-ab4353eaca76f690.js} +0 -0
  59. /khoj/interface/compiled/_next/static/chunks/{4363-e6ac2203564d1a3b.js → 4363-4efaf12abe696251.js} +0 -0
  60. /khoj/interface/compiled/_next/static/chunks/{4447-e038b251d626c340.js → 4447-5d44807c40355b1a.js} +0 -0
  61. /khoj/interface/compiled/_next/static/chunks/{8667-8136f74e9a086fca.js → 8667-adbe6017a66cef10.js} +0 -0
  62. /khoj/interface/compiled/_next/static/chunks/{9259-640fdd77408475df.js → 9259-d8bcd9da9e80c81e.js} +0 -0
  63. {khoj-1.37.0.dist-info → khoj-1.37.1.dist-info}/WHEEL +0 -0
  64. {khoj-1.37.0.dist-info → khoj-1.37.1.dist-info}/entry_points.txt +0 -0
  65. {khoj-1.37.0.dist-info → khoj-1.37.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,9 @@
1
1
  import logging
2
+ import os
2
3
  import random
3
4
  from copy import deepcopy
4
5
  from threading import Thread
6
+ from typing import Dict
5
7
 
6
8
  from google import genai
7
9
  from google.genai import errors as gerrors
@@ -22,6 +24,7 @@ from khoj.processor.conversation.utils import (
22
24
  get_image_from_url,
23
25
  )
24
26
  from khoj.utils.helpers import (
27
+ get_ai_api_info,
25
28
  get_chat_usage_metrics,
26
29
  is_none_or_empty,
27
30
  is_promptrace_enabled,
@@ -29,6 +32,7 @@ from khoj.utils.helpers import (
29
32
 
30
33
  logger = logging.getLogger(__name__)
31
34
 
35
+ gemini_clients: Dict[str, genai.Client] = {}
32
36
 
33
37
  MAX_OUTPUT_TOKENS_GEMINI = 8192
34
38
  SAFETY_SETTINGS = [
@@ -51,6 +55,17 @@ SAFETY_SETTINGS = [
51
55
  ]
52
56
 
53
57
 
58
+ def get_gemini_client(api_key, api_base_url=None) -> genai.Client:
59
+ api_info = get_ai_api_info(api_key, api_base_url)
60
+ return genai.Client(
61
+ location=api_info.region,
62
+ project=api_info.project,
63
+ credentials=api_info.credentials,
64
+ api_key=api_info.api_key,
65
+ vertexai=api_info.api_key is None,
66
+ )
67
+
68
+
54
69
  @retry(
55
70
  wait=wait_random_exponential(min=1, max=10),
56
71
  stop=stop_after_attempt(2),
@@ -58,9 +73,14 @@ SAFETY_SETTINGS = [
58
73
  reraise=True,
59
74
  )
60
75
  def gemini_completion_with_backoff(
61
- messages, system_prompt, model_name, temperature=0, api_key=None, model_kwargs=None, tracer={}
76
+ messages, system_prompt, model_name, temperature=0.8, api_key=None, api_base_url=None, model_kwargs=None, tracer={}
62
77
  ) -> str:
63
- client = genai.Client(api_key=api_key)
78
+ client = gemini_clients.get(api_key)
79
+ if not client:
80
+ client = get_gemini_client(api_key, api_base_url)
81
+ gemini_clients[api_key] = client
82
+
83
+ seed = int(os.getenv("KHOJ_LLM_SEED")) if os.getenv("KHOJ_LLM_SEED") else None
64
84
  config = gtypes.GenerateContentConfig(
65
85
  system_instruction=system_prompt,
66
86
  temperature=temperature,
@@ -68,6 +88,7 @@ def gemini_completion_with_backoff(
68
88
  safety_settings=SAFETY_SETTINGS,
69
89
  response_mime_type=model_kwargs.get("response_mime_type", "text/plain") if model_kwargs else "text/plain",
70
90
  response_schema=model_kwargs.get("response_schema", None) if model_kwargs else None,
91
+ seed=seed,
71
92
  )
72
93
 
73
94
  formatted_messages = [gtypes.Content(role=message.role, parts=message.content) for message in messages]
@@ -112,6 +133,7 @@ def gemini_chat_completion_with_backoff(
112
133
  model_name,
113
134
  temperature,
114
135
  api_key,
136
+ api_base_url,
115
137
  system_prompt,
116
138
  completion_func=None,
117
139
  model_kwargs=None,
@@ -120,23 +142,37 @@ def gemini_chat_completion_with_backoff(
120
142
  g = ThreadedGenerator(compiled_references, online_results, completion_func=completion_func)
121
143
  t = Thread(
122
144
  target=gemini_llm_thread,
123
- args=(g, messages, system_prompt, model_name, temperature, api_key, model_kwargs, tracer),
145
+ args=(g, messages, system_prompt, model_name, temperature, api_key, api_base_url, model_kwargs, tracer),
124
146
  )
125
147
  t.start()
126
148
  return g
127
149
 
128
150
 
129
151
  def gemini_llm_thread(
130
- g, messages, system_prompt, model_name, temperature, api_key, model_kwargs=None, tracer: dict = {}
152
+ g,
153
+ messages,
154
+ system_prompt,
155
+ model_name,
156
+ temperature,
157
+ api_key,
158
+ api_base_url=None,
159
+ model_kwargs=None,
160
+ tracer: dict = {},
131
161
  ):
132
162
  try:
133
- client = genai.Client(api_key=api_key)
163
+ client = gemini_clients.get(api_key)
164
+ if not client:
165
+ client = get_gemini_client(api_key, api_base_url)
166
+ gemini_clients[api_key] = client
167
+
168
+ seed = int(os.getenv("KHOJ_LLM_SEED")) if os.getenv("KHOJ_LLM_SEED") else None
134
169
  config = gtypes.GenerateContentConfig(
135
170
  system_instruction=system_prompt,
136
171
  temperature=temperature,
137
172
  max_output_tokens=MAX_OUTPUT_TOKENS_GEMINI,
138
173
  stop_sequences=["Notes:\n["],
139
174
  safety_settings=SAFETY_SETTINGS,
175
+ seed=seed,
140
176
  )
141
177
 
142
178
  aggregated_response = ""
@@ -63,7 +63,6 @@ def extract_questions(
63
63
  today = datetime.today()
64
64
  current_new_year = today.replace(month=1, day=1)
65
65
  last_new_year = current_new_year.replace(year=today.year - 1)
66
- temperature = 0.7
67
66
 
68
67
  prompt = prompts.extract_questions.format(
69
68
  current_date=today.strftime("%Y-%m-%d"),
@@ -99,7 +98,6 @@ def extract_questions(
99
98
  model,
100
99
  response_type="json_object",
101
100
  api_base_url=api_base_url,
102
- temperature=temperature,
103
101
  tracer=tracer,
104
102
  )
105
103
 
@@ -127,7 +125,6 @@ def send_message_to_model(
127
125
  response_type="text",
128
126
  response_schema=None,
129
127
  api_base_url=None,
130
- temperature=0,
131
128
  tracer: dict = {},
132
129
  ):
133
130
  """
@@ -146,7 +143,6 @@ def send_message_to_model(
146
143
  messages=messages,
147
144
  model_name=model,
148
145
  openai_api_key=api_key,
149
- temperature=temperature,
150
146
  api_base_url=api_base_url,
151
147
  model_kwargs=model_kwargs,
152
148
  tracer=tracer,
@@ -162,7 +158,7 @@ def converse_openai(
162
158
  model: str = "gpt-4o-mini",
163
159
  api_key: Optional[str] = None,
164
160
  api_base_url: Optional[str] = None,
165
- temperature: float = 0.2,
161
+ temperature: float = 0.4,
166
162
  completion_func=None,
167
163
  conversation_commands=[ConversationCommand.Default],
168
164
  max_prompt_size=None,
@@ -48,14 +48,14 @@ openai_clients: Dict[str, openai.OpenAI] = {}
48
48
  def completion_with_backoff(
49
49
  messages,
50
50
  model_name: str,
51
- temperature=0,
51
+ temperature=0.8,
52
52
  openai_api_key=None,
53
53
  api_base_url=None,
54
54
  model_kwargs: dict = {},
55
55
  tracer: dict = {},
56
56
  ) -> str:
57
57
  client_key = f"{openai_api_key}--{api_base_url}"
58
- client: openai.OpenAI | None = openai_clients.get(client_key)
58
+ client = openai_clients.get(client_key)
59
59
  if not client:
60
60
  client = get_openai_client(openai_api_key, api_base_url)
61
61
  openai_clients[client_key] = client
@@ -150,9 +150,8 @@ def llm_thread(
150
150
  ):
151
151
  try:
152
152
  client_key = f"{openai_api_key}--{api_base_url}"
153
- if client_key in openai_clients:
154
- client = openai_clients[client_key]
155
- else:
153
+ client = openai_clients.get(client_key)
154
+ if not client:
156
155
  client = get_openai_client(openai_api_key, api_base_url)
157
156
  openai_clients[client_key] = client
158
157
 
@@ -247,4 +246,6 @@ def get_openai_api_json_support(model_name: str, api_base_url: str = None) -> Js
247
246
  host = urlparse(api_base_url).hostname
248
247
  if host and host.endswith(".ai.azure.com"):
249
248
  return JsonSupport.OBJECT
249
+ if host == "api.deepinfra.com":
250
+ return JsonSupport.OBJECT
250
251
  return JsonSupport.SCHEMA
khoj/routers/api.py CHANGED
@@ -463,12 +463,14 @@ async def extract_references_and_questions(
463
463
  )
464
464
  elif chat_model.model_type == ChatModel.ModelType.ANTHROPIC:
465
465
  api_key = chat_model.ai_model_api.api_key
466
+ api_base_url = chat_model.ai_model_api.api_base_url
466
467
  chat_model_name = chat_model.name
467
468
  inferred_queries = extract_questions_anthropic(
468
469
  defiltered_query,
469
470
  query_images=query_images,
470
471
  model=chat_model_name,
471
472
  api_key=api_key,
473
+ api_base_url=api_base_url,
472
474
  conversation_log=meta_log,
473
475
  location_data=location_data,
474
476
  user=user,
@@ -479,12 +481,14 @@ async def extract_references_and_questions(
479
481
  )
480
482
  elif chat_model.model_type == ChatModel.ModelType.GOOGLE:
481
483
  api_key = chat_model.ai_model_api.api_key
484
+ api_base_url = chat_model.ai_model_api.api_base_url
482
485
  chat_model_name = chat_model.name
483
486
  inferred_queries = extract_questions_gemini(
484
487
  defiltered_query,
485
488
  query_images=query_images,
486
489
  model=chat_model_name,
487
490
  api_key=api_key,
491
+ api_base_url=api_base_url,
488
492
  conversation_log=meta_log,
489
493
  location_data=location_data,
490
494
  max_tokens=chat_model.max_prompt_size,
khoj/routers/auth.py CHANGED
@@ -43,12 +43,9 @@ class MagicLinkForm(BaseModel):
43
43
  if not state.anonymous_mode:
44
44
  missing_requirements = []
45
45
  from authlib.integrations.starlette_client import OAuth, OAuthError
46
+ from google.auth.transport import requests as google_requests
47
+ from google.oauth2 import id_token
46
48
 
47
- try:
48
- from google.auth.transport import requests as google_requests
49
- from google.oauth2 import id_token
50
- except ImportError:
51
- missing_requirements += ["Install the Khoj production package with `pip install khoj[prod]`"]
52
49
  if not os.environ.get("RESEND_API_KEY") and (
53
50
  not os.environ.get("GOOGLE_CLIENT_ID") or not os.environ.get("GOOGLE_CLIENT_SECRET")
54
51
  ):
khoj/routers/helpers.py CHANGED
@@ -1220,6 +1220,7 @@ async def send_message_to_model_wrapper(
1220
1220
  )
1221
1221
  elif model_type == ChatModel.ModelType.ANTHROPIC:
1222
1222
  api_key = chat_model.ai_model_api.api_key
1223
+ api_base_url = chat_model.ai_model_api.api_base_url
1223
1224
  truncated_messages = generate_chatml_messages_with_context(
1224
1225
  user_message=query,
1225
1226
  context_message=context,
@@ -1239,10 +1240,12 @@ async def send_message_to_model_wrapper(
1239
1240
  model=chat_model_name,
1240
1241
  response_type=response_type,
1241
1242
  deepthought=deepthought,
1243
+ api_base_url=api_base_url,
1242
1244
  tracer=tracer,
1243
1245
  )
1244
1246
  elif model_type == ChatModel.ModelType.GOOGLE:
1245
1247
  api_key = chat_model.ai_model_api.api_key
1248
+ api_base_url = chat_model.ai_model_api.api_base_url
1246
1249
  truncated_messages = generate_chatml_messages_with_context(
1247
1250
  user_message=query,
1248
1251
  context_message=context,
@@ -1262,6 +1265,7 @@ async def send_message_to_model_wrapper(
1262
1265
  model=chat_model_name,
1263
1266
  response_type=response_type,
1264
1267
  response_schema=response_schema,
1268
+ api_base_url=api_base_url,
1265
1269
  tracer=tracer,
1266
1270
  )
1267
1271
  else:
@@ -1328,7 +1332,7 @@ def send_message_to_model_wrapper_sync(
1328
1332
  query_files=query_files,
1329
1333
  )
1330
1334
 
1331
- openai_response = send_message_to_model(
1335
+ return send_message_to_model(
1332
1336
  messages=truncated_messages,
1333
1337
  api_key=api_key,
1334
1338
  api_base_url=api_base_url,
@@ -1338,10 +1342,9 @@ def send_message_to_model_wrapper_sync(
1338
1342
  tracer=tracer,
1339
1343
  )
1340
1344
 
1341
- return openai_response
1342
-
1343
1345
  elif chat_model.model_type == ChatModel.ModelType.ANTHROPIC:
1344
1346
  api_key = chat_model.ai_model_api.api_key
1347
+ api_base_url = chat_model.ai_model_api.api_base_url
1345
1348
  truncated_messages = generate_chatml_messages_with_context(
1346
1349
  user_message=message,
1347
1350
  system_message=system_message,
@@ -1356,6 +1359,7 @@ def send_message_to_model_wrapper_sync(
1356
1359
  return anthropic_send_message_to_model(
1357
1360
  messages=truncated_messages,
1358
1361
  api_key=api_key,
1362
+ api_base_url=api_base_url,
1359
1363
  model=chat_model_name,
1360
1364
  response_type=response_type,
1361
1365
  tracer=tracer,
@@ -1363,6 +1367,7 @@ def send_message_to_model_wrapper_sync(
1363
1367
 
1364
1368
  elif chat_model.model_type == ChatModel.ModelType.GOOGLE:
1365
1369
  api_key = chat_model.ai_model_api.api_key
1370
+ api_base_url = chat_model.ai_model_api.api_base_url
1366
1371
  truncated_messages = generate_chatml_messages_with_context(
1367
1372
  user_message=message,
1368
1373
  system_message=system_message,
@@ -1377,6 +1382,7 @@ def send_message_to_model_wrapper_sync(
1377
1382
  return gemini_send_message_to_model(
1378
1383
  messages=truncated_messages,
1379
1384
  api_key=api_key,
1385
+ api_base_url=api_base_url,
1380
1386
  model=chat_model_name,
1381
1387
  response_type=response_type,
1382
1388
  response_schema=response_schema,
@@ -1510,6 +1516,7 @@ def generate_chat_response(
1510
1516
 
1511
1517
  elif chat_model.model_type == ChatModel.ModelType.ANTHROPIC:
1512
1518
  api_key = chat_model.ai_model_api.api_key
1519
+ api_base_url = chat_model.ai_model_api.api_base_url
1513
1520
  chat_response = converse_anthropic(
1514
1521
  compiled_references,
1515
1522
  query_to_run,
@@ -1519,6 +1526,7 @@ def generate_chat_response(
1519
1526
  conversation_log=meta_log,
1520
1527
  model=chat_model.name,
1521
1528
  api_key=api_key,
1529
+ api_base_url=api_base_url,
1522
1530
  completion_func=partial_completion,
1523
1531
  conversation_commands=conversation_commands,
1524
1532
  max_prompt_size=chat_model.max_prompt_size,
@@ -1536,6 +1544,7 @@ def generate_chat_response(
1536
1544
  )
1537
1545
  elif chat_model.model_type == ChatModel.ModelType.GOOGLE:
1538
1546
  api_key = chat_model.ai_model_api.api_key
1547
+ api_base_url = chat_model.ai_model_api.api_base_url
1539
1548
  chat_response = converse_gemini(
1540
1549
  compiled_references,
1541
1550
  query_to_run,
@@ -1544,6 +1553,7 @@ def generate_chat_response(
1544
1553
  meta_log,
1545
1554
  model=chat_model.name,
1546
1555
  api_key=api_key,
1556
+ api_base_url=api_base_url,
1547
1557
  completion_func=partial_completion,
1548
1558
  conversation_commands=conversation_commands,
1549
1559
  max_prompt_size=chat_model.max_prompt_size,
khoj/utils/constants.py CHANGED
@@ -49,8 +49,10 @@ model_to_cost: Dict[str, Dict[str, float]] = {
49
49
  "gemini-2.0-flash": {"input": 0.10, "output": 0.40},
50
50
  # Anthropic Pricing: https://www.anthropic.com/pricing#anthropic-api_
51
51
  "claude-3-5-haiku-20241022": {"input": 1.0, "output": 5.0},
52
+ "claude-3-5-haiku@20241022": {"input": 1.0, "output": 5.0},
52
53
  "claude-3-5-sonnet-20241022": {"input": 3.0, "output": 15.0},
53
54
  "claude-3-5-sonnet-latest": {"input": 3.0, "output": 15.0},
54
55
  "claude-3-7-sonnet-20250219": {"input": 3.0, "output": 15.0},
56
+ "claude-3-7-sonnet@20250219": {"input": 3.0, "output": 15.0},
55
57
  "claude-3-7-sonnet-latest": {"input": 3.0, "output": 15.0},
56
58
  }
khoj/utils/helpers.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations # to avoid quoting type hints
2
2
 
3
+ import base64
3
4
  import copy
4
5
  import datetime
5
6
  import io
@@ -19,15 +20,18 @@ from itertools import islice
19
20
  from os import path
20
21
  from pathlib import Path
21
22
  from time import perf_counter
22
- from typing import TYPE_CHECKING, Any, Optional, Union
23
- from urllib.parse import urlparse
23
+ from typing import TYPE_CHECKING, Any, NamedTuple, Optional, Tuple, Union
24
+ from urllib.parse import ParseResult, urlparse
24
25
 
25
26
  import openai
26
27
  import psutil
28
+ import pyjson5
27
29
  import requests
28
30
  import torch
29
31
  from asgiref.sync import sync_to_async
30
32
  from email_validator import EmailNotValidError, EmailUndeliverableError, validate_email
33
+ from google.auth.credentials import Credentials
34
+ from google.oauth2 import service_account
31
35
  from magika import Magika
32
36
  from PIL import Image
33
37
  from pytz import country_names, country_timezones
@@ -618,6 +622,58 @@ def get_chat_usage_metrics(
618
622
  }
619
623
 
620
624
 
625
+ class AiApiInfo(NamedTuple):
626
+ region: str
627
+ project: str
628
+ credentials: Credentials
629
+ api_key: str
630
+
631
+
632
+ def get_gcp_credentials(credentials_b64: str) -> Optional[Credentials]:
633
+ """
634
+ Get GCP credentials from base64 encoded service account credentials json keyfile
635
+ """
636
+ credentials_json = base64.b64decode(credentials_b64).decode("utf-8")
637
+ credentials_dict = pyjson5.loads(credentials_json)
638
+ credentials = service_account.Credentials.from_service_account_info(credentials_dict)
639
+ return credentials.with_scopes(scopes=["https://www.googleapis.com/auth/cloud-platform"])
640
+
641
+
642
+ def get_gcp_project_info(parsed_api_url: ParseResult) -> Tuple[str, str]:
643
+ """
644
+ Extract region, project id from GCP API url
645
+ API url is of form https://{REGION}-aiplatform.googleapis.com/v1/projects/{PROJECT_ID}...
646
+ """
647
+ # Extract region from hostname
648
+ hostname = parsed_api_url.netloc
649
+ region = hostname.split("-aiplatform")[0] if "-aiplatform" in hostname else ""
650
+
651
+ # Extract project ID from path (e.g., "/v1/projects/my-project/...")
652
+ path_parts = parsed_api_url.path.split("/")
653
+ project_id = ""
654
+ for i, part in enumerate(path_parts):
655
+ if part == "projects" and i + 1 < len(path_parts):
656
+ project_id = path_parts[i + 1]
657
+ break
658
+
659
+ return region, project_id
660
+
661
+
662
+ def get_ai_api_info(api_key, api_base_url: str = None) -> AiApiInfo:
663
+ """
664
+ Get the GCP Vertex or default AI API client info based on the API key and URL.
665
+ """
666
+ region, project_id, credentials = None, None, None
667
+ # Check if AI model to be used via GCP Vertex API
668
+ parsed_api_url = urlparse(api_base_url)
669
+ if parsed_api_url.hostname and parsed_api_url.hostname.endswith(".googleapis.com"):
670
+ region, project_id = get_gcp_project_info(parsed_api_url)
671
+ credentials = get_gcp_credentials(api_key)
672
+ if credentials:
673
+ api_key = None
674
+ return AiApiInfo(region=region, project=project_id, credentials=credentials, api_key=api_key)
675
+
676
+
621
677
  def get_openai_client(api_key: str, api_base_url: str) -> Union[openai.OpenAI, openai.AzureOpenAI]:
622
678
  """Get OpenAI or AzureOpenAI client based on the API Base URL"""
623
679
  parsed_url = urlparse(api_base_url)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: khoj
3
- Version: 1.37.0
3
+ Version: 1.37.1
4
4
  Summary: Your Second Brain
5
5
  Project-URL: Homepage, https://khoj.dev
6
6
  Project-URL: Documentation, https://docs.khoj.dev
@@ -39,6 +39,7 @@ Requires-Dist: e2b-code-interpreter~=1.0.0
39
39
  Requires-Dist: einops==0.8.0
40
40
  Requires-Dist: email-validator==2.2.0
41
41
  Requires-Dist: fastapi>=0.110.0
42
+ Requires-Dist: google-auth~=2.23.3
42
43
  Requires-Dist: google-genai==1.5.0
43
44
  Requires-Dist: httpx==0.28.1
44
45
  Requires-Dist: huggingface-hub>=0.22.2
@@ -69,12 +70,12 @@ Requires-Dist: requests>=2.26.0
69
70
  Requires-Dist: resend==1.0.1
70
71
  Requires-Dist: rich>=13.3.1
71
72
  Requires-Dist: schedule==1.1.0
72
- Requires-Dist: sentence-transformers==3.0.1
73
+ Requires-Dist: sentence-transformers==3.4.1
73
74
  Requires-Dist: tenacity==8.3.0
74
75
  Requires-Dist: tenacity>=8.2.2
75
76
  Requires-Dist: tiktoken>=0.3.2
76
- Requires-Dist: torch==2.2.2
77
- Requires-Dist: transformers>=4.28.0
77
+ Requires-Dist: torch==2.6.0
78
+ Requires-Dist: transformers<4.50.0,>=4.28.0
78
79
  Requires-Dist: tzdata==2023.3
79
80
  Requires-Dist: uvicorn==0.30.6
80
81
  Requires-Dist: websockets==13.0
@@ -85,7 +86,6 @@ Requires-Dist: datasets; extra == 'dev'
85
86
  Requires-Dist: factory-boy>=3.2.1; extra == 'dev'
86
87
  Requires-Dist: freezegun>=1.2.0; extra == 'dev'
87
88
  Requires-Dist: gitpython~=3.1.43; extra == 'dev'
88
- Requires-Dist: google-auth==2.23.3; extra == 'dev'
89
89
  Requires-Dist: gunicorn==22.0.0; extra == 'dev'
90
90
  Requires-Dist: mypy>=1.0.1; extra == 'dev'
91
91
  Requires-Dist: pandas; extra == 'dev'
@@ -98,7 +98,6 @@ Requires-Dist: stripe==7.3.0; extra == 'dev'
98
98
  Requires-Dist: twilio==8.11; extra == 'dev'
99
99
  Provides-Extra: prod
100
100
  Requires-Dist: boto3>=1.34.57; extra == 'prod'
101
- Requires-Dist: google-auth==2.23.3; extra == 'prod'
102
101
  Requires-Dist: gunicorn==22.0.0; extra == 'prod'
103
102
  Requires-Dist: stripe==7.3.0; extra == 'prod'
104
103
  Requires-Dist: twilio==8.11; extra == 'prod'