mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2026-06-19 07:36:59 +00:00
feat: integrate Atlas Cloud provider (#1674)
This commit is contained in:
@@ -37,12 +37,12 @@ If you find this project helpful, please consider sponsoring us to support ongoi
|
||||
### AI Infrastructure Support
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.atlascloud.ai/" target="_blank">
|
||||
<a href="https://www.atlascloud.ai/?utm_source=github&utm_medium=link&utm_campaign=nginx-ui" target="_blank">
|
||||
<img src="resources/atlas-cloud-logo.svg" alt="Atlas Cloud Logo" width="240">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
Atlas Cloud is a full-modal AI inference platform that gives developers a single AI API to access the world's best video generation API, image generation API, LLM API.
|
||||
[Atlas Cloud](https://www.atlascloud.ai/?utm_source=github&utm_medium=link&utm_campaign=nginx-ui) is a full-modal AI inference platform that gives developers a single AI API to access video generation, image generation, and LLM APIs.
|
||||
|
||||
### Official Community Group
|
||||
|
||||
|
||||
+12
-1
@@ -17,6 +17,13 @@ import (
|
||||
"github.com/uozi-tech/cosy/logger"
|
||||
)
|
||||
|
||||
func getStreamDeltaContent(response openai.ChatCompletionStreamResponse) string {
|
||||
if len(response.Choices) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return response.Choices[0].Delta.Content
|
||||
}
|
||||
|
||||
func MakeChatCompletionRequest(c *gin.Context) {
|
||||
var json struct {
|
||||
@@ -121,7 +128,11 @@ func MakeChatCompletionRequest(c *gin.Context) {
|
||||
logger.Errorf("Stream error: %v\n", err)
|
||||
return
|
||||
}
|
||||
messageCh <- response.Choices[0].Delta.Content
|
||||
content := getStreamDeltaContent(response)
|
||||
if content == "" {
|
||||
continue
|
||||
}
|
||||
messageCh <- content
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/sashabaranov/go-openai"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetStreamDeltaContent(t *testing.T) {
|
||||
t.Run("returns empty string when choices are missing", func(t *testing.T) {
|
||||
response := openai.ChatCompletionStreamResponse{}
|
||||
|
||||
assert.Empty(t, getStreamDeltaContent(response))
|
||||
})
|
||||
|
||||
t.Run("returns delta content from first choice", func(t *testing.T) {
|
||||
response := openai.ChatCompletionStreamResponse{
|
||||
Choices: []openai.ChatCompletionStreamChoice{
|
||||
{
|
||||
Delta: openai.ChatCompletionStreamChoiceDelta{
|
||||
Content: "atlas",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, "atlas", getStreamDeltaContent(response))
|
||||
})
|
||||
}
|
||||
@@ -53,6 +53,10 @@ func buildSettingsResponse() gin.H {
|
||||
node["secret"] = redactedSensitiveValue
|
||||
|
||||
openai := cloneSettingsSection(settings.OpenAISettings)
|
||||
openai["provider"] = settings.OpenAISettings.GetProvider()
|
||||
if baseURL := settings.OpenAISettings.GetBaseURL(); openai["base_url"] == "" && baseURL != "" {
|
||||
openai["base_url"] = baseURL
|
||||
}
|
||||
openai["token"] = redactedSensitiveValue
|
||||
|
||||
return gin.H{
|
||||
|
||||
@@ -90,6 +90,7 @@ SkipInstallation = false
|
||||
Demo = false
|
||||
|
||||
[openai]
|
||||
Provider = openai
|
||||
BaseUrl =
|
||||
Token =
|
||||
Proxy =
|
||||
|
||||
@@ -88,6 +88,7 @@ export interface NodeSettings {
|
||||
}
|
||||
|
||||
export interface OpenaiSettings {
|
||||
provider: string
|
||||
model: string
|
||||
base_url: string
|
||||
proxy: string
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
export interface LLMProviderPreset {
|
||||
value: string
|
||||
label: string
|
||||
baseUrl?: string
|
||||
}
|
||||
|
||||
export const LLM_MODELS = [
|
||||
'deepseek-ai/deepseek-v4-flash',
|
||||
'deepseek-v3',
|
||||
'o3-mini',
|
||||
'o1',
|
||||
'deepseek-reasoner',
|
||||
@@ -11,8 +19,25 @@ export const LLM_MODELS = [
|
||||
'gpt-3.5-turbo',
|
||||
]
|
||||
|
||||
export const LLM_PROVIDERS = [
|
||||
export const LLM_PROVIDERS: LLMProviderPreset[] = [
|
||||
{
|
||||
value: 'openai',
|
||||
label: 'OpenAI',
|
||||
},
|
||||
{
|
||||
value: 'atlas_cloud',
|
||||
label: 'Atlas Cloud',
|
||||
baseUrl: 'https://api.atlascloud.ai/v1',
|
||||
},
|
||||
{
|
||||
value: 'custom',
|
||||
label: 'Custom',
|
||||
},
|
||||
]
|
||||
|
||||
export const LLM_PROVIDER_BASE_URLS = [
|
||||
'https://api.openai.com',
|
||||
'https://api.atlascloud.ai/v1',
|
||||
'https://api.deepseek.com',
|
||||
'http://localhost:11434',
|
||||
]
|
||||
|
||||
@@ -98,7 +98,7 @@ onMounted(() => {
|
||||
</ATabPane>
|
||||
<ATabPane
|
||||
key="openai"
|
||||
:tab="$gettext('OpenAI')"
|
||||
:tab="$gettext('LLM')"
|
||||
>
|
||||
<OpenAISettings />
|
||||
</ATabPane>
|
||||
|
||||
@@ -88,6 +88,7 @@ const useSystemSettingsStore = defineStore('systemSettings', () => {
|
||||
public_security_number: '',
|
||||
},
|
||||
openai: {
|
||||
provider: 'openai',
|
||||
model: '',
|
||||
base_url: '',
|
||||
proxy: '',
|
||||
|
||||
@@ -1,22 +1,85 @@
|
||||
<script setup lang="ts">
|
||||
import { SensitiveInput } from '@/components/SensitiveString'
|
||||
import { LLM_MODELS, LLM_PROVIDERS } from '@/constants/llm'
|
||||
import { LLM_MODELS, LLM_PROVIDER_BASE_URLS, LLM_PROVIDERS } from '@/constants/llm'
|
||||
import useSystemSettingsStore from '../store'
|
||||
|
||||
const systemSettingsStore = useSystemSettingsStore()
|
||||
const { data, errors } = storeToRefs(systemSettingsStore)
|
||||
|
||||
const models = LLM_MODELS.map(model => ({
|
||||
const modelOptions = LLM_MODELS.map(model => ({
|
||||
value: model,
|
||||
}))
|
||||
|
||||
const providers = LLM_PROVIDERS.map(provider => ({
|
||||
value: provider,
|
||||
const providerOptions = LLM_PROVIDERS.map(provider => ({
|
||||
label: provider.label,
|
||||
value: provider.value,
|
||||
}))
|
||||
|
||||
const baseUrlOptions = LLM_PROVIDER_BASE_URLS.map(baseUrl => ({
|
||||
value: baseUrl,
|
||||
}))
|
||||
|
||||
const providerBaseUrlMap = LLM_PROVIDERS.reduce<Record<string, string>>((acc, provider) => {
|
||||
if (provider.baseUrl)
|
||||
acc[provider.value] = provider.baseUrl
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
const baseUrlPlaceholder = computed(() => {
|
||||
if (data.value?.openai.provider === 'atlas_cloud')
|
||||
return $gettext('Leave blank to use the Atlas Cloud endpoint: https://api.atlascloud.ai/v1')
|
||||
|
||||
return $gettext('Leave blank for the default: https://api.openai.com/')
|
||||
})
|
||||
|
||||
const baseUrlHelp = computed(() => {
|
||||
if (errors.value?.openai?.base_url === 'url')
|
||||
return $gettext('The url is invalid.')
|
||||
|
||||
if (data.value?.openai.provider === 'atlas_cloud') {
|
||||
return $gettext('Atlas Cloud is OpenAI-compatible. Use https://api.atlascloud.ai/v1 and an Atlas Cloud API key.')
|
||||
}
|
||||
|
||||
return $gettext('To use a local large model, deploy it with ollama, vllm or lmdeploy. '
|
||||
+ 'They provide an OpenAI-compatible API endpoint, so just set the baseUrl to your local API.')
|
||||
})
|
||||
|
||||
watch(
|
||||
() => data.value?.openai.provider,
|
||||
(provider, previousProvider) => {
|
||||
if (!data.value || !provider)
|
||||
return
|
||||
|
||||
const nextBaseUrl = providerBaseUrlMap[provider]
|
||||
if (!nextBaseUrl)
|
||||
return
|
||||
|
||||
const currentBaseUrl = data.value.openai.base_url?.trim()
|
||||
const previousBaseUrl = previousProvider ? providerBaseUrlMap[previousProvider] : ''
|
||||
|
||||
if (!currentBaseUrl || currentBaseUrl === previousBaseUrl)
|
||||
data.value.openai.base_url = nextBaseUrl
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AForm layout="vertical">
|
||||
<AFormItem
|
||||
:label="$gettext('Provider')"
|
||||
:validate-status="errors?.openai?.provider ? 'error' : ''"
|
||||
>
|
||||
<ASelect v-model:value="data.openai.provider">
|
||||
<ASelectOption
|
||||
v-for="provider in providerOptions"
|
||||
:key="provider.value"
|
||||
:value="provider.value"
|
||||
>
|
||||
{{ provider.label }}
|
||||
</ASelectOption>
|
||||
</ASelect>
|
||||
</AFormItem>
|
||||
<AFormItem
|
||||
:label="$gettext('Model')"
|
||||
:validate-status="errors?.openai?.model ? 'error' : ''"
|
||||
@@ -26,21 +89,18 @@ const providers = LLM_PROVIDERS.map(provider => ({
|
||||
>
|
||||
<AAutoComplete
|
||||
v-model:value="data.openai.model"
|
||||
:options="models"
|
||||
:options="modelOptions"
|
||||
/>
|
||||
</AFormItem>
|
||||
<AFormItem
|
||||
:label="$gettext('API Base Url')"
|
||||
:validate-status="errors?.openai?.base_url ? 'error' : ''"
|
||||
:help="errors?.openai?.base_url === 'url'
|
||||
? $gettext('The url is invalid.')
|
||||
: $gettext('To use a local large model, deploy it with ollama, vllm or lmdeploy. '
|
||||
+ 'They provide an OpenAI-compatible API endpoint, so just set the baseUrl to your local API.')"
|
||||
:help="baseUrlHelp"
|
||||
>
|
||||
<AAutoComplete
|
||||
v-model:value="data.openai.base_url"
|
||||
:placeholder="$gettext('Leave blank for the default: https://api.openai.com/')"
|
||||
:options="providers"
|
||||
:placeholder="baseUrlPlaceholder"
|
||||
:options="baseUrlOptions"
|
||||
/>
|
||||
</AFormItem>
|
||||
<AFormItem
|
||||
@@ -95,7 +155,7 @@ const providers = LLM_PROVIDERS.map(provider => ({
|
||||
>
|
||||
<AAutoComplete
|
||||
v-model:value="data.openai.code_completion_model"
|
||||
:options="models"
|
||||
:options="modelOptions"
|
||||
/>
|
||||
</AFormItem>
|
||||
</AForm>
|
||||
|
||||
@@ -4,11 +4,26 @@ This section is for setting up ChatGPT configurations. Please be aware that we d
|
||||
information you provide. If the configuration is incorrect, it might cause API request failures, making the ChatGPT
|
||||
assistant unusable.
|
||||
|
||||
## Provider
|
||||
|
||||
- Type: `string`
|
||||
- Default: `openai`
|
||||
|
||||
This option selects a preset for OpenAI-compatible providers.
|
||||
|
||||
- `openai`: Use the default OpenAI endpoint.
|
||||
- `atlas_cloud`: Use the Atlas Cloud endpoint `https://api.atlascloud.ai/v1`.
|
||||
- `custom`: Keep using the custom `BaseUrl` value.
|
||||
|
||||
## BaseUrl
|
||||
|
||||
- Type: `string`
|
||||
|
||||
This option is used to set the base url of the api of Open AI, leave it blank if you do not need to change the url.
|
||||
This option is used to set the base URL of the API. Leave it blank if you do not need to change the URL.
|
||||
|
||||
For Atlas Cloud, use `https://api.atlascloud.ai/v1`. Atlas Cloud is OpenAI-compatible, so the existing chat and code
|
||||
completion features work without additional backend changes. You can find the Atlas Cloud model guide at
|
||||
<https://www.atlascloud.ai/docs/models/get-start>.
|
||||
|
||||
## Token
|
||||
|
||||
|
||||
@@ -116,6 +116,7 @@ Applicable for version v2.0.0-beta.37 and above.
|
||||
## OpenAI
|
||||
| Configuration Setting | Environment Variable |
|
||||
|-----------------------|--------------------------|
|
||||
| Provider | NGINX_UI_OPENAI_PROVIDER |
|
||||
| Model | NGINX_UI_OPENAI_MODEL |
|
||||
| BaseUrl | NGINX_UI_OPENAI_BASE_URL |
|
||||
| Proxy | NGINX_UI_OPENAI_PROXY |
|
||||
|
||||
@@ -9,8 +9,9 @@ import (
|
||||
|
||||
func GetClient() (*openai.Client, error) {
|
||||
var config openai.ClientConfig
|
||||
baseURL := settings.OpenAISettings.GetBaseURL()
|
||||
if openai.APIType(settings.OpenAISettings.APIType) == openai.APITypeAzure {
|
||||
config = openai.DefaultAzureConfig(settings.OpenAISettings.Token, settings.OpenAISettings.BaseUrl)
|
||||
config = openai.DefaultAzureConfig(settings.OpenAISettings.Token, baseURL)
|
||||
} else {
|
||||
config = openai.DefaultConfig(settings.OpenAISettings.Token)
|
||||
}
|
||||
@@ -25,8 +26,8 @@ func GetClient() (*openai.Client, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if settings.OpenAISettings.BaseUrl != "" {
|
||||
config.BaseURL = settings.OpenAISettings.BaseUrl
|
||||
if baseURL != "" {
|
||||
config.BaseURL = baseURL
|
||||
}
|
||||
|
||||
return openai.NewClientWithConfig(config), nil
|
||||
|
||||
@@ -66,6 +66,7 @@ SkipInstallation = false
|
||||
Demo = true
|
||||
|
||||
[openai]
|
||||
Provider = openai
|
||||
BaseUrl =
|
||||
Token =
|
||||
Proxy =
|
||||
|
||||
+52
-1
@@ -1,8 +1,20 @@
|
||||
package settings
|
||||
|
||||
import "github.com/sashabaranov/go-openai"
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
const (
|
||||
OpenAIProviderOpenAI = "openai"
|
||||
OpenAIProviderAtlasCloud = "atlas_cloud"
|
||||
OpenAIProviderCustom = "custom"
|
||||
AtlasCloudBaseURL = "https://api.atlascloud.ai/v1"
|
||||
)
|
||||
|
||||
type OpenAI struct {
|
||||
Provider string `json:"provider" binding:"omitempty,oneof=openai atlas_cloud custom"`
|
||||
BaseUrl string `json:"base_url" binding:"omitempty,url"`
|
||||
Token string `json:"token" binding:"omitempty,safety_text"`
|
||||
Proxy string `json:"proxy" binding:"omitempty,url"`
|
||||
@@ -13,6 +25,7 @@ type OpenAI struct {
|
||||
}
|
||||
|
||||
var OpenAISettings = &OpenAI{
|
||||
Provider: OpenAIProviderOpenAI,
|
||||
APIType: string(openai.APITypeOpenAI),
|
||||
}
|
||||
|
||||
@@ -22,3 +35,41 @@ func (o *OpenAI) GetCodeCompletionModel() string {
|
||||
}
|
||||
return o.CodeCompletionModel
|
||||
}
|
||||
|
||||
func (o *OpenAI) GetProvider() string {
|
||||
if o == nil {
|
||||
return OpenAIProviderOpenAI
|
||||
}
|
||||
|
||||
switch normalizeOpenAIBaseURL(o.BaseUrl) {
|
||||
case AtlasCloudBaseURL:
|
||||
return OpenAIProviderAtlasCloud
|
||||
}
|
||||
|
||||
if o.Provider == "" {
|
||||
return OpenAIProviderOpenAI
|
||||
}
|
||||
|
||||
return o.Provider
|
||||
}
|
||||
|
||||
func (o *OpenAI) GetBaseURL() string {
|
||||
if o == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if baseURL := normalizeOpenAIBaseURL(o.BaseUrl); baseURL != "" {
|
||||
return baseURL
|
||||
}
|
||||
|
||||
switch o.GetProvider() {
|
||||
case OpenAIProviderAtlasCloud:
|
||||
return AtlasCloudBaseURL
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeOpenAIBaseURL(baseURL string) string {
|
||||
return strings.TrimRight(strings.TrimSpace(baseURL), "/")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestOpenAIGetProvider(t *testing.T) {
|
||||
t.Run("defaults to openai", func(t *testing.T) {
|
||||
cfg := &OpenAI{}
|
||||
|
||||
assert.Equal(t, OpenAIProviderOpenAI, cfg.GetProvider())
|
||||
})
|
||||
|
||||
t.Run("infers atlas cloud from base url", func(t *testing.T) {
|
||||
cfg := &OpenAI{BaseUrl: "https://api.atlascloud.ai/v1/"}
|
||||
|
||||
assert.Equal(t, OpenAIProviderAtlasCloud, cfg.GetProvider())
|
||||
})
|
||||
}
|
||||
|
||||
func TestOpenAIGetBaseURL(t *testing.T) {
|
||||
t.Run("returns atlas default base url from provider preset", func(t *testing.T) {
|
||||
cfg := &OpenAI{Provider: OpenAIProviderAtlasCloud}
|
||||
|
||||
assert.Equal(t, AtlasCloudBaseURL, cfg.GetBaseURL())
|
||||
})
|
||||
|
||||
t.Run("normalizes custom base url", func(t *testing.T) {
|
||||
cfg := &OpenAI{Provider: OpenAIProviderCustom, BaseUrl: "https://example.com/v1/"}
|
||||
|
||||
assert.Equal(t, "https://example.com/v1", cfg.GetBaseURL())
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user