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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@
"code-inspector-plugin": "^0.20.14",
"dexie": "^4.0.8",
"dexie-react-hooks": "^1.1.7",
"electron": "^37.2.3",
"electron": "^37.10.0",
"electron-builder": "^25.1.8",
"electron-rebuild": "^3.2.9",
"electron-vite": "^4.0.0",
Expand Down Expand Up @@ -145,4 +145,4 @@
"esbuild"
]
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 7 additions & 1 deletion frontend/src/renderer/src/pages/settings/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import { ReactNode } from 'react'
import openAI from '../../assets/images/settings/OpenAI.png'
import doubao from '../../assets/images/settings/doubao.png'
import custom from '../../assets/images/settings/custom.svg'

import Azure from '../../assets/images/settings/azure.png'
export enum ModelTypeList {
Doubao = 'doubao',
OpenAI = 'openai',
Azure = 'azure',
Custom = 'custom'
}

Expand Down Expand Up @@ -70,6 +71,11 @@ export const ModelInfoList = [
}
]
},
{
icon: <img src={Azure} className="!max-w-none w-[24px] h-[24px]" />,
key: 'Azure',
value: 'azure'
},
{
icon: <img src={custom} className="!max-w-none w-[18px] h-[18px]" />,
key: 'Custom',
Expand Down
116 changes: 114 additions & 2 deletions frontend/src/renderer/src/pages/settings/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,110 @@ const CustomFormItems: FC<CustomFormItemsProps> = (props) => {
</>
)
}

export interface AzureFormItemsProps {
prefix: string
}
const AzureFormItems: FC<AzureFormItemsProps> = (props) => {
const { prefix } = props
return (
<>


<div className="flex flex-col gap-6 mb-6">
{/* VLM Configuration */}
<div className="flex flex-col gap-[8px]">
<span className="text-[#0B0B0F] font-roboto text-base font-normal leading-[22px]">
Vision language model
</span>
<FormItem
field={`${prefix}-modelId`}
className="!mb-0"
rules={[{ required: true, message: 'Cannot be empty' }]}
requiredSymbol={false}>
<Input
addBefore={<InputPrefix label="Model name" />}
placeholder="Deployment name"
allowClear
className="[&_.arco-input-inner-wrapper]: !w-[574px]"
/>
</FormItem>
<FormItem
field={`${prefix}-baseUrl`}
className="!mb-0"
rules={[{ required: true, message: 'Cannot be empty' }]}
requiredSymbol={false}>
<Input
addBefore={<InputPrefix label="Base URL" />}
placeholder="Paste complete Azure URL here"
allowClear
className="[&_.arco-input-inner-wrapper]: !w-[574px]"
/>
</FormItem>
<Text type="warning" style={{ fontSize: 12 }}>
Complete URL like: https://your-resource.openai.azure.com/openai/deployments/gpt-4/chat/completions?api-version=2024-02-01
</Text>
<FormItem
field={`${prefix}-apiKey`}
className="!mb-0"
rules={[{ required: true, message: 'Cannot be empty' }]}
requiredSymbol={false}>
<Input
addBefore={<InputPrefix label="API Key" />}
placeholder="Enter your Azure OpenAI API Key"
allowClear
className="!w-[574px]"
/>
</FormItem>
</div>

{/* Embedding Configuration */}
<div className="flex flex-col gap-[8px]">
<span className="text-[#0B0B0F] font-roboto text-base font-normal leading-[22px]">
Embedding model
</span>
<FormItem
field={`${prefix}-embeddingModelId`}
className="!mb-0"
rules={[{ required: true, message: 'Cannot be empty' }]}
requiredSymbol={false}>
<Input
addBefore={<InputPrefix label="Model name" />}
placeholder="Embedding deployment name"
allowClear
className="!w-[574px]"
/>
</FormItem>
<FormItem
field={`${prefix}-embeddingBaseUrl`}
className="!mb-0"
rules={[{ required: true, message: 'Cannot be empty' }]}
requiredSymbol={false}>
<Input
addBefore={<InputPrefix label="Base URL" />}
placeholder="Complete Azure embedding URL or other base URL"
allowClear
className="!w-[574px]"
/>
</FormItem>
<FormItem
field={`${prefix}-embeddingApiKey`}
className="!mb-0"
rules={[{ required: true, message: 'Cannot be empty' }]}
requiredSymbol={false}>
<Input
addBefore={<InputPrefix label="API Key" />}
placeholder="Enter Azure API Key"
allowClear
className="!w-[574px]"
/>
</FormItem>
</div>
</div>
</>
)
}

