Skip to content
Draft
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
85 changes: 85 additions & 0 deletions PR_DESCRIPTION_CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# fix: 模型渠道配置体验改进 — 无 AI Proxy 时的替代方案与直连 API 测试

关联: #6525
分支: `fix/issue-6525-aiproxy-channel-stability`(提交 `a20ec2e47`)

---

## 背景

自部署用户在不使用或未正确配置 **AI Proxy** 时,容易在渠道管理页遇到 **fetch failed**,或在模型测试中因 **base_url / 路径** 填写不当出现 **404**。本 PR 在错误提示、API 测试路径与文档上补齐「可操作的」指引。

---

## 变更摘要

### 1. 用户友好的替代配置指引

- 当 **AI Proxy 不可用或未配置** 时,返回**结构化错误信息**,明确提示可改用 **自定义请求地址**(`requestUrl` / `requestAuth`)直连厂商或自建网关。
- 对网络类错误补充**可诊断**的说明(例如 Docker 内 `localhost` 指向容器自身、需改用服务名或可访问主机等)。

### 2. 直连 API 配置与模型测试

- 更新 **模型测试 API**(`projects/app/src/pages/api/core/ai/model/test.ts`):支持在模型配置中填写 **自定义 `requestUrl` / `requestAuth`** 进行连通性测试,**不强制依赖 AI Proxy**。
- 厘清 **渠道路由** 与 **自定义请求 URL** 的适用场景:需要走统一代理/审计时用渠道 + Proxy;仅需直连官方或兼容 OpenAI 的地址时,用自定义 URL 即可。

### 3. 界面提示

- 在 **渠道管理** 相关界面(`Channel/index.tsx`)增加 **警告横幅**:当 AI Proxy 不可用或请求失败时,给出醒目提示,减少「空白失败」带来的困惑。

### 4. 文档与模板

- **中英文**文档(`document/content/docs/self-host/config/model/intro.mdx`、`intro.en.mdx`):说明如何**在不配置 AI Proxy** 的情况下配置模型与官方 API。
- 更新 **`projects/app/.env.template`**:对 `AIPROXY_*` 等变量补充注释与可选性说明。
- 提供 **官方 API + 自定义请求 URL** 的配置示例,便于对照填写。

### 5. 其他(与 #6525 修复一致)

- **`[...path].ts`**:AI Proxy 代理层错误处理增强。
- **`channel.ts`**:`normalizeChannelBaseUrl()` 等,避免误填完整 `.../chat/completions` 路径导致 404。

---

## 涉及文件

| 文件 | 说明 |
|------|------|
| `projects/app/src/pages/api/aiproxy/[...path].ts` | AI Proxy 代理错误处理 |
| `projects/app/src/pages/api/core/ai/model/test.ts` | 自定义 URL 模型测试 |
| `projects/app/src/pageComponents/account/model/Channel/index.tsx` | 渠道页警告 UI |
| `projects/app/src/web/core/ai/channel.ts` | base_url 规范化等 |
| `projects/app/.env.template` | 环境变量说明 |
| `document/content/docs/self-host/config/model/intro.mdx` | 中文文档 |
| `document/content/docs/self-host/config/model/intro.en.mdx` | 英文文档 |

---

## 如何测试

### 方式 A:不使用 AI Proxy(替代路径)

1. **不设置**或暂时注释 `AIPROXY_API_ENDPOINT` / `AIPROXY_API_TOKEN`(按你部署方式为准)。
2. 在模型配置中填写 **`requestUrl`**(如 `https://api.openai.com/v1`)与 **`requestAuth`**(API Key)。
3. 打开 **模型测试**,应能直接验证连通性,**无需** AI Proxy。

### 方式 B:使用 AI Proxy

1. 在 `.env` 中配置例如:
```env
AIPROXY_API_ENDPOINT=http://your-aiproxy-host:3010
AIPROXY_API_TOKEN=your-token
```
2. 进入 **渠道管理** 等依赖代理的功能,应能正常加载;若代理宕机,应看到**明确错误与横幅提示**,而非仅「fetch failed」。

### 回归建议

- [ ] 未配置 AI Proxy 时进入渠道页:有清晰错误/指引,无未处理异常。
- [ ] 使用官方兼容地址 + 自定义 `requestUrl`:模型测试成功。
- [ ] `base_url` 误带 `/chat/completions` 等后缀:保存/请求前被规范化,避免 404。

---

## 合并说明(供维护者)

- **不依赖** GitHub/GitLab 上再开 PR 时,可将本文件作为描述粘贴;本地仅作备份与评审用。
- 若远程使用 **私有仓库**,将本分支推送至 `origin` 后,在内部按流程评审合并即可。
42 changes: 42 additions & 0 deletions document/content/docs/self-host/config/model/intro.en.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,48 @@ If you find it tedious to configure models through the UI, you can use a configu

