khoj 1.40.1.dev1__py3-none-any.whl → 1.40.1.dev15__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 (67) hide show
  1. khoj/app/settings.py +6 -12
  2. khoj/database/adapters/__init__.py +31 -10
  3. khoj/database/migrations/0089_chatmodel_price_tier_and_more.py +34 -0
  4. khoj/database/models/__init__.py +20 -0
  5. khoj/interface/compiled/404/index.html +2 -2
  6. khoj/interface/compiled/_next/static/chunks/2327-abb42c2498f4438a.js +1 -0
  7. khoj/interface/compiled/_next/static/chunks/5427-442f34b514b9fc26.js +1 -0
  8. khoj/interface/compiled/_next/static/chunks/{8515-f305779d95dd5780.js → 8515-010dd769c584b672.js} +1 -1
  9. khoj/interface/compiled/_next/static/chunks/app/agents/{page-3993a8df749f2f29.js → page-ceeb9a91edea74ce.js} +1 -1
  10. khoj/interface/compiled/_next/static/chunks/app/automations/{page-50182e85e30880e1.js → page-e3cb78747ab98cc7.js} +1 -1
  11. khoj/interface/compiled/_next/static/chunks/app/chat/layout-33934fc2d6ae6838.js +1 -0
  12. khoj/interface/compiled/_next/static/chunks/app/chat/page-f9ff86b966e90ce0.js +1 -0
  13. khoj/interface/compiled/_next/static/chunks/app/layout-baa6e7974e560a7a.js +1 -0
  14. khoj/interface/compiled/_next/static/chunks/app/{page-392b7719999f2e46.js → page-a4053e1bb578b2ce.js} +1 -1
  15. khoj/interface/compiled/_next/static/chunks/app/search/layout-c02531d586972d7d.js +1 -0
  16. khoj/interface/compiled/_next/static/chunks/app/search/{page-f7f648807b59310a.js → page-8973da2f4c076fe1.js} +1 -1
  17. khoj/interface/compiled/_next/static/chunks/app/settings/page-375136dbb400525b.js +1 -0
  18. khoj/interface/compiled/_next/static/chunks/app/share/chat/layout-6fb51c5c80f8ec67.js +1 -0
  19. khoj/interface/compiled/_next/static/chunks/app/share/chat/{page-52a567d6080cd5bb.js → page-384b54fc953b18f2.js} +1 -1
  20. khoj/interface/compiled/_next/static/chunks/{webpack-c6bde5961098facd.js → webpack-1169ca6e9e7e6247.js} +1 -1
  21. khoj/interface/compiled/_next/static/css/37a73b87f02df402.css +1 -0
  22. khoj/interface/compiled/_next/static/css/f29752d6e1be7624.css +1 -0
  23. khoj/interface/compiled/_next/static/css/{0db53bacf81896f5.css → fca983d49c3dd1a3.css} +1 -1
  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 +3 -3
  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 +3 -3
  36. khoj/interface/compiled/share/chat/index.html +2 -2
  37. khoj/interface/compiled/share/chat/index.txt +3 -3
  38. khoj/processor/content/images/image_to_entries.py +2 -2
  39. khoj/processor/conversation/anthropic/utils.py +7 -2
  40. khoj/processor/conversation/google/gemini_chat.py +4 -0
  41. khoj/processor/conversation/google/utils.py +44 -5
  42. khoj/processor/conversation/openai/utils.py +32 -6
  43. khoj/processor/tools/run_code.py +7 -2
  44. khoj/routers/api_agents.py +39 -11
  45. khoj/routers/api_model.py +41 -7
  46. khoj/routers/helpers.py +19 -4
  47. khoj/utils/constants.py +9 -0
  48. khoj/utils/helpers.py +13 -2
  49. {khoj-1.40.1.dev1.dist-info → khoj-1.40.1.dev15.dist-info}/METADATA +16 -3
  50. {khoj-1.40.1.dev1.dist-info → khoj-1.40.1.dev15.dist-info}/RECORD +57 -56
  51. khoj/interface/compiled/_next/static/chunks/2327-9d37761c0bdbc3ff.js +0 -1
  52. khoj/interface/compiled/_next/static/chunks/5427-ec87e7fa4b0d76cb.js +0 -1
  53. khoj/interface/compiled/_next/static/chunks/app/chat/layout-d5ae861e1ade9d08.js +0 -1
  54. khoj/interface/compiled/_next/static/chunks/app/chat/page-8233a00e74d4aa5f.js +0 -1
  55. khoj/interface/compiled/_next/static/chunks/app/layout-bd8210ff1de491d7.js +0 -1
  56. khoj/interface/compiled/_next/static/chunks/app/search/layout-f5881c7ae3ba0795.js +0 -1
  57. khoj/interface/compiled/_next/static/chunks/app/settings/page-32122d865d786a47.js +0 -1
  58. khoj/interface/compiled/_next/static/chunks/app/share/chat/layout-64a53f8ec4afa6b3.js +0 -1
  59. khoj/interface/compiled/_next/static/css/bb7ea98028b368f3.css +0 -1
  60. khoj/interface/compiled/_next/static/css/ee66643a6a5bf71c.css +0 -1
  61. /khoj/interface/compiled/_next/static/{cbvk0UJyMR5BLJ1jy2JpS → FzcZLvNUS7ajZQ9U8JFVB}/_buildManifest.js +0 -0
  62. /khoj/interface/compiled/_next/static/{cbvk0UJyMR5BLJ1jy2JpS → FzcZLvNUS7ajZQ9U8JFVB}/_ssgManifest.js +0 -0
  63. /khoj/interface/compiled/_next/static/chunks/{4986-10ca6c2d6cbcb448.js → 4986-14ea63faad1615a4.js} +0 -0
  64. /khoj/interface/compiled/_next/static/chunks/{5477-060a89922423c280.js → 5477-c47f3a23981c89da.js} +0 -0
  65. {khoj-1.40.1.dev1.dist-info → khoj-1.40.1.dev15.dist-info}/WHEEL +0 -0
  66. {khoj-1.40.1.dev1.dist-info → khoj-1.40.1.dev15.dist-info}/entry_points.txt +0 -0
  67. {khoj-1.40.1.dev1.dist-info → khoj-1.40.1.dev15.dist-info}/licenses/LICENSE +0 -0