export interface StandardFormItemsProps {
modelPlatform: ModelTypeList
prefix: string
Expand Down Expand Up @@ -194,7 +298,12 @@ export type SettingsFormProps = SettingsFormBase & {
[K in
| `${ModelTypeList.Custom}-embeddingModelId`
| `${ModelTypeList.Custom}-embeddingBaseUrl`
| `${ModelTypeList.Custom}-embeddingApiKey`]?: string
| `${ModelTypeList.Custom}-embeddingApiKey`
| `${ModelTypeList.Custom}-baseUrl`
| `${ModelTypeList.Azure}-embeddingModelId`
| `${ModelTypeList.Azure}-embeddingBaseUrl`
| `${ModelTypeList.Azure}-embeddingApiKey`
| `${ModelTypeList.Azure}-baseUrl`]?: string
}
const Settings: FC<SettingsProps> = (props) => {
const { closeSetting, init } = props
Expand All @@ -221,6 +330,7 @@ const Settings: FC<SettingsProps> = (props) => {
await form.validate()
const values = form.getFieldsValue()
const isCustom = values.modelPlatform === ModelTypeList.Custom
const isAzure = values.modelPlatform === ModelTypeList.Azure
if (!values.modelPlatform) {
Message.error('Please select Model Platform')
return
Expand All @@ -238,7 +348,7 @@ const Settings: FC<SettingsProps> = (props) => {
const formatData = Object.fromEntries(
Object.entries(data).map(([key, value]) => [key.replace(`${values.modelPlatform}-`, ''), value])
)
const params = isCustom
const params = (isCustom || isAzure)
? formatData
: {
...formatData,
Expand Down Expand Up @@ -303,6 +413,8 @@ const Settings: FC<SettingsProps> = (props) => {
const modelPlatform = values.modelPlatform
if (modelPlatform === ModelTypeList.Custom) {
return <CustomFormItems prefix={ModelTypeList.Custom} />
} else if (modelPlatform === ModelTypeList.Azure) {
return <AzureFormItems prefix={ModelTypeList.Azure} />
} else if (modelPlatform === ModelTypeList.Doubao) {
return <StandardFormItems modelPlatform={modelPlatform} prefix={ModelTypeList.Doubao} />
} else if (modelPlatform === ModelTypeList.OpenAI) {
Expand Down
156 changes: 141 additions & 15 deletions opencontext/llm/llm_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
OpenContext module: llm_client
"""

import re
from enum import Enum
from typing import Any, Dict, List
from typing import Any, Dict, List, Optional
from urllib.parse import urlparse, parse_qs

from openai import APIError, AsyncOpenAI, OpenAI
from openai import APIError, AsyncOpenAI, OpenAI, AzureOpenAI, AsyncAzureOpenAI

from opencontext.models.context import Vectorize
from opencontext.utils.logging_utils import get_logger
Expand All @@ -21,6 +23,7 @@
class LLMProvider(Enum):
OPENAI = "openai"
DOUBAO = "doubao"
AZURE = "azure"


class LLMType(Enum):
Expand All @@ -34,15 +37,141 @@ def __init__(self, llm_type: LLMType, config: Dict[str, Any]):
self.config = config
self.model = config.get("model")
self.api_key = config.get("api_key")
self.base_url = config.get("base_url")
self.timeout = config.get("timeout", 300)
self.provider = config.get("provider", LLMProvider.OPENAI.value)
if not self.api_key or not self.base_url or not self.model:
raise ValueError("API key, base URL, and model must be provided")
self.client = OpenAI(api_key=self.api_key, base_url=self.base_url, timeout=self.timeout)
self.async_client = AsyncOpenAI(
api_key=self.api_key, base_url=self.base_url, timeout=self.timeout
)

# Azure OpenAI specific initialization
if self.provider == LLMProvider.AZURE.value:
# Check if user provided base_url (which might be a complete Azure URL)
base_url = config.get("base_url")
azure_endpoint = config.get("azure_endpoint")

# If no azure_endpoint but has base_url, try to parse it
if not azure_endpoint and base_url:
parsed_azure = self._parse_azure_url(base_url)
azure_endpoint = parsed_azure['azure_endpoint']

# If URL contains model and user didn't provide one, use parsed model
if parsed_azure['model'] and not self.model:
self.model = parsed_azure['model']
logger.info(f"Extracted model from Azure URL: {self.model}")

# If URL contains api_version, use it
if parsed_azure['api_version']:
self.api_version = parsed_azure['api_version']
logger.info(f"Extracted api_version from Azure URL: {self.api_version}")
else:
# No api_version in URL, check config
self.api_version = config.get("api_version")
else:
# User provided azure_endpoint (standard way)
self.api_version = config.get("api_version")

self.azure_endpoint = azure_endpoint

# Validate required parameters
if not self.api_key:
raise ValueError(
"Azure OpenAI requires api_key. "
"Please provide your Azure OpenAI API key."
)

if not self.azure_endpoint:
raise ValueError(
"Azure OpenAI requires azure_endpoint or a full URL in base_url. "
"Example: https://your-resource.openai.azure.com/openai/deployments/gpt-4/chat/completions?api-version=2024-02-01"
)

if not self.model:
raise ValueError(
"Azure OpenAI requires model (deployment name). "
"Please provide it explicitly or include it in the URL."
)

# Validate api_version must exist
if not self.api_version:
raise ValueError(
"Azure OpenAI requires api_version. "
"Please provide a full Azure API URL with api-version parameter. "
"Example: https://your-resource.openai.azure.com/openai/deployments/gpt-4/chat/completions?api-version=2024-02-01"
)

self.client = AzureOpenAI(
api_key=self.api_key,
azure_endpoint=self.azure_endpoint,
api_version=self.api_version,
timeout=self.timeout
)
self.async_client = AsyncAzureOpenAI(
api_key=self.api_key,
azure_endpoint=self.azure_endpoint,
api_version=self.api_version,
timeout=self.timeout
)
else:
# Standard OpenAI or compatible APIs (Doubao, custom, etc.)
self.base_url = config.get("base_url")
if not self.api_key or not self.base_url or not self.model:
raise ValueError("API key, base URL, and model must be provided")

self.client = OpenAI(
api_key=self.api_key,
base_url=self.base_url,
timeout=self.timeout
)
self.async_client = AsyncOpenAI(
api_key=self.api_key,
base_url=self.base_url,
timeout=self.timeout
)

@staticmethod
def _parse_azure_url(url: str) -> Dict[str, Optional[str]]:
"""
Parse Azure OpenAI URL and extract configuration parameters

Args:
url: Azure OpenAI URL (full URL or just endpoint)

Returns:
Dict with keys: azure_endpoint, model, api_version

Examples:
>>> _parse_azure_url("https://xxx.openai.azure.com/openai/deployments/gpt-4/chat/completions?api-version=2024-02-01")
{
'azure_endpoint': 'https://xxx.openai.azure.com/',
'model': 'gpt-4',
'api_version': '2024-02-01'
}
"""
if not url or not url.strip():
return {'azure_endpoint': None, 'model': None, 'api_version': None}

url = url.strip()
parsed = urlparse(url)

# Extract azure_endpoint (base domain)
azure_endpoint = f"{parsed.scheme}://{parsed.netloc}/"

# Extract deployment name (model) from path
# Path format: /openai/deployments/{deployment-name}/...
model = None
if parsed.path:
match = re.search(r'/openai/deployments/([^/]+)', parsed.path)
if match:
model = match.group(1)

# Extract api_version from query params
api_version = None
if parsed.query:
query_params = parse_qs(parsed.query)
api_version = query_params.get('api-version', [None])[0]

return {
'azure_endpoint': azure_endpoint,
'model': model,
'api_version': api_version
}

def generate(self, prompt: str, **kwargs) -> str:
messages = [{"role": "user", "content": prompt}]
Expand Down Expand Up @@ -263,12 +392,9 @@ async def _openai_chat_completion_stream_async(self, messages: List[Dict[str, An
tools = kwargs.get("tools", None)
thinking = kwargs.get("thinking", None)

# Create async client
from openai import AsyncOpenAI

async_client = AsyncOpenAI(
api_key=self.api_key, base_url=self.base_url, timeout=self.timeout
)
# Use the existing async_client that was initialized in __init__
# This ensures proper Azure vs OpenAI client is used
async_client = self.async_client

create_params = {
"model": self.model,
Expand Down
Loading