</Accordions>

## Configuration Without AI Proxy

If you don't want to deploy the AI Proxy service, you can configure models to use official APIs directly by setting custom request URLs. This approach is suitable for simple scenarios but lacks load balancing, logging, and other features.

### Configuration Steps

1. Navigate to the `Model Configuration` page and click "Add Model"
2. Fill in the basic model information (Model ID, Name, etc.)
3. In the `Custom Request URL` field, enter the complete API endpoint
4. In the `Custom Request Key` field, enter your API Key

<Alert icon="⚠️" context="warning">
Important Notes:

1. The custom request URL must be a **complete endpoint URL**, not just the BaseUrl. For example:
- LLM: `https://api.openai.com/v1/chat/completions`
- Embedding: `https://api.openai.com/v1/embeddings`

2. If both an AI Proxy channel and a custom request URL are configured, the system will prioritize the custom request URL.

3. When using custom request URLs, the model test function works directly without requiring the AI Proxy service.
</Alert>

### Example: Direct OpenAI API Usage

```json
{
"model": "gpt-4o-custom",
"metadata": {
"provider": "OpenAI",
"model": "gpt-4o",
"name": "GPT-4o (Direct)",
"requestUrl": "https://api.openai.com/v1/chat/completions",
"requestAuth": "sk-xxxxx",
"maxContext": 128000,
"maxResponse": 4096,
"isActive": true,
"isCustom": true
}
}
```

## Other

### Channel Priority
Expand Down
42 changes: 42 additions & 0 deletions document/content/docs/self-host/config/model/intro.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,48 @@ FastGPT 页面上提供了每类模型的简单测试,可以初步检查模型

</Accordions>

## 不使用 AI Proxy 的配置方法

如果你不想部署 AI Proxy 服务,也可以通过配置模型的自定义请求地址来直接使用官方 API。这种方式适合简单场景,但缺少负载均衡、日志等功能。

### 配置步骤

1. 在`模型配置`页面中,点击新增模型
2. 填写模型基本信息(模型 ID、名称等)
3. 在`自定义请求地址`字段填写完整的 API 地址
4. 在`自定义请求 Key` 字段填写 API Key

<Alert icon="⚠️" context="warning">
注意事项:

1. 自定义请求地址需要填写**完整的请求地址**,不是 BaseUrl。例如:
- LLM: `https://api.openai.com/v1/chat/completions`
- Embedding: `https://api.openai.com/v1/embeddings`

2. 如果同时配置了 AI Proxy 渠道和自定义请求地址,系统会优先使用自定义请求地址。

3. 使用自定义请求地址时,模型测试功能可以直接使用,不需要 AI Proxy 服务。
</Alert>

### 示例:直接使用 OpenAI API

```json
{
"model": "gpt-4o-custom",
"metadata": {
"provider": "OpenAI",
"model": "gpt-4o",
"name": "GPT-4o (Direct)",
"requestUrl": "https://api.openai.com/v1/chat/completions",
"requestAuth": "sk-xxxxx",
"maxContext": 128000,
"maxResponse": 4096,
"isActive": true,
"isCustom": true
}
}
```

## 其他

### 渠道优先级
Expand Down
7 changes: 5 additions & 2 deletions projects/app/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@ PLUGIN_TOKEN=token
CODE_SANDBOX_URL=http://localhost:3002
CODE_SANDBOX_TOKEN=codesandbox

# AI Proxy API
AIPROXY_API_ENDPOINT=https://localhost:3010
# AI Proxy API - Optional
# Configure these if you want to use the channel management feature for load balancing, logging, etc.
# If not configured, you can still use models by setting custom request URLs in the model configuration.
# See: https://doc.fastgpt.io/docs/self-host/config/model/intro/#configuration-without-ai-proxy
AIPROXY_API_ENDPOINT=http://localhost:3010
AIPROXY_API_TOKEN=token

# Agent sandbox
Expand Down
21 changes: 19 additions & 2 deletions projects/app/src/pageComponents/account/model/Channel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ import {
Box,
Flex,
Button,
HStack
HStack,
Alert,
AlertIcon,
AlertDescription
} from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import MyBox from '@fastgpt/web/components/common/MyBox';
Expand Down Expand Up @@ -53,7 +56,14 @@ const ChannelTable = ({ Tab }: { Tab: React.ReactNode }) => {
runAsync: refreshChannelList,
loading: loadingChannelList
} = useRequest(getChannelList, {
manual: false
manual: false,
onError: (error: any) => {
// Handle AI Proxy configuration errors with user-friendly messages
const errorMsg = error?.message || error;
if (errorMsg.includes('AI Proxy') || errorMsg.includes('AIPROXY')) {
setAiproxyError(errorMsg);
}
}
});