@@ -64,9 +64,13 @@ def completion_with_backoff(
64
64
  formatted_messages = [{"role": message.role, "content": message.content} for message in messages]
65
65
 
66
66
  # Tune reasoning models arguments
67
- if model_name.startswith("o1") or model_name.startswith("o3"):
67
+ if is_openai_reasoning_model(model_name, api_base_url):
68
68
  temperature = 1
69
- model_kwargs["reasoning_effort"] = "medium"
69
+ reasoning_effort = "medium" if deepthought else "low"
70
+ model_kwargs["reasoning_effort"] = reasoning_effort
71
+ elif is_twitter_reasoning_model(model_name, api_base_url):
72
+ reasoning_effort = "high" if deepthought else "low"
73
+ model_kwargs["reasoning_effort"] = reasoning_effort
70
74
 
71
75
  model_kwargs["stream_options"] = {"include_usage": True}
72
76
  if os.getenv("KHOJ_LLM_SEED"):
@@ -162,12 +166,13 @@ def llm_thread(
162
166
 
163
167
  formatted_messages = [{"role": message.role, "content": message.content} for message in messages]
164
168
 
165
- # Tune reasoning models arguments
166
- if model_name.startswith("o1") or model_name.startswith("o3"):
169
+ # Configure thinking for openai reasoning models
170
+ if is_openai_reasoning_model(model_name, api_base_url):
167
171
  temperature = 1
168
- model_kwargs["reasoning_effort"] = "medium"
172
+ reasoning_effort = "medium" if deepthought else "low"
173
+ model_kwargs["reasoning_effort"] = reasoning_effort
174
+ model_kwargs.pop("stop", None) # Remove unsupported stop param for reasoning models
169
175
 
170
- if model_name.startswith("o3"):
171
176
  # Get the first system message and add the string `Formatting re-enabled` to it.
172
177
  # See https://platform.openai.com/docs/guides/reasoning-best-practices
173
178
  if len(formatted_messages) > 0:
@@ -179,6 +184,9 @@ def llm_thread(
179
184
  formatted_messages[first_system_message_index][
180
185
  "content"
181
186
  ] = f"{first_system_message} Formatting re-enabled"
187
+ elif is_twitter_reasoning_model(model_name, api_base_url):
188
+ reasoning_effort = "high" if deepthought else "low"
189
+ model_kwargs["reasoning_effort"] = reasoning_effort
182
190
  elif model_name.startswith("deepseek-reasoner"):
183
191
  # Two successive messages cannot be from the same role. Should merge any back-to-back messages from the same role.
184
192
  # The first message should always be a user message (except system message).
@@ -257,3 +265,21 @@ def get_openai_api_json_support(model_name: str, api_base_url: str = None) -> Js
257
265
  if host == "api.deepinfra.com":
258
266
  return JsonSupport.OBJECT
259
267
  return JsonSupport.SCHEMA
268
+
269
+
270
+ def is_openai_reasoning_model(model_name: str, api_base_url: str = None) -> bool:
271
+ """
272
+ Check if the model is an OpenAI reasoning model
273
+ """
274
+ return model_name.startswith("o") and (api_base_url is None or api_base_url.startswith("https://api.openai.com/v1"))
275
+
276
+
277
+ def is_twitter_reasoning_model(model_name: str, api_base_url: str = None) -> bool:
278
+ """
279
+ Check if the model is a Twitter reasoning model
280
+ """
281
+ return (
282
+ model_name.startswith("grok-3-mini")
283
+ and api_base_url is not None
284
+ and api_base_url.startswith("https://api.x.ai/v1")
285
+ )
@@ -244,6 +244,7 @@ async def execute_e2b(code: str, input_files: list[dict]) -> dict[str, Any]:
244
244
 
245
245
  # Collect output files
246
246
  output_files = []
247
+ image_file_ext = {".png", ".jpeg", ".jpg", ".svg"}
247
248
 
248
249
  # Identify new files created during execution
249
250
  new_files = set(E2bFile(f.name, f.path) for f in await sandbox.files.list("~")) - original_files
@@ -254,7 +255,7 @@ async def execute_e2b(code: str, input_files: list[dict]) -> dict[str, Any]:
254
255
  if isinstance(content, bytes):
255
256
  # Binary files like PNG - encode as base64
256
257
  b64_data = base64.b64encode(content).decode("utf-8")
257
- elif Path(f.name).suffix in [".png", ".jpeg", ".jpg", ".svg"]:
258
+ elif Path(f.name).suffix in image_file_ext:
258
259
  # Ignore image files as they are extracted from execution results below for inline display
259
260
  continue
260
261
  else:
@@ -263,8 +264,12 @@ async def execute_e2b(code: str, input_files: list[dict]) -> dict[str, Any]:
263
264
  output_files.append({"filename": f.name, "b64_data": b64_data})
264
265
 
265
266
  # Collect output files from execution results
267
+ # Repect ordering of output result types to disregard text output associated with images
268
+ output_result_types = ["png", "jpeg", "svg", "text", "markdown", "json"]
266
269
  for idx, result in enumerate(execution.results):
267
- for result_type in {"png", "jpeg", "svg", "text", "markdown", "json"}:
270
+ if getattr(result, "chart", None):
271
+ continue
272
+ for result_type in output_result_types:
268
273
  if b64_data := getattr(result, result_type, None):
269
274
  output_files.append({"filename": f"{idx}.{result_type}", "b64_data": b64_data})
270
275
  break
@@ -12,7 +12,7 @@ from pydantic import BaseModel
12
12
  from starlette.authentication import has_required_scope, requires
13
13
 
14
14
  from khoj.database.adapters import AgentAdapters, ConversationAdapters, EntryAdapters
15
- from khoj.database.models import Agent, Conversation, KhojUser
15
+ from khoj.database.models import Agent, Conversation, KhojUser, PriceTier
16
16
  from khoj.routers.helpers import CommonQueryParams, acheck_if_safe_prompt
17
17
  from khoj.utils.helpers import (
18
18
  ConversationCommand,
@@ -125,8 +125,20 @@ async def get_agent_by_conversation(
125
125
  else:
126
126
  agent = await AgentAdapters.aget_default_agent()
127
127
 
128
+ if agent is None:
129
+ return Response(
130
+ content=json.dumps({"error": f"Agent for conversation id {conversation_id} not found for user {user}."}),
131
+ media_type="application/json",
132
+ status_code=404,
133
+ )
134
+
135
+ chat_model = await AgentAdapters.aget_agent_chat_model(agent, user)
136
+ if is_subscribed or chat_model.price_tier == PriceTier.FREE:
137
+ agent_chat_model = chat_model.name
138
+ else:
139
+ agent_chat_model = None
140
+
128
141
  has_files = agent.fileobject_set.exists()
129
- agent.chat_model = await AgentAdapters.aget_agent_chat_model(agent, user)
130
142
 
131
143
  agents_packet = {
132
144
  "slug": agent.slug,
@@ -137,7 +149,7 @@ async def get_agent_by_conversation(
137
149
  "color": agent.style_color,
138
150
  "icon": agent.style_icon,
139
151
  "privacy_level": agent.privacy_level,
140
- "chat_model": agent.chat_model.name if is_subscribed else None,
152
+ "chat_model": agent_chat_model,
141
153
  "has_files": has_files,
142
154
  "input_tools": agent.input_tools,
143
155
  "output_modes": agent.output_modes,
@@ -249,7 +261,11 @@ async def update_hidden_agent(
249
261
  user: KhojUser = request.user.object
250
262
 
251
263
  subscribed = has_required_scope(request, ["premium"])
252
- chat_model = body.chat_model if subscribed else None
264
+ chat_model = await ConversationAdapters.aget_chat_model_by_name(body.chat_model)
265
+ if subscribed or chat_model.price_tier == PriceTier.FREE:
266
+ agent_chat_model = body.chat_model
267
+ else:
268
+ agent_chat_model = None
253
269
 
254
270
  selected_agent = await AgentAdapters.aget_agent_by_slug(body.slug, user)
255
271
 
@@ -264,7 +280,7 @@ async def update_hidden_agent(
264
280
  user=user,
265
281
  slug=body.slug,
266
282
  persona=body.persona,
267
- chat_model=chat_model,
283
+ chat_model=agent_chat_model,
268
284
  input_tools=body.input_tools,
269
285
  output_modes=body.output_modes,
270
286
  existing_agent=selected_agent,
@@ -295,7 +311,11 @@ async def create_hidden_agent(
295
311
  user: KhojUser = request.user.object
296
312
 
297
313
  subscribed = has_required_scope(request, ["premium"])
298
- chat_model = body.chat_model if subscribed else None
314
+ chat_model = await ConversationAdapters.aget_chat_model_by_name(body.chat_model)
315
+ if subscribed or chat_model.price_tier == PriceTier.FREE:
316
+ agent_chat_model = body.chat_model
317
+ else:
318
+ agent_chat_model = None
299
319
 
300
320
  conversation = await ConversationAdapters.aget_conversation_by_user(user=user, conversation_id=conversation_id)
301
321
  if not conversation:
@@ -320,7 +340,7 @@ async def create_hidden_agent(
320
340
  user=user,
321
341
  slug=body.slug,
322
342
  persona=body.persona,
323
- chat_model=chat_model,
343
+ chat_model=agent_chat_model,
324
344
  input_tools=body.input_tools,
325
345
  output_modes=body.output_modes,
326
346
  existing_agent=None,
@@ -364,7 +384,11 @@ async def create_agent(
364
384
  )
365
385
 
366
386
  subscribed = has_required_scope(request, ["premium"])
367
- chat_model = body.chat_model if subscribed else None
387
+ chat_model = await ConversationAdapters.aget_chat_model_by_name(body.chat_model)
388
+ if subscribed or chat_model.price_tier == PriceTier.FREE:
389
+ agent_chat_model = body.chat_model
390
+ else:
391
+ agent_chat_model = None
368
392
 
369
393
  agent = await AgentAdapters.aupdate_agent(
370
394
  user,
@@ -373,7 +397,7 @@ async def create_agent(
373
397
  body.privacy_level,
374
398
  body.icon,
375
399
  body.color,
376
- chat_model,
400
+ agent_chat_model,
377
401
  body.files,
378
402
  body.input_tools,
379
403
  body.output_modes,
@@ -431,7 +455,11 @@ async def update_agent(
431
455
  )
432
456
 
433
457
  subscribed = has_required_scope(request, ["premium"])
434
- chat_model = body.chat_model if subscribed else None
458
+ chat_model = await ConversationAdapters.aget_chat_model_by_name(body.chat_model)
459
+ if subscribed or chat_model.price_tier == PriceTier.FREE:
460
+ agent_chat_model = body.chat_model
461
+ else:
462
+ agent_chat_model = None
435
463
 
436
464
  agent = await AgentAdapters.aupdate_agent(
437
465
  user,
@@ -440,7 +468,7 @@ async def update_agent(
440
468
  body.privacy_level,
441
469
  body.icon,
442
470
  body.color,
443
- chat_model,
471
+ agent_chat_model,
444
472
  body.files,
445
473
  body.input_tools,
446
474
  body.output_modes,
khoj/routers/api_model.py CHANGED
@@ -2,13 +2,18 @@ import json
2
2
  import logging
3
3
  from typing import Dict, Optional, Union
4
4
 
5
- from fastapi import APIRouter, HTTPException, Request
5
+ from fastapi import APIRouter, Request
6
6
  from fastapi.requests import Request
7
7
  from fastapi.responses import Response
8
8
  from starlette.authentication import has_required_scope, requires
9
9
 
10
- from khoj.database import adapters
11
- from khoj.database.adapters import ConversationAdapters, EntryAdapters
10
+ from khoj.database.adapters import ConversationAdapters
11
+ from khoj.database.models import (
12
+ ChatModel,
13
+ PriceTier,
14
+ TextToImageModelConfig,
15
+ VoiceModelOption,
16
+ )
12
17
  from khoj.routers.helpers import update_telemetry_state
13
18
 
14
19
  api_model = APIRouter()
@@ -53,13 +58,24 @@ def get_user_chat_model(
53
58
 
54
59
 
55
60
  @api_model.post("/chat", status_code=200)
56
- @requires(["authenticated", "premium"])
61
+ @requires(["authenticated"])
57
62
  async def update_chat_model(
58
63
  request: Request,
59
64
  id: str,
60
65
  client: Optional[str] = None,
61
66
  ):
62
67
  user = request.user.object
68
+ subscribed = has_required_scope(request, ["premium"])
69
+
70
+ # Validate if model can be switched
71
+ chat_model = await ChatModel.objects.filter(id=int(id)).afirst()
72
+ if chat_model is None:
73
+ return Response(status_code=404, content=json.dumps({"status": "error", "message": "Chat model not found"}))
74
+ if not subscribed and chat_model.price_tier != PriceTier.FREE:
75
+ raise Response(
76
+ status_code=403,
77
+ content=json.dumps({"status": "error", "message": "Subscribe to switch to this chat model"}),
78
+ )
63
79
 
64
80
  new_config = await ConversationAdapters.aset_user_conversation_processor(user, int(id))
65
81
 
@@ -78,13 +94,24 @@ async def update_chat_model(
78
94
 
79
95
 
80
96
  @api_model.post("/voice", status_code=200)
81
- @requires(["authenticated", "premium"])
97
+ @requires(["authenticated"])
82
98
  async def update_voice_model(
83
99
  request: Request,
84
100
  id: str,
85
101
  client: Optional[str] = None,
86
102
  ):
87
103
  user = request.user.object
104
+ subscribed = has_required_scope(request, ["premium"])
105
+
106
+ # Validate if model can be switched
107
+ voice_model = await VoiceModelOption.objects.filter(id=int(id)).afirst()
108
+ if voice_model is None:
109
+ return Response(status_code=404, content=json.dumps({"status": "error", "message": "Voice model not found"}))
110
+ if not subscribed and voice_model.price_tier != PriceTier.FREE:
111
+ raise Response(
112
+ status_code=403,
113
+ content=json.dumps({"status": "error", "message": "Subscribe to switch to this voice model"}),
114
+ )
88
115
 
89
116
  new_config = await ConversationAdapters.aset_user_voice_model(user, id)
90
117
 
@@ -111,8 +138,15 @@ async def update_paint_model(
111
138
  user = request.user.object
112
139
  subscribed = has_required_scope(request, ["premium"])
113
140
 
114
- if not subscribed:
115
- raise HTTPException(status_code=403, detail="User is not subscribed to premium")
141
+ # Validate if model can be switched
142
+ image_model = await TextToImageModelConfig.objects.filter(id=int(id)).afirst()
143
+ if image_model is None:
144
+ return Response(status_code=404, content=json.dumps({"status": "error", "message": "Image model not found"}))
145
+ if not subscribed and image_model.price_tier != PriceTier.FREE:
146
+ raise Response(
147
+ status_code=403,
148
+ content=json.dumps({"status": "error", "message": "Subscribe to switch to this image model"}),
149
+ )
116
150
 
117
151
  new_config = await ConversationAdapters.aset_user_text_to_image_model(user, int(id))
118
152
 
khoj/routers/helpers.py CHANGED
@@ -480,7 +480,7 @@ async def infer_webpage_urls(
480
480
  username = prompts.user_name.format(name=user.get_full_name()) if user.get_full_name() else ""
481
481
  chat_history = construct_chat_history(conversation_history)
482
482
 
483
- utc_date = datetime.utcnow().strftime("%Y-%m-%d")
483
+ utc_date = datetime.now(timezone.utc).strftime("%Y-%m-%d")
484
484
  personality_context = (
485
485
  prompts.personality_context.format(personality=agent.personality) if agent and agent.personality else ""
486
486
  )
@@ -545,7 +545,7 @@ async def generate_online_subqueries(
545
545
  chat_history = construct_chat_history(conversation_history)
546
546
 
547
547
  max_queries = 3
548
- utc_date = datetime.utcnow().strftime("%Y-%m-%d")
548
+ utc_date = datetime.now(timezone.utc).strftime("%Y-%m-%d")
549
549
  personality_context = (
550
550
  prompts.personality_context.format(personality=agent.personality) if agent and agent.personality else ""
551
551
  )
@@ -1290,6 +1290,7 @@ async def send_message_to_model_wrapper(
1290
1290
  model=chat_model_name,
1291
1291
  response_type=response_type,
1292
1292
  response_schema=response_schema,
1293
+ deepthought=deepthought,
1293
1294
  api_base_url=api_base_url,
1294
1295
  tracer=tracer,
1295
1296
  )
@@ -1593,6 +1594,7 @@ def generate_chat_response(
1593
1594
  generated_files=raw_generated_files,
1594
1595
  generated_asset_results=generated_asset_results,
1595
1596
  program_execution_context=program_execution_context,
1597
+ deepthought=deepthought,
1596
1598
  tracer=tracer,
1597
1599
  )
1598
1600
 
@@ -2362,6 +2364,7 @@ def get_user_config(user: KhojUser, request: Request, is_detailed: bool = False)
2362
2364
  "id": chat_model.id,
2363
2365
  "strengths": chat_model.strengths,
2364
2366
  "description": chat_model.description,
2367
+ "tier": chat_model.price_tier,
2365
2368
  }
2366
2369
  )
2367
2370
 
@@ -2369,12 +2372,24 @@ def get_user_config(user: KhojUser, request: Request, is_detailed: bool = False)
2369
2372
  paint_model_options = ConversationAdapters.get_text_to_image_model_options().all()
2370
2373
  all_paint_model_options = list()
2371
2374
  for paint_model in paint_model_options:
2372
- all_paint_model_options.append({"name": paint_model.model_name, "id": paint_model.id})
2375
+ all_paint_model_options.append(
2376
+ {
2377
+ "name": paint_model.model_name,
2378
+ "id": paint_model.id,
2379
+ "tier": paint_model.price_tier,
2380
+ }
2381
+ )
2373
2382
 
2374
2383
  voice_models = ConversationAdapters.get_voice_model_options()
2375
2384
  voice_model_options = list()
2376
2385
  for voice_model in voice_models:
2377
- voice_model_options.append({"name": voice_model.name, "id": voice_model.model_id})
2386
+ voice_model_options.append(
2387
+ {
2388
+ "name": voice_model.name,
2389
+ "id": voice_model.model_id,
2390
+ "tier": voice_model.price_tier,
2391
+ }
2392
+ )
2378
2393
 
2379
2394
  if len(voice_model_options) == 0:
2380
2395
  eleven_labs_enabled = False
khoj/utils/constants.py CHANGED
@@ -39,14 +39,18 @@ model_to_cost: Dict[str, Dict[str, float]] = {
39
39
  "gpt-4o": {"input": 2.50, "output": 10.00},
40
40
  "gpt-4o-mini": {"input": 0.15, "output": 0.60},
41
41
  "o1": {"input": 15.0, "output": 60.00},
42
+ "o3": {"input": 10.0, "output": 40.00},
42
43
  "o1-mini": {"input": 3.0, "output": 12.0},
43
44
  "o3-mini": {"input": 1.10, "output": 4.40},
45
+ "o4-mini": {"input": 1.10, "output": 4.40},
44
46
  # Gemini Pricing: https://ai.google.dev/pricing
45
47
  "gemini-1.5-flash": {"input": 0.075, "output": 0.30},
46
48
  "gemini-1.5-flash-002": {"input": 0.075, "output": 0.30},
47
49
  "gemini-1.5-pro": {"input": 1.25, "output": 5.00},
48
50
  "gemini-1.5-pro-002": {"input": 1.25, "output": 5.00},
49
51
  "gemini-2.0-flash": {"input": 0.10, "output": 0.40},
52
+ "gemini-2.5-flash-preview-04-17": {"input": 0.15, "output": 0.60, "thought": 3.50},
53
+ "gemini-2.5-pro-preview-03-25": {"input": 1.25, "output": 10.0},
50
54
  # Anthropic Pricing: https://www.anthropic.com/pricing#anthropic-api
51
55
  "claude-3-5-haiku-20241022": {"input": 1.0, "output": 5.0, "cache_read": 0.08, "cache_write": 1.0},
52
56
  "claude-3-5-haiku@20241022": {"input": 1.0, "output": 5.0, "cache_read": 0.08, "cache_write": 1.0},
@@ -55,4 +59,9 @@ model_to_cost: Dict[str, Dict[str, float]] = {
55
59
  "claude-3-7-sonnet-20250219": {"input": 3.0, "output": 15.0, "cache_read": 0.3, "cache_write": 3.75},
56
60
  "claude-3-7-sonnet@20250219": {"input": 3.0, "output": 15.0, "cache_read": 0.3, "cache_write": 3.75},
57
61
  "claude-3-7-sonnet-latest": {"input": 3.0, "output": 15.0, "cache_read": 0.3, "cache_write": 3.75},
62
+ # Grok pricing: https://docs.x.ai/docs/models
63
+ "grok-3": {"input": 3.0, "output": 15.0},
64
+ "grok-3-latest": {"input": 3.0, "output": 15.0},
65
+ "grok-3-mini": {"input": 0.30, "output": 0.50},
66
+ "grok-3-mini-latest": {"input": 0.30, "output": 0.50},
58
67
  }
khoj/utils/helpers.py CHANGED
@@ -601,6 +601,7 @@ def get_cost_of_chat_message(
601
601
  model_name: str,
602
602
  input_tokens: int = 0,
603
603
  output_tokens: int = 0,
604
+ thought_tokens: int = 0,
604
605
  cache_read_tokens: int = 0,
605
606
  cache_write_tokens: int = 0,
606
607
  prev_cost: float = 0.0,
@@ -612,10 +613,11 @@ def get_cost_of_chat_message(
612
613
  # Calculate cost of input and output tokens. Costs are per million tokens
613
614
  input_cost = constants.model_to_cost.get(model_name, {}).get("input", 0) * (input_tokens / 1e6)
614
615
  output_cost = constants.model_to_cost.get(model_name, {}).get("output", 0) * (output_tokens / 1e6)
616
+ thought_cost = constants.model_to_cost.get(model_name, {}).get("thought", 0) * (thought_tokens / 1e6)
615
617
  cache_read_cost = constants.model_to_cost.get(model_name, {}).get("cache_read", 0) * (cache_read_tokens / 1e6)
616
618
  cache_write_cost = constants.model_to_cost.get(model_name, {}).get("cache_write", 0) * (cache_write_tokens / 1e6)
617
619
 
618
- return input_cost + output_cost + cache_read_cost + cache_write_cost + prev_cost
620
+ return input_cost + output_cost + thought_cost + cache_read_cost + cache_write_cost + prev_cost
619
621
 
620
622
 
621
623
  def get_chat_usage_metrics(
@@ -624,6 +626,7 @@ def get_chat_usage_metrics(
624
626
  output_tokens: int = 0,
625
627
  cache_read_tokens: int = 0,
626
628
  cache_write_tokens: int = 0,
629
+ thought_tokens: int = 0,
627
630
  usage: dict = {},
628
631
  cost: float = None,
629
632
  ):
@@ -633,6 +636,7 @@ def get_chat_usage_metrics(
633
636
  prev_usage = usage or {
634
637
  "input_tokens": 0,
635
638
  "output_tokens": 0,
639
+ "thought_tokens": 0,
636
640
  "cache_read_tokens": 0,
637
641
  "cache_write_tokens": 0,
638
642
  "cost": 0.0,
@@ -640,11 +644,18 @@ def get_chat_usage_metrics(
640
644
  return {
641
645
  "input_tokens": prev_usage["input_tokens"] + input_tokens,
642
646
  "output_tokens": prev_usage["output_tokens"] + output_tokens,
647
+ "thought_tokens": prev_usage.get("thought_tokens", 0) + thought_tokens,
643
648
  "cache_read_tokens": prev_usage.get("cache_read_tokens", 0) + cache_read_tokens,
644
649
  "cache_write_tokens": prev_usage.get("cache_write_tokens", 0) + cache_write_tokens,
645
650
  "cost": cost
646
651
  or get_cost_of_chat_message(
647
- model_name, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, prev_cost=prev_usage["cost"]
652
+ model_name,
653
+ input_tokens,
654
+ output_tokens,
655
+ thought_tokens,
656
+ cache_read_tokens,
657
+ cache_write_tokens,
658
+ prev_cost=prev_usage["cost"],
648
659
  ),
649
660
  }
650
661
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: khoj
3
- Version: 1.40.1.dev1
3
+ Version: 1.40.1.dev15
4
4
  Summary: Your Second Brain
5
5
  Project-URL: Homepage, https://khoj.dev
6
6
  Project-URL: Documentation, https://docs.khoj.dev
@@ -40,7 +40,7 @@ Requires-Dist: einops==0.8.0
40
40
  Requires-Dist: email-validator==2.2.0
41
41
  Requires-Dist: fastapi>=0.110.0
42
42
  Requires-Dist: google-auth~=2.23.3
43
- Requires-Dist: google-genai==1.5.0
43
+ Requires-Dist: google-genai==1.11.0
44
44
  Requires-Dist: httpx==0.28.1
45
45
  Requires-Dist: huggingface-hub>=0.22.2
46
46
  Requires-Dist: itsdangerous==2.1.2
@@ -150,7 +150,7 @@ Description-Content-Type: text/markdown
150
150
 
151
151
  [Khoj](https://khoj.dev) is a personal AI app to extend your capabilities. It smoothly scales up from an on-device personal AI to a cloud-scale enterprise AI.
152
152
 
153
- - Chat with any local or online LLM (e.g llama3, qwen, gemma, mistral, gpt, claude, gemini).
153
+ - Chat with any local or online LLM (e.g llama3, qwen, gemma, mistral, gpt, claude, gemini, deepseek).
154
154
  - Get answers from the internet and your docs (including image, pdf, markdown, org-mode, word, notion files).
155
155
  - Access it from your Browser, Obsidian, Emacs, Desktop, Phone or Whatsapp.
156
156
  - Create agents with custom knowledge, persona, chat model and tools to take on any role.
@@ -179,6 +179,19 @@ To get started with self-hosting Khoj, [read the docs](https://docs.khoj.dev/get
179
179
 
180
180
  Khoj is available as a cloud service, on-premises, or as a hybrid solution. To learn more about Khoj Enterprise, [visit our website](https://khoj.dev/teams).
181
181
 
182
+ ## Frequently Asked Questions (FAQ)
183
+
184
+ Q: Can I use Khoj without self-hosting?
185
+ Yes! You can use Khoj right away at [https://app.khoj.dev](https://app.khoj.dev) — no setup required.
186
+
187
+ Q: What kinds of documents can Khoj read?
188
+ Khoj supports a wide variety: PDFs, Markdown, Notion, Word docs, org-mode files, and more.
189
+
190
+ Q: How can I make my own agent?
191
+ Check out [this blog post](https://blog.khoj.dev/posts/create-agents-on-khoj/) for a step-by-step guide to custom agents.
192
+ For more questions, head over to our [Discord](https://discord.gg/BDgyabRM6e)!
193
+
194
+
182
195
  ## Contributors
183
196
  Cheers to our awesome contributors! 🎉
184
197