Rename OpenAIProcessorConversationConfig DB model to more apt AiModelApi (#998)

* Rename OpenAIProcessorConversationConfig to more apt AiModelAPI

The DB model name had drifted from what it is being used for,
a general chat api provider that supports other chat api providers like
anthropic and google chat models apart from openai based chat models.

This change renames the DB model and updates the docs to remove this
confusion.

Using Ai Model Api we catch most use-cases including chat, stt, image generation etc.
This commit is contained in:
Debanjum 2024-12-08 18:02:29 -08:00 committed by GitHub
parent df66fb23ab
commit 9dd3782f5c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 108 additions and 105 deletions

View file

@ -38,14 +38,13 @@ To add a server chat setting:
- The `Advanced` field doesn't need to be set when self-hosting. When unset, the `Default` chat model is used for all users and the intermediate steps. - The `Advanced` field doesn't need to be set when self-hosting. When unset, the `Default` chat model is used for all users and the intermediate steps.
### OpenAI Processor Conversation Configs ### AI Model API
These settings configure chat model providers to be accessed over API. These settings configure APIs to interact with AI models.
The name of this setting is kind of a misnomer, we know, it'll hopefully be changed at some point. For each AI Model API you [add](http://localhost:42110/server/admin/database/aimodelapi/add):
For each chat model provider you [add](http://localhost:42110/server/admin/database/openaiprocessorconversationconfig/add):
- `Api key`: Set to your [OpenAI](https://platform.openai.com/api-keys), [Anthropic](https://console.anthropic.com/account/keys) or [Gemini](https://aistudio.google.com/app/apikey) API keys. - `Api key`: Set to your [OpenAI](https://platform.openai.com/api-keys), [Anthropic](https://console.anthropic.com/account/keys) or [Gemini](https://aistudio.google.com/app/apikey) API keys.
- `Name`: Give the configuration any friendly name like `OpenAI`, `Gemini`, `Anthropic`. - `Name`: Give the configuration any friendly name like `OpenAI`, `Gemini`, `Anthropic`.
- `Api base url`: Set the API base URL. This is only relevant to set if you're using another OpenAI-compatible proxy server like [Ollama](/advanced/ollama) or [LMStudio](/advanced/lmstudio). - `Api base url`: Set the API base URL. This is only relevant to set if you're using another OpenAI-compatible proxy server like [Ollama](/advanced/ollama) or [LMStudio](/advanced/lmstudio).
![example configuration for openai processor](/img/example_openai_processor_config.png) ![example configuration for ai model api](/img/example_openai_processor_config.png)
### Search Model Configs ### Search Model Configs
Search models are used to generate vector embeddings of your documents for natural language search and chat. You can choose any [embeddings models on HuggingFace](https://huggingface.co/models?pipeline_tag=sentence-similarity) to try, use for your to create vector embeddings of your documents for natural language search and chat. Search models are used to generate vector embeddings of your documents for natural language search and chat. You can choose any [embeddings models on HuggingFace](https://huggingface.co/models?pipeline_tag=sentence-similarity) to try, use for your to create vector embeddings of your documents for natural language search and chat.

View file

@ -21,7 +21,7 @@ Using LiteLLM with Khoj makes it possible to turn any LLM behind an API into you
export MISTRAL_API_KEY=<MISTRAL_API_KEY> export MISTRAL_API_KEY=<MISTRAL_API_KEY>
litellm --model mistral/mistral-tiny --drop_params litellm --model mistral/mistral-tiny --drop_params
``` ```
3. Create a new [OpenAI Processor Conversation Config](http://localhost:42110/server/admin/database/openaiprocessorconversationconfig/add) on your Khoj admin panel 3. Create a new [API Model API](http://localhost:42110/server/admin/database/aimodelapi/add) on your Khoj admin panel
- Name: `proxy-name` - Name: `proxy-name`
- Api Key: `any string` - Api Key: `any string`
- Api Base Url: **URL of your Openai Proxy API** - Api Base Url: **URL of your Openai Proxy API**

View file

@ -60,7 +60,7 @@ Restart your Khoj server after first run or update to the settings below to ensu
```bash ```bash
ollama pull llama3.1 ollama pull llama3.1
``` ```
3. Create a new [OpenAI Processor Conversation Config](http://localhost:42110/server/admin/database/openaiprocessorconversationconfig/add) on your Khoj admin panel 3. Create a new [AI Model API](http://localhost:42110/server/admin/database/aimodelapi/add) on your Khoj admin panel
- Name: `ollama` - Name: `ollama`
- Api Key: `any string` - Api Key: `any string`
- Api Base Url: `http://localhost:11434/v1/` (default for Ollama) - Api Base Url: `http://localhost:11434/v1/` (default for Ollama)

View file

@ -11,7 +11,7 @@ This is only helpful for self-hosted users. If you're using [Khoj Cloud](https:/
Khoj natively supports local LLMs [available on HuggingFace in GGUF format](https://huggingface.co/models?library=gguf). Using an OpenAI API proxy with Khoj maybe useful for ease of setup, trying new models or using commercial LLMs via API. Khoj natively supports local LLMs [available on HuggingFace in GGUF format](https://huggingface.co/models?library=gguf). Using an OpenAI API proxy with Khoj maybe useful for ease of setup, trying new models or using commercial LLMs via API.
::: :::
Khoj can use any OpenAI API compatible server including [Ollama](/advanced/ollama), [LMStudio](/advanced/lmstudio) and [LiteLLM](/advanced/litellm). Khoj can use any OpenAI API compatible server including local providers like [Ollama](/advanced/ollama), [LMStudio](/advanced/lmstudio) and [LiteLLM](/advanced/litellm) and commercial providers like [HuggingFace](https://huggingface.co/docs/api-inference/tasks/chat-completion#using-the-api), [OpenRouter](https://openrouter.ai/docs/quick-start) etc.
Configuring this allows you to use non-standard, open or commercial, local or hosted LLM models for Khoj Configuring this allows you to use non-standard, open or commercial, local or hosted LLM models for Khoj
Combine them with Khoj can turn your favorite LLM into an AI agent. Allowing you to chat with your docs, find answers from the internet, build custom agents and run automations. Combine them with Khoj can turn your favorite LLM into an AI agent. Allowing you to chat with your docs, find answers from the internet, build custom agents and run automations.
@ -20,8 +20,8 @@ For specific integrations, see our [Ollama](/advanced/ollama), [LMStudio](/advan
## General Setup ## General Setup
1. Start your preferred OpenAI API compatible app 1. Start your preferred OpenAI API compatible app locally or get API keys from commercial AI model providers.
2. Create a new [OpenAI Processor Conversation Config](http://localhost:42110/server/admin/database/openaiprocessorconversationconfig/add) on your Khoj admin panel 3. Create a new [API Model API](http://localhost:42110/server/admin/database/aimodelapi/add) on your Khoj admin panel
- Name: `any name` - Name: `any name`
- Api Key: `any string` - Api Key: `any string`
- Api Base Url: **URL of your Openai Proxy API** - Api Base Url: **URL of your Openai Proxy API**

View file

@ -302,11 +302,11 @@ Setup which chat model you'd want to use. Khoj supports local and online chat mo
Using Ollama? See the [Ollama Integration](/advanced/ollama) section for more custom setup instructions. Using Ollama? See the [Ollama Integration](/advanced/ollama) section for more custom setup instructions.
::: :::
1. Create a new [OpenAI processor conversation config](http://localhost:42110/server/admin/database/openaiprocessorconversationconfig/add) in the server admin settings. This is kind of a misnomer, we know. 1. Create a new [AI Model Api](http://localhost:42110/server/admin/database/aimodelapi/add) in the server admin settings.
- Add your [OpenAI API key](https://platform.openai.com/api-keys) - Add your [OpenAI API key](https://platform.openai.com/api-keys)
- Give the configuration a friendly name like `OpenAI` - Give the configuration a friendly name like `OpenAI`
- (Optional) Set the API base URL. It is only relevant if you're using another OpenAI-compatible proxy server like [Ollama](/advanced/ollama) or [LMStudio](/advanced/lmstudio).<br /> - (Optional) Set the API base URL. It is only relevant if you're using another OpenAI-compatible proxy server like [Ollama](/advanced/ollama) or [LMStudio](/advanced/lmstudio).<br />
![example configuration for openai processor](/img/example_openai_processor_config.png) ![example configuration for ai model api](/img/example_openai_processor_config.png)
2. Create a new [chat model options](http://localhost:42110/server/admin/database/chatmodeloptions/add) 2. Create a new [chat model options](http://localhost:42110/server/admin/database/chatmodeloptions/add)
- Set the `chat-model` field to an [OpenAI chat model](https://platform.openai.com/docs/models). Example: `gpt-4o`. - Set the `chat-model` field to an [OpenAI chat model](https://platform.openai.com/docs/models). Example: `gpt-4o`.
- Make sure to set the `model-type` field to `OpenAI`. - Make sure to set the `model-type` field to `OpenAI`.
@ -315,22 +315,22 @@ Using Ollama? See the [Ollama Integration](/advanced/ollama) section for more cu
![example configuration for chat model options](/img/example_chatmodel_option.png) ![example configuration for chat model options](/img/example_chatmodel_option.png)
</TabItem> </TabItem>
<TabItem value="anthropic" label="Anthropic"> <TabItem value="anthropic" label="Anthropic">
1. Create a new [OpenAI processor conversation config](http://localhost:42110/server/admin/database/openaiprocessorconversationconfig/add) in the server admin settings. This is kind of a misnomer, we know. 1. Create a new [AI Model API](http://localhost:42110/server/admin/database/aimodelapi/add) in the server admin settings.
- Add your [Anthropic API key](https://console.anthropic.com/account/keys) - Add your [Anthropic API key](https://console.anthropic.com/account/keys)
- Give the configuration a friendly name like `Anthropic`. Do not configure the API base url. - Give the configuration a friendly name like `Anthropic`. Do not configure the API base url.
2. Create a new [chat model options](http://localhost:42110/server/admin/database/chatmodeloptions/add) 2. Create a new [chat model options](http://localhost:42110/server/admin/database/chatmodeloptions/add)
- Set the `chat-model` field to an [Anthropic chat model](https://docs.anthropic.com/en/docs/about-claude/models#model-names). Example: `claude-3-5-sonnet-20240620`. - Set the `chat-model` field to an [Anthropic chat model](https://docs.anthropic.com/en/docs/about-claude/models#model-names). Example: `claude-3-5-sonnet-20240620`.
- Set the `model-type` field to `Anthropic`. - Set the `model-type` field to `Anthropic`.
- Set the `Openai config` field to the OpenAI processor conversation config for Anthropic you created in step 1. - Set the `ai model api` field to the Anthropic AI Model API you created in step 1.
</TabItem> </TabItem>
<TabItem value="gemini" label="Gemini"> <TabItem value="gemini" label="Gemini">
1. Create a new [OpenAI processor conversation config](http://localhost:42110/server/admin/database/openaiprocessorconversationconfig/add) in the server admin settings. This is kind of a misnomer, we know. 1. Create a new [AI Model API](http://localhost:42110/server/admin/database/aimodelapi/add) in the server admin settings.
- Add your [Gemini API key](https://aistudio.google.com/app/apikey) - Add your [Gemini API key](https://aistudio.google.com/app/apikey)
- Give the configuration a friendly name like `Gemini`. Do not configure the API base url. - Give the configuration a friendly name like `Gemini`. Do not configure the API base url.
2. Create a new [chat model options](http://localhost:42110/server/admin/database/chatmodeloptions/add) 2. Create a new [chat model options](http://localhost:42110/server/admin/database/chatmodeloptions/add)
- Set the `chat-model` field to a [Google Gemini chat model](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-models). Example: `gemini-1.5-flash`. - Set the `chat-model` field to a [Google Gemini chat model](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-models). Example: `gemini-1.5-flash`.
- Set the `model-type` field to `Gemini`. - Set the `model-type` field to `Gemini`.
- Set the `Openai config` field to the OpenAI processor conversation config for Gemini you created in step 1. - Set the `ai model api` field to the Gemini AI Model API you created in step 1.
</TabItem> </TabItem>
<TabItem value="offline" label="Offline"> <TabItem value="offline" label="Offline">

View file

@ -232,9 +232,9 @@ def configure_server(
config = FullConfig() config = FullConfig()
state.config = config state.config = config
if ConversationAdapters.has_valid_openai_conversation_config(): if ConversationAdapters.has_valid_ai_model_api():
openai_config = ConversationAdapters.get_openai_conversation_config() ai_model_api = ConversationAdapters.get_ai_model_api()
state.openai_client = openai.OpenAI(api_key=openai_config.api_key) state.openai_client = openai.OpenAI(api_key=ai_model_api.api_key)
# Initialize Search Models from Config and initialize content # Initialize Search Models from Config and initialize content
try: try:

View file

@ -35,6 +35,7 @@ from torch import Tensor
from khoj.database.models import ( from khoj.database.models import (
Agent, Agent,
AiModelApi,
ChatModelOptions, ChatModelOptions,
ClientApplication, ClientApplication,
Conversation, Conversation,
@ -46,7 +47,6 @@ from khoj.database.models import (
KhojApiUser, KhojApiUser,
KhojUser, KhojUser,
NotionConfig, NotionConfig,
OpenAIProcessorConversationConfig,
ProcessLock, ProcessLock,
PublicConversation, PublicConversation,
ReflectiveQuestion, ReflectiveQuestion,
@ -981,7 +981,7 @@ class ConversationAdapters:
@staticmethod @staticmethod
async def aget_all_conversation_configs(): async def aget_all_conversation_configs():
return await sync_to_async(list)(ChatModelOptions.objects.prefetch_related("openai_config").all()) return await sync_to_async(list)(ChatModelOptions.objects.prefetch_related("ai_model_api").all())
@staticmethod @staticmethod
def get_vision_enabled_config(): def get_vision_enabled_config():
@ -1000,12 +1000,12 @@ class ConversationAdapters:
return None return None
@staticmethod @staticmethod
def get_openai_conversation_config(): def get_ai_model_api():
return OpenAIProcessorConversationConfig.objects.filter().first() return AiModelApi.objects.filter().first()
@staticmethod @staticmethod
def has_valid_openai_conversation_config(): def has_valid_ai_model_api():
return OpenAIProcessorConversationConfig.objects.filter().exists() return AiModelApi.objects.filter().exists()
@staticmethod @staticmethod
@arequire_valid_user @arequire_valid_user
@ -1093,7 +1093,7 @@ class ConversationAdapters:
server_chat_settings: ServerChatSettings = ( server_chat_settings: ServerChatSettings = (
await ServerChatSettings.objects.filter() await ServerChatSettings.objects.filter()
.prefetch_related( .prefetch_related(
"chat_default", "chat_default__openai_config", "chat_advanced", "chat_advanced__openai_config" "chat_default", "chat_default__ai_model_api", "chat_advanced", "chat_advanced__ai_model_api"
) )
.afirst() .afirst()
) )
@ -1109,7 +1109,7 @@ class ConversationAdapters:
# Get the user's chat settings, if the server chat settings are not set # Get the user's chat settings, if the server chat settings are not set
user_chat_settings = ( user_chat_settings = (
(await UserConversationConfig.objects.filter(user=user).prefetch_related("setting__openai_config").afirst()) (await UserConversationConfig.objects.filter(user=user).prefetch_related("setting__ai_model_api").afirst())
if user if user
else None else None
) )
@ -1117,7 +1117,7 @@ class ConversationAdapters:
return user_chat_settings.setting return user_chat_settings.setting
# Get the first chat model if even the user chat settings are not set # Get the first chat model if even the user chat settings are not set
return await ChatModelOptions.objects.filter().prefetch_related("openai_config").afirst() return await ChatModelOptions.objects.filter().prefetch_related("ai_model_api").afirst()
@staticmethod @staticmethod
def get_advanced_conversation_config(user: KhojUser): def get_advanced_conversation_config(user: KhojUser):
@ -1130,7 +1130,7 @@ class ConversationAdapters:
async def aget_advanced_conversation_config(user: KhojUser = None): async def aget_advanced_conversation_config(user: KhojUser = None):
server_chat_settings: ServerChatSettings = ( server_chat_settings: ServerChatSettings = (
await ServerChatSettings.objects.filter() await ServerChatSettings.objects.filter()
.prefetch_related("chat_advanced", "chat_advanced__openai_config") .prefetch_related("chat_advanced", "chat_advanced__ai_model_api")
.afirst() .afirst()
) )
if server_chat_settings is not None and server_chat_settings.chat_advanced is not None: if server_chat_settings is not None and server_chat_settings.chat_advanced is not None:
@ -1258,7 +1258,7 @@ class ConversationAdapters:
@staticmethod @staticmethod
async def aget_user_conversation_config(user: KhojUser): async def aget_user_conversation_config(user: KhojUser):
config = ( config = (
await UserConversationConfig.objects.filter(user=user).prefetch_related("setting__openai_config").afirst() await UserConversationConfig.objects.filter(user=user).prefetch_related("setting__ai_model_api").afirst()
) )
if not config: if not config:
return None return None
@ -1313,7 +1313,7 @@ class ConversationAdapters:
ChatModelOptions.ModelType.OPENAI, ChatModelOptions.ModelType.OPENAI,
ChatModelOptions.ModelType.GOOGLE, ChatModelOptions.ModelType.GOOGLE,
] ]
) and conversation_config.openai_config: ) and conversation_config.ai_model_api:
return conversation_config return conversation_config
else: else:
@ -1321,7 +1321,7 @@ class ConversationAdapters:
@staticmethod @staticmethod
async def aget_text_to_image_model_config(): async def aget_text_to_image_model_config():
return await TextToImageModelConfig.objects.filter().prefetch_related("openai_config").afirst() return await TextToImageModelConfig.objects.filter().prefetch_related("ai_model_api").afirst()
@staticmethod @staticmethod
def get_text_to_image_model_config(): def get_text_to_image_model_config():
@ -1343,9 +1343,9 @@ class ConversationAdapters:
@staticmethod @staticmethod
async def aget_user_text_to_image_model(user: KhojUser) -> Optional[TextToImageModelConfig]: async def aget_user_text_to_image_model(user: KhojUser) -> Optional[TextToImageModelConfig]:
# Create a custom queryset for prefetching settings__openai_config, handling null cases # Create a custom queryset for prefetching settings__ai_model_api, handling null cases
settings_prefetch = Prefetch( settings_prefetch = Prefetch(
"setting", queryset=TextToImageModelConfig.objects.prefetch_related("openai_config") "setting", queryset=TextToImageModelConfig.objects.prefetch_related("ai_model_api")
) )
config = await UserTextToImageModelConfig.objects.filter(user=user).prefetch_related(settings_prefetch).afirst() config = await UserTextToImageModelConfig.objects.filter(user=user).prefetch_related(settings_prefetch).afirst()

View file

@ -1,6 +1,6 @@
import csv import csv
import json import json
from datetime import date, datetime, timedelta, timezone from datetime import datetime, timedelta
from apscheduler.job import Job from apscheduler.job import Job
from django.contrib import admin, messages from django.contrib import admin, messages
@ -15,6 +15,7 @@ from unfold import admin as unfold_admin
from khoj.database.models import ( from khoj.database.models import (
Agent, Agent,
AiModelApi,
ChatModelOptions, ChatModelOptions,
ClientApplication, ClientApplication,
Conversation, Conversation,
@ -22,7 +23,6 @@ from khoj.database.models import (
GithubConfig, GithubConfig,
KhojUser, KhojUser,
NotionConfig, NotionConfig,
OpenAIProcessorConversationConfig,
ProcessLock, ProcessLock,
ReflectiveQuestion, ReflectiveQuestion,
SearchModelConfig, SearchModelConfig,
@ -232,8 +232,8 @@ class TextToImageModelOptionsAdmin(unfold_admin.ModelAdmin):
search_fields = ("id", "model_name", "model_type") search_fields = ("id", "model_name", "model_type")
@admin.register(OpenAIProcessorConversationConfig) @admin.register(AiModelApi)
class OpenAIProcessorConversationConfigAdmin(unfold_admin.ModelAdmin): class AiModelApiAdmin(unfold_admin.ModelAdmin):
list_display = ( list_display = (
"id", "id",
"name", "name",

View file

@ -0,0 +1,26 @@
# Generated by Django 5.0.9 on 2024-12-05 09:00
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("database", "0075_migrate_generated_assets_and_validate"),
]
operations = [
migrations.RenameModel(
old_name="OpenAIProcessorConversationConfig",
new_name="AiModelApi",
),
migrations.RenameField(
model_name="chatmodeloptions",
old_name="openai_config",
new_name="ai_model_api",
),
migrations.RenameField(
model_name="texttoimagemodelconfig",
old_name="openai_config",
new_name="ai_model_api",
),
]

View file

@ -181,7 +181,7 @@ class Subscription(DbBaseModel):
enabled_trial_at = models.DateTimeField(null=True, default=None, blank=True) enabled_trial_at = models.DateTimeField(null=True, default=None, blank=True)
class OpenAIProcessorConversationConfig(DbBaseModel): class AiModelApi(DbBaseModel):
name = models.CharField(max_length=200) name = models.CharField(max_length=200)
api_key = models.CharField(max_length=200) api_key = models.CharField(max_length=200)
api_base_url = models.URLField(max_length=200, default=None, blank=True, null=True) api_base_url = models.URLField(max_length=200, default=None, blank=True, null=True)
@ -200,9 +200,7 @@ class ChatModelOptions(DbBaseModel):
chat_model = models.CharField(max_length=200, default="bartowski/Meta-Llama-3.1-8B-Instruct-GGUF") chat_model = models.CharField(max_length=200, default="bartowski/Meta-Llama-3.1-8B-Instruct-GGUF")
model_type = models.CharField(max_length=200, choices=ModelType.choices, default=ModelType.OFFLINE) model_type = models.CharField(max_length=200, choices=ModelType.choices, default=ModelType.OFFLINE)
vision_enabled = models.BooleanField(default=False) vision_enabled = models.BooleanField(default=False)
openai_config = models.ForeignKey( ai_model_api = models.ForeignKey(AiModelApi, on_delete=models.CASCADE, default=None, null=True, blank=True)
OpenAIProcessorConversationConfig, on_delete=models.CASCADE, default=None, null=True, blank=True
)
class VoiceModelOption(DbBaseModel): class VoiceModelOption(DbBaseModel):
@ -504,26 +502,24 @@ class TextToImageModelConfig(DbBaseModel):
model_name = models.CharField(max_length=200, default="dall-e-3") model_name = models.CharField(max_length=200, default="dall-e-3")
model_type = models.CharField(max_length=200, choices=ModelType.choices, default=ModelType.OPENAI) model_type = models.CharField(max_length=200, choices=ModelType.choices, default=ModelType.OPENAI)
api_key = models.CharField(max_length=200, default=None, null=True, blank=True) api_key = models.CharField(max_length=200, default=None, null=True, blank=True)
openai_config = models.ForeignKey( ai_model_api = models.ForeignKey(AiModelApi, on_delete=models.CASCADE, default=None, null=True, blank=True)
OpenAIProcessorConversationConfig, on_delete=models.CASCADE, default=None, null=True, blank=True
)
def clean(self): def clean(self):
# Custom validation logic # Custom validation logic
error = {} error = {}
if self.model_type == self.ModelType.OPENAI: if self.model_type == self.ModelType.OPENAI:
if self.api_key and self.openai_config: if self.api_key and self.ai_model_api:
error[ error[
"api_key" "api_key"
] = "Both API key and OpenAI config cannot be set for OpenAI models. Please set only one of them." ] = "Both API key and AI Model API cannot be set for OpenAI models. Please set only one of them."
error[ error[
"openai_config" "ai_model_api"
] = "Both API key and OpenAI config cannot be set for OpenAI models. Please set only one of them." ] = "Both API key and OpenAI config cannot be set for OpenAI models. Please set only one of them."
if self.model_type != self.ModelType.OPENAI: if self.model_type != self.ModelType.OPENAI:
if not self.api_key: if not self.api_key:
error["api_key"] = "The API key field must be set for non OpenAI models." error["api_key"] = "The API key field must be set for non OpenAI models."
if self.openai_config: if self.ai_model_api:
error["openai_config"] = "OpenAI config cannot be set for non OpenAI models." error["ai_model_api"] = "AI Model API cannot be set for non OpenAI models."
if error: if error:
raise ValidationError(error) raise ValidationError(error)

View file

@ -60,11 +60,7 @@ import logging
from packaging import version from packaging import version
from khoj.database.models import ( from khoj.database.models import AiModelApi, ChatModelOptions, SearchModelConfig
ChatModelOptions,
OpenAIProcessorConversationConfig,
SearchModelConfig,
)
from khoj.utils.yaml import load_config_from_file, save_config_to_file from khoj.utils.yaml import load_config_from_file, save_config_to_file
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -121,16 +117,14 @@ def migrate_server_pg(args):
if openai.get("chat-model") is None: if openai.get("chat-model") is None:
openai["chat-model"] = "gpt-3.5-turbo" openai["chat-model"] = "gpt-3.5-turbo"
openai_config = OpenAIProcessorConversationConfig.objects.create( openai_model_api = AiModelApi.objects.create(api_key=openai.get("api-key"), name="default")
api_key=openai.get("api-key"), name="default"
)
ChatModelOptions.objects.create( ChatModelOptions.objects.create(
chat_model=openai.get("chat-model"), chat_model=openai.get("chat-model"),
tokenizer=processor_conversation.get("tokenizer"), tokenizer=processor_conversation.get("tokenizer"),
max_prompt_size=processor_conversation.get("max-prompt-size"), max_prompt_size=processor_conversation.get("max-prompt-size"),
model_type=ChatModelOptions.ModelType.OPENAI, model_type=ChatModelOptions.ModelType.OPENAI,
openai_config=openai_config, ai_model_api=openai_model_api,
) )
save_config_to_file(raw_config, args.config_file) save_config_to_file(raw_config, args.config_file)

View file

@ -19,12 +19,7 @@ from khoj.processor.conversation.utils import (
ThreadedGenerator, ThreadedGenerator,
commit_conversation_trace, commit_conversation_trace,
) )
from khoj.utils import state from khoj.utils.helpers import get_chat_usage_metrics, is_promptrace_enabled
from khoj.utils.helpers import (
get_chat_usage_metrics,
in_debug_mode,
is_promptrace_enabled,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View file

@ -124,8 +124,8 @@ def generate_image_with_openai(
# Get the API key from the user's configuration # Get the API key from the user's configuration
if text_to_image_config.api_key: if text_to_image_config.api_key:
api_key = text_to_image_config.api_key api_key = text_to_image_config.api_key
elif text_to_image_config.openai_config: elif text_to_image_config.ai_model_api:
api_key = text_to_image_config.openai_config.api_key api_key = text_to_image_config.ai_model_api.api_key
elif state.openai_client: elif state.openai_client:
api_key = state.openai_client.api_key api_key = state.openai_client.api_key
auth_header = {"Authorization": f"Bearer {api_key}"} if api_key else {} auth_header = {"Authorization": f"Bearer {api_key}"} if api_key else {}

View file

@ -430,9 +430,8 @@ async def extract_references_and_questions(
tracer=tracer, tracer=tracer,
) )
elif conversation_config.model_type == ChatModelOptions.ModelType.OPENAI: elif conversation_config.model_type == ChatModelOptions.ModelType.OPENAI:
openai_chat_config = conversation_config.openai_config api_key = conversation_config.ai_model_api.api_key
api_key = openai_chat_config.api_key base_url = conversation_config.ai_model_api.api_base_url
base_url = openai_chat_config.api_base_url
chat_model = conversation_config.chat_model chat_model = conversation_config.chat_model
inferred_queries = extract_questions( inferred_queries = extract_questions(
defiltered_query, defiltered_query,
@ -449,7 +448,7 @@ async def extract_references_and_questions(
tracer=tracer, tracer=tracer,
) )
elif conversation_config.model_type == ChatModelOptions.ModelType.ANTHROPIC: elif conversation_config.model_type == ChatModelOptions.ModelType.ANTHROPIC:
api_key = conversation_config.openai_config.api_key api_key = conversation_config.ai_model_api.api_key
chat_model = conversation_config.chat_model chat_model = conversation_config.chat_model
inferred_queries = extract_questions_anthropic( inferred_queries = extract_questions_anthropic(
defiltered_query, defiltered_query,
@ -465,7 +464,7 @@ async def extract_references_and_questions(
tracer=tracer, tracer=tracer,
) )
elif conversation_config.model_type == ChatModelOptions.ModelType.GOOGLE: elif conversation_config.model_type == ChatModelOptions.ModelType.GOOGLE:
api_key = conversation_config.openai_config.api_key api_key = conversation_config.ai_model_api.api_key
chat_model = conversation_config.chat_model chat_model = conversation_config.chat_model
inferred_queries = extract_questions_gemini( inferred_queries = extract_questions_gemini(
defiltered_query, defiltered_query,

View file

@ -136,7 +136,7 @@ def validate_conversation_config(user: KhojUser):
if default_config is None: if default_config is None:
raise HTTPException(status_code=500, detail="Contact the server administrator to add a chat model.") raise HTTPException(status_code=500, detail="Contact the server administrator to add a chat model.")
if default_config.model_type == "openai" and not default_config.openai_config: if default_config.model_type == "openai" and not default_config.ai_model_api:
raise HTTPException(status_code=500, detail="Contact the server administrator to add a chat model.") raise HTTPException(status_code=500, detail="Contact the server administrator to add a chat model.")
@ -163,7 +163,7 @@ async def is_ready_to_chat(user: KhojUser):
ChatModelOptions.ModelType.GOOGLE, ChatModelOptions.ModelType.GOOGLE,
] ]
) )
and user_conversation_config.openai_config and user_conversation_config.ai_model_api
): ):
return True return True
@ -990,7 +990,7 @@ async def send_message_to_model_wrapper(
) )
elif model_type == ChatModelOptions.ModelType.OPENAI: elif model_type == ChatModelOptions.ModelType.OPENAI:
openai_chat_config = conversation_config.openai_config openai_chat_config = conversation_config.ai_model_api
api_key = openai_chat_config.api_key api_key = openai_chat_config.api_key
api_base_url = openai_chat_config.api_base_url api_base_url = openai_chat_config.api_base_url
truncated_messages = generate_chatml_messages_with_context( truncated_messages = generate_chatml_messages_with_context(
@ -1015,7 +1015,7 @@ async def send_message_to_model_wrapper(
tracer=tracer, tracer=tracer,
) )
elif model_type == ChatModelOptions.ModelType.ANTHROPIC: elif model_type == ChatModelOptions.ModelType.ANTHROPIC:
api_key = conversation_config.openai_config.api_key api_key = conversation_config.ai_model_api.api_key
truncated_messages = generate_chatml_messages_with_context( truncated_messages = generate_chatml_messages_with_context(
user_message=query, user_message=query,
context_message=context, context_message=context,
@ -1037,7 +1037,7 @@ async def send_message_to_model_wrapper(
tracer=tracer, tracer=tracer,
) )
elif model_type == ChatModelOptions.ModelType.GOOGLE: elif model_type == ChatModelOptions.ModelType.GOOGLE:
api_key = conversation_config.openai_config.api_key api_key = conversation_config.ai_model_api.api_key
truncated_messages = generate_chatml_messages_with_context( truncated_messages = generate_chatml_messages_with_context(
user_message=query, user_message=query,
context_message=context, context_message=context,
@ -1102,7 +1102,7 @@ def send_message_to_model_wrapper_sync(
) )
elif conversation_config.model_type == ChatModelOptions.ModelType.OPENAI: elif conversation_config.model_type == ChatModelOptions.ModelType.OPENAI:
api_key = conversation_config.openai_config.api_key api_key = conversation_config.ai_model_api.api_key
truncated_messages = generate_chatml_messages_with_context( truncated_messages = generate_chatml_messages_with_context(
user_message=message, user_message=message,
system_message=system_message, system_message=system_message,
@ -1124,7 +1124,7 @@ def send_message_to_model_wrapper_sync(
return openai_response return openai_response
elif conversation_config.model_type == ChatModelOptions.ModelType.ANTHROPIC: elif conversation_config.model_type == ChatModelOptions.ModelType.ANTHROPIC:
api_key = conversation_config.openai_config.api_key api_key = conversation_config.ai_model_api.api_key
truncated_messages = generate_chatml_messages_with_context( truncated_messages = generate_chatml_messages_with_context(
user_message=message, user_message=message,
system_message=system_message, system_message=system_message,
@ -1144,7 +1144,7 @@ def send_message_to_model_wrapper_sync(
) )
elif conversation_config.model_type == ChatModelOptions.ModelType.GOOGLE: elif conversation_config.model_type == ChatModelOptions.ModelType.GOOGLE:
api_key = conversation_config.openai_config.api_key api_key = conversation_config.ai_model_api.api_key
truncated_messages = generate_chatml_messages_with_context( truncated_messages = generate_chatml_messages_with_context(
user_message=message, user_message=message,
system_message=system_message, system_message=system_message,
@ -1255,7 +1255,7 @@ def generate_chat_response(
) )
elif conversation_config.model_type == ChatModelOptions.ModelType.OPENAI: elif conversation_config.model_type == ChatModelOptions.ModelType.OPENAI:
openai_chat_config = conversation_config.openai_config openai_chat_config = conversation_config.ai_model_api
api_key = openai_chat_config.api_key api_key = openai_chat_config.api_key
chat_model = conversation_config.chat_model chat_model = conversation_config.chat_model
chat_response = converse( chat_response = converse(
@ -1285,7 +1285,7 @@ def generate_chat_response(
) )
elif conversation_config.model_type == ChatModelOptions.ModelType.ANTHROPIC: elif conversation_config.model_type == ChatModelOptions.ModelType.ANTHROPIC:
api_key = conversation_config.openai_config.api_key api_key = conversation_config.ai_model_api.api_key
chat_response = converse_anthropic( chat_response = converse_anthropic(
compiled_references, compiled_references,
query_to_run, query_to_run,
@ -1311,7 +1311,7 @@ def generate_chat_response(
tracer=tracer, tracer=tracer,
) )
elif conversation_config.model_type == ChatModelOptions.ModelType.GOOGLE: elif conversation_config.model_type == ChatModelOptions.ModelType.GOOGLE:
api_key = conversation_config.openai_config.api_key api_key = conversation_config.ai_model_api.api_key
chat_response = converse_gemini( chat_response = converse_gemini(
compiled_references, compiled_references,
query_to_run, query_to_run,

View file

@ -6,9 +6,9 @@ import openai
from khoj.database.adapters import ConversationAdapters from khoj.database.adapters import ConversationAdapters
from khoj.database.models import ( from khoj.database.models import (
AiModelApi,
ChatModelOptions, ChatModelOptions,
KhojUser, KhojUser,
OpenAIProcessorConversationConfig,
SpeechToTextModelOptions, SpeechToTextModelOptions,
TextToImageModelConfig, TextToImageModelConfig,
) )
@ -98,7 +98,7 @@ def initialization(interactive: bool = True):
TextToImageModelConfig.objects.create( TextToImageModelConfig.objects.create(
model_name=openai_text_to_image_model, model_name=openai_text_to_image_model,
model_type=TextToImageModelConfig.ModelType.OPENAI, model_type=TextToImageModelConfig.ModelType.OPENAI,
openai_config=openai_provider, ai_model_api=openai_provider,
) )
# Set up Google's Gemini online chat models # Set up Google's Gemini online chat models
@ -177,7 +177,7 @@ def initialization(interactive: bool = True):
vision_enabled: bool = False, vision_enabled: bool = False,
is_offline: bool = False, is_offline: bool = False,
provider_name: str = None, provider_name: str = None,
) -> Tuple[bool, OpenAIProcessorConversationConfig]: ) -> Tuple[bool, AiModelApi]:
supported_vision_models = ( supported_vision_models = (
default_openai_chat_models + default_anthropic_chat_models + default_gemini_chat_models default_openai_chat_models + default_anthropic_chat_models + default_gemini_chat_models
) )
@ -192,16 +192,14 @@ def initialization(interactive: bool = True):
logger.info(f"️💬 Setting up your {provider_name} chat configuration") logger.info(f"️💬 Setting up your {provider_name} chat configuration")
chat_provider = None ai_model_api = None
if not is_offline: if not is_offline:
if interactive: if interactive:
user_api_key = input(f"Enter your {provider_name} API key (default: {default_api_key}): ") user_api_key = input(f"Enter your {provider_name} API key (default: {default_api_key}): ")
api_key = user_api_key if user_api_key != "" else default_api_key api_key = user_api_key if user_api_key != "" else default_api_key
else: else:
api_key = default_api_key api_key = default_api_key
chat_provider = OpenAIProcessorConversationConfig.objects.create( ai_model_api = AiModelApi.objects.create(api_key=api_key, name=provider_name, api_base_url=api_base_url)
api_key=api_key, name=provider_name, api_base_url=api_base_url
)
if interactive: if interactive:
chat_model_names = input( chat_model_names = input(
@ -223,19 +221,19 @@ def initialization(interactive: bool = True):
"max_prompt_size": default_max_tokens, "max_prompt_size": default_max_tokens,
"vision_enabled": vision_enabled, "vision_enabled": vision_enabled,
"tokenizer": default_tokenizer, "tokenizer": default_tokenizer,
"openai_config": chat_provider, "ai_model_api": ai_model_api,
} }
ChatModelOptions.objects.create(**chat_model_options) ChatModelOptions.objects.create(**chat_model_options)
logger.info(f"🗣️ {provider_name} chat model configuration complete") logger.info(f"🗣️ {provider_name} chat model configuration complete")
return True, chat_provider return True, ai_model_api
def _update_chat_model_options(): def _update_chat_model_options():
"""Update available chat models for OpenAI-compatible APIs""" """Update available chat models for OpenAI-compatible APIs"""
try: try:
# Get OpenAI configs with custom base URLs # Get OpenAI configs with custom base URLs
custom_configs = OpenAIProcessorConversationConfig.objects.exclude(api_base_url__isnull=True) custom_configs = AiModelApi.objects.exclude(api_base_url__isnull=True)
for config in custom_configs: for config in custom_configs:
try: try:
@ -247,7 +245,7 @@ def initialization(interactive: bool = True):
# Get existing chat model options for this config # Get existing chat model options for this config
existing_models = ChatModelOptions.objects.filter( existing_models = ChatModelOptions.objects.filter(
openai_config=config, model_type=ChatModelOptions.ModelType.OPENAI ai_model_api=config, model_type=ChatModelOptions.ModelType.OPENAI
) )
# Add new models # Add new models
@ -259,7 +257,7 @@ def initialization(interactive: bool = True):
max_prompt_size=model_to_prompt_size.get(model), max_prompt_size=model_to_prompt_size.get(model),
vision_enabled=model in default_openai_chat_models, vision_enabled=model in default_openai_chat_models,
tokenizer=model_to_tokenizer.get(model), tokenizer=model_to_tokenizer.get(model),
openai_config=config, ai_model_api=config,
) )
# Remove models that are no longer available # Remove models that are no longer available

View file

@ -34,8 +34,8 @@ from khoj.utils.constants import web_directory
from khoj.utils.helpers import resolve_absolute_path from khoj.utils.helpers import resolve_absolute_path
from khoj.utils.rawconfig import ContentConfig, ImageSearchConfig, SearchConfig from khoj.utils.rawconfig import ContentConfig, ImageSearchConfig, SearchConfig
from tests.helpers import ( from tests.helpers import (
AiModelApiFactory,
ChatModelOptionsFactory, ChatModelOptionsFactory,
OpenAIProcessorConversationConfigFactory,
ProcessLockFactory, ProcessLockFactory,
SubscriptionFactory, SubscriptionFactory,
UserConversationProcessorConfigFactory, UserConversationProcessorConfigFactory,
@ -319,9 +319,7 @@ def chat_client_builder(search_config, user, index_content=True, require_auth=Fa
elif chat_provider == ChatModelOptions.ModelType.ANTHROPIC: elif chat_provider == ChatModelOptions.ModelType.ANTHROPIC:
online_chat_model = ChatModelOptionsFactory(chat_model="claude-3-5-haiku-20241022", model_type="anthropic") online_chat_model = ChatModelOptionsFactory(chat_model="claude-3-5-haiku-20241022", model_type="anthropic")
if online_chat_model: if online_chat_model:
online_chat_model.openai_config = OpenAIProcessorConversationConfigFactory( online_chat_model.ai_model_api = AiModelApiFactory(api_key=get_chat_api_key(chat_provider))
api_key=get_chat_api_key(chat_provider)
)
UserConversationProcessorConfigFactory(user=user, setting=online_chat_model) UserConversationProcessorConfigFactory(user=user, setting=online_chat_model)
state.anonymous_mode = not require_auth state.anonymous_mode = not require_auth

View file

@ -5,11 +5,11 @@ import factory
from django.utils.timezone import make_aware from django.utils.timezone import make_aware
from khoj.database.models import ( from khoj.database.models import (
AiModelApi,
ChatModelOptions, ChatModelOptions,
Conversation, Conversation,
KhojApiUser, KhojApiUser,
KhojUser, KhojUser,
OpenAIProcessorConversationConfig,
ProcessLock, ProcessLock,
SearchModelConfig, SearchModelConfig,
Subscription, Subscription,
@ -76,9 +76,9 @@ class ApiUserFactory(factory.django.DjangoModelFactory):
token = factory.Faker("password") token = factory.Faker("password")
class OpenAIProcessorConversationConfigFactory(factory.django.DjangoModelFactory): class AiModelApiFactory(factory.django.DjangoModelFactory):
class Meta: class Meta:
model = OpenAIProcessorConversationConfig model = AiModelApi
api_key = get_chat_api_key() api_key = get_chat_api_key()
@ -91,9 +91,7 @@ class ChatModelOptionsFactory(factory.django.DjangoModelFactory):
tokenizer = None tokenizer = None
chat_model = "bartowski/Meta-Llama-3.2-3B-Instruct-GGUF" chat_model = "bartowski/Meta-Llama-3.2-3B-Instruct-GGUF"
model_type = get_chat_provider() model_type = get_chat_provider()
openai_config = factory.LazyAttribute( ai_model_api = factory.LazyAttribute(lambda obj: AiModelApiFactory() if get_chat_api_key() else None)
lambda obj: OpenAIProcessorConversationConfigFactory() if get_chat_api_key() else None
)
class UserConversationProcessorConfigFactory(factory.django.DjangoModelFactory): class UserConversationProcessorConfigFactory(factory.django.DjangoModelFactory):