const { data: channelProviders = {} } = useRequest(getChannelProviders, {
Expand Down Expand Up @@ -88,6 +98,7 @@ const ChannelTable = ({ Tab }: { Tab: React.ReactNode }) => {
});

const [modelTestData, setTestModelData] = useState<{ channelId: number; models: string[] }>();
const [aiproxyError, setAiproxyError] = useState<string>();

const isLoading =
loadingChannelList ||
Expand All @@ -106,6 +117,12 @@ const ChannelTable = ({ Tab }: { Tab: React.ReactNode }) => {
</Button>
</Flex>
)}
{aiproxyError && (
<Alert status="warning" mb={2} borderRadius="md">
<AlertIcon />
<AlertDescription fontSize="sm">{aiproxyError}</AlertDescription>
</Alert>
)}
<MyBox flex={'1 0 0'} h={0} isLoading={isLoading}>
<TableContainer h={'100%'} overflowY={'auto'} fontSize={'sm'}>
<Table>
Expand Down
44 changes: 37 additions & 7 deletions projects/app/src/pages/api/aiproxy/[...path].ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { authSystemAdmin } from '@fastgpt/service/support/permission/user/auth';
import { Readable } from 'stream';
import { getErrText } from '@fastgpt/global/common/error/utils';

const baseUrl = process.env.AIPROXY_API_ENDPOINT;
const token = process.env.AIPROXY_API_TOKEN;
Expand All @@ -11,12 +11,42 @@ const endPathMap: Record<string, boolean> = {
'api/dashboardv2': true
};

const normalizeEndpoint = (endpoint: string) => endpoint.trim().replace(/\/+$/, '');

const formatAiproxyError = (error: unknown, endpoint?: string) => {
const errorText = getErrText(error);
const detail = endpoint ? ` (AIPROXY_API_ENDPOINT=${endpoint})` : '';
const tips: string[] = [];

if (endpoint) {
try {
const url = new URL(endpoint);
const hostname = url.hostname.toLowerCase();
const isLocalHost = ['localhost', '127.0.0.1', '0.0.0.0'].includes(hostname);

if (isLocalHost && url.protocol === 'https:') {
tips.push(
'If AI Proxy is local and not behind TLS, use http:// instead of https:// for AIPROXY_API_ENDPOINT.'
);
}
if (isLocalHost) {
tips.push(
'If FastGPT runs in Docker, localhost points to the FastGPT container itself. Use a container service name or a reachable host IP.'
);
}
} catch {}
}

return `AI Proxy request failed${detail}: ${errorText}${tips.length > 0 ? ` ${tips.join(' ')}` : ''}`;
};

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await authSystemAdmin({ req });

if (!baseUrl || !token) {
throw new Error('AIPROXY_API_ENDPOINT or AIPROXY_API_TOKEN is not set');
const errorMsg = 'AI Proxy service is not configured. Please set AIPROXY_API_ENDPOINT and AIPROXY_API_TOKEN environment variables to use channel management features. Alternatively, you can configure models with custom request URLs in the model configuration section.';
throw new Error(errorMsg);
}

const { path = [], ...query } = req.query as any;
Expand All @@ -29,8 +59,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// Determine whether the base path requires a trailing slash.
const basePath = `/${path?.join('/')}${endPathMap[path?.join('/')] ? '/' : ''}`;
const requestPath = queryStr ? `${basePath}?${queryStr}` : basePath;

const targetUrl = new URL(requestPath, baseUrl);
const targetUrl = new URL(requestPath, `${normalizeEndpoint(baseUrl)}/`);

const headers: Record<string, string> = {};
for (const [key, value] of Object.entries(req.headers)) {
Expand Down Expand Up @@ -66,9 +95,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
res.end();
}
} catch (error) {
jsonRes(res, {
code: 500,
error
res.status(500).json({
success: false,
message: formatAiproxyError(error, baseUrl),
data: null
});
}
}
Expand Down
11 changes: 11 additions & 0 deletions projects/app/src/pages/api/core/ai/model/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,20 @@ async function handler(

if (!modelData) return Promise.reject('Model not found');

// If channelId is provided, use channel-based routing (AI Proxy)
// Otherwise, use custom requestUrl/requestAuth if available
if (channelId) {
// When using AI Proxy channel, remove custom URL to force channel routing
delete modelData.requestUrl;
delete modelData.requestAuth;
} else {
// When not using channel, check if model has custom request configuration
// This allows direct API testing without AI Proxy
const hasCustomRequest = modelData.requestUrl && modelData.requestAuth;
if (!hasCustomRequest) {
// If no custom request is configured, will use default AI API configuration
logger.debug('Testing model without custom request URL, will use default AI API configuration');
}
}

const headers: Record<string, string> = channelId
Expand Down
Loading