Commit a3d4fa64 authored by David Yang's avatar David Yang

fix: route llm chat through backend

parent 1488c6e6
UMI_APP_LLM_API_KEY=your_api_key_here # 模型请求通过后端 /api/llm/chat 处理;不要在前端配置模型服务 URL 或 API Key。
# Optional overrides. Defaults are shown below.
# UMI_APP_LLM_API_BASE=/llm-api
# UMI_APP_LLM_CHAT_PATH=/v1/chat/completions
# UMI_APP_LLM_MODEL=gpt-5.5
...@@ -64,13 +64,6 @@ export default defineConfig({ ...@@ -64,13 +64,6 @@ export default defineConfig({
changeOrigin: true, changeOrigin: true,
pathRewrite: { '^/api': '' }, pathRewrite: { '^/api': '' },
}, },
'/llm-api': {
target: 'https://lingsuan.org',
changeOrigin: true,
pathRewrite: { '^/llm-api': '' },
timeout: 600000,
proxyTimeout: 600000,
},
'/myapi/upload_invoices': { '/myapi/upload_invoices': {
target: 'http://1.94.111.115:5173', target: 'http://1.94.111.115:5173',
changeOrigin: true, changeOrigin: true,
......
...@@ -11,21 +11,9 @@ ...@@ -11,21 +11,9 @@
## 本地启动 ## 本地启动
如果本地模型代理需要浏览器携带 API Key,先创建本地环境变量文件:
```bash
cp .env.example .env.local
```
然后把 `.env.local` 里的 `UMI_APP_LLM_API_KEY` 替换成线上模型服务 API Key。如果 `/llm-api` 代理已在服务端处理鉴权,可以不创建该文件。
```bash ```bash
pnpm install pnpm install
pnpm dev pnpm dev
``` ```
可选配置: 聊天请求固定发送到同源后端接口 `/api/llm/chat`。模型服务鉴权和上游地址均由后端管理,前端不需要也不应配置模型 API Key。
- `UMI_APP_LLM_API_BASE`:模型服务地址,默认走 Umi 代理 `/llm-api`
- `UMI_APP_LLM_CHAT_PATH`:聊天接口路径,默认 `/v1/chat/completions`
- `UMI_APP_LLM_MODEL`:默认模型,默认 `gpt-5.5`
...@@ -15,29 +15,7 @@ function joinUrl(baseUrl, path) { ...@@ -15,29 +15,7 @@ function joinUrl(baseUrl, path) {
return `${baseUrl.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`; return `${baseUrl.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`;
} }
const DEFAULT_LLM_CHAT_PATH = '/v1/chat/completions'; const BACKEND_LLM_CHAT_PATH = '/api/llm/chat';
const SAME_ORIGIN_LLM_PROXY = '/llm-api';
const PROXIED_LLM_HOSTS = new Set(['edge.lingsuan.org', 'lingsuan.org']);
function resolveConfiguredLlmUrl(url, path) {
if (!url) return null;
if (path) return joinUrl(url, path);
if (
/\/(?:chat\/completions|responses|completions)\/?(?:[?#].*)?$/i.test(url)
) {
return url;
}
return joinUrl(url, DEFAULT_LLM_CHAT_PATH);
}
function routeLlmUrlThroughSameOriginProxy(url) {
if (!url || !/^https?:\/\//i.test(url)) return url;
const parsed = new URL(url);
if (!PROXIED_LLM_HOSTS.has(parsed.hostname.toLowerCase())) return url;
return `${SAME_ORIGIN_LLM_PROXY}${parsed.pathname}${parsed.search}`;
}
function firstDefined(...values) { function firstDefined(...values) {
return values.find((value) => value !== undefined && value !== null); return values.find((value) => value !== undefined && value !== null);
...@@ -276,31 +254,6 @@ function normalizeApplicationConfig(application = {}) { ...@@ -276,31 +254,6 @@ function normalizeApplicationConfig(application = {}) {
settingsConfig.long_thinking_model, settingsConfig.long_thinking_model,
), ),
}, },
apiKey: firstNonEmptyString(
layoutsConfig.apiKey,
layoutsConfig.api_key,
layoutsConfig.apikey,
layoutsConfig.llmApiKey,
layoutsConfig.llm_api_key,
llmConfig.apiKey,
llmConfig.api_key,
llmConfig.apikey,
settingsConfig.apiKey,
settingsConfig.api_key,
settingsConfig.apikey,
process.env.UMI_APP_LLM_API_KEY,
process.env.VITE_LLM_API_KEY,
),
authorization: firstNonEmptyString(
layoutsConfig.authorization,
llmConfig.authorization,
settingsConfig.authorization,
),
headers: firstDefined(
layoutsConfig.headers,
llmConfig.headers,
settingsConfig.headers,
),
requestParams: pickRequestParams( requestParams: pickRequestParams(
settingsConfig, settingsConfig,
llmConfig, llmConfig,
...@@ -384,31 +337,8 @@ function normalizeLlmList(payload) { ...@@ -384,31 +337,8 @@ function normalizeLlmList(payload) {
return [...new Map(models.map((model) => [model.id, model])).values()]; return [...new Map(models.map((model) => [model.id, model])).values()];
} }
function resolveLlmUrl(llmConfig = {}) { function resolveLlmUrl() {
const envBaseUrl = firstNonEmptyString( return BACKEND_LLM_CHAT_PATH;
process.env.UMI_APP_LLM_API_BASE,
process.env.VITE_LLM_API_BASE,
);
const envPath = firstNonEmptyString(
process.env.UMI_APP_LLM_CHAT_PATH,
process.env.VITE_LLM_CHAT_PATH,
);
const configUrl = llmConfig.url;
const configPath = llmConfig.path;
if (configUrl) {
return routeLlmUrlThroughSameOriginProxy(
resolveConfiguredLlmUrl(configUrl, configPath),
);
}
if (envBaseUrl) {
return routeLlmUrlThroughSameOriginProxy(
resolveConfiguredLlmUrl(envBaseUrl, envPath),
);
}
return null;
} }
export function extractLlmChunkContent(value) { export function extractLlmChunkContent(value) {
...@@ -739,10 +669,6 @@ export async function llmChat(data, signal = undefined) { ...@@ -739,10 +669,6 @@ export async function llmChat(data, signal = undefined) {
const url = resolveLlmUrl(llmConfig); const url = resolveLlmUrl(llmConfig);
const model = data.model || llmConfig.model; const model = data.model || llmConfig.model;
if (!url) {
throw new Error('应用配置缺少模型请求地址:请在 layouts.config.url 中配置');
}
if (!model) { if (!model) {
throw new Error('应用配置缺少模型名称:请在 layouts.config.model 中配置'); throw new Error('应用配置缺少模型名称:请在 layouts.config.model 中配置');
} }
...@@ -752,23 +678,6 @@ export async function llmChat(data, signal = undefined) { ...@@ -752,23 +678,6 @@ export async function llmChat(data, signal = undefined) {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}); });
const configuredHeaders = normalizeObjectConfig(llmConfig.headers);
if (Object.keys(configuredHeaders).length) {
Object.entries(configuredHeaders).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
headers.set(key, String(value));
}
});
}
if (llmConfig.authorization && !headers.has('Authorization')) {
headers.set('Authorization', llmConfig.authorization);
}
if (llmConfig.apiKey && !headers.has('Authorization')) {
headers.set('Authorization', `Bearer ${llmConfig.apiKey}`);
}
const response = await fetch(url, { const response = await fetch(url, {
method: 'POST', method: 'POST',
headers, headers,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment