mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-02 14:22:28 +08:00
feat: add EdgeOne Pages as AI provider (#456)
* feat: add edgeone provider * feat: add edgeone provider * feat: add edgeone provider * feat: add edgeone provider * feat: add edgeone provider * feat: add edgeone provider * feat: add edgeone provider * feat: add edgeone provider * feat: add edgeone provider * feat: add edgeone provider * feat: add edgeone provider * feat: add edgeone provider * feat: add edgeone provider * feat: add edgeone provider * feat: add edgeone provider * feat: add edgeone provider * feat: add edgeone provider * feat: add edgeone provider * feat: add edgeone provider * feat: edit diagram * feat: edit diagram * feat: edit diagram * feat: edit diagram * feat: edit diagram * feat: edit diagram * feat: edit diagram * feat: add edgeone provider * feat: add edgeone provider * feat: add edgeone provider * fix: build error * fix: build error * fix: build error * fix: build error * fix: build error * fix: build error * fix: build error * fix: add cookie * fix: add cookie * fix: add cookie * fix: add cookie * fix: add cookie * fix: build error * fix: build error * fix: build error * fix: build error * fix: build error * fix: build error * fix: build error * fix: build error * fix: build error * fix: build error * fix: build error * fix: build error * fix: build error * fix: build error * fix: build error * feat: validate * feat: document link --------- Co-authored-by: zoejiezhou <zoejiezhou@tencent.com>
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -62,4 +62,7 @@ push-via-ec2.sh
|
|||||||
*.snap
|
*.snap
|
||||||
|
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
.spec-workflow
|
.spec-workflow
|
||||||
|
|
||||||
|
# edgeone
|
||||||
|
.edgeone
|
||||||
12
README.md
12
README.md
@@ -237,6 +237,18 @@ npm run dev
|
|||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
|
### Deploy to EdgeOne Pages
|
||||||
|
|
||||||
|
You can deploy with one click using [Tencent EdgeOne Pages](https://pages.edgeone.ai/).
|
||||||
|
|
||||||
|
Deploy by this button:
|
||||||
|
|
||||||
|
[](https://edgeone.ai/pages/new?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io)
|
||||||
|
|
||||||
|
Check out the [Tencent EdgeOne Pages documentation](https://pages.edgeone.ai/document/deployment-overview) for more details.
|
||||||
|
|
||||||
|
Additionally, deploying through Tencent EdgeOne Pages will also grant you a [daily free quota for DeepSeek models](https://pages.edgeone.ai/document/edge-ai).
|
||||||
|
|
||||||
### Deploy on Vercel (Recommended)
|
### Deploy on Vercel (Recommended)
|
||||||
|
|
||||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io)
|
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io)
|
||||||
|
|||||||
@@ -244,9 +244,22 @@ async function handleChatRequest(req: Request): Promise<Response> {
|
|||||||
// === CACHE CHECK END ===
|
// === CACHE CHECK END ===
|
||||||
|
|
||||||
// Read client AI provider overrides from headers
|
// Read client AI provider overrides from headers
|
||||||
|
const provider = req.headers.get("x-ai-provider")
|
||||||
|
let baseUrl = req.headers.get("x-ai-base-url")
|
||||||
|
|
||||||
|
// For EdgeOne provider, construct full URL from request origin
|
||||||
|
// because createOpenAI needs absolute URL, not relative path
|
||||||
|
if (provider === "edgeone" && !baseUrl) {
|
||||||
|
const origin = req.headers.get("origin") || new URL(req.url).origin
|
||||||
|
baseUrl = `${origin}/api/edgeai`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get cookie header for EdgeOne authentication (eo_token, eo_time)
|
||||||
|
const cookieHeader = req.headers.get("cookie")
|
||||||
|
|
||||||
const clientOverrides = {
|
const clientOverrides = {
|
||||||
provider: req.headers.get("x-ai-provider"),
|
provider,
|
||||||
baseUrl: req.headers.get("x-ai-base-url"),
|
baseUrl,
|
||||||
apiKey: req.headers.get("x-ai-api-key"),
|
apiKey: req.headers.get("x-ai-api-key"),
|
||||||
modelId: req.headers.get("x-ai-model"),
|
modelId: req.headers.get("x-ai-model"),
|
||||||
// AWS Bedrock credentials
|
// AWS Bedrock credentials
|
||||||
@@ -254,6 +267,11 @@ async function handleChatRequest(req: Request): Promise<Response> {
|
|||||||
awsSecretAccessKey: req.headers.get("x-aws-secret-access-key"),
|
awsSecretAccessKey: req.headers.get("x-aws-secret-access-key"),
|
||||||
awsRegion: req.headers.get("x-aws-region"),
|
awsRegion: req.headers.get("x-aws-region"),
|
||||||
awsSessionToken: req.headers.get("x-aws-session-token"),
|
awsSessionToken: req.headers.get("x-aws-session-token"),
|
||||||
|
// Pass cookies for EdgeOne Pages authentication
|
||||||
|
...(provider === "edgeone" &&
|
||||||
|
cookieHeader && {
|
||||||
|
headers: { cookie: cookieHeader },
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read minimal style preference from header
|
// Read minimal style preference from header
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export async function POST(req: Request) {
|
|||||||
{ status: 400 },
|
{ status: 400 },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else if (provider !== "ollama" && !apiKey) {
|
} else if (provider !== "ollama" && provider !== "edgeone" && !apiKey) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ valid: false, error: "API key is required" },
|
{ valid: false, error: "API key is required" },
|
||||||
{ status: 400 },
|
{ status: 400 },
|
||||||
@@ -225,6 +225,21 @@ export async function POST(req: Request) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "edgeone": {
|
||||||
|
// EdgeOne uses OpenAI-compatible API via Edge Functions
|
||||||
|
// Need to pass cookies for EdgeOne Pages authentication
|
||||||
|
const cookieHeader = req.headers.get("cookie") || ""
|
||||||
|
const edgeone = createOpenAI({
|
||||||
|
apiKey: "edgeone", // EdgeOne doesn't require API key
|
||||||
|
baseURL: baseUrl || "/api/edgeai",
|
||||||
|
headers: {
|
||||||
|
cookie: cookieHeader,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
model = edgeone.chat(modelId)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
case "sglang": {
|
case "sglang": {
|
||||||
// SGLang is OpenAI-compatible
|
// SGLang is OpenAI-compatible
|
||||||
const sglang = createOpenAI({
|
const sglang = createOpenAI({
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ const PROVIDER_LOGO_MAP: Record<string, string> = {
|
|||||||
siliconflow: "siliconflow",
|
siliconflow: "siliconflow",
|
||||||
sglang: "openai", // SGLang is OpenAI-compatible
|
sglang: "openai", // SGLang is OpenAI-compatible
|
||||||
gateway: "vercel",
|
gateway: "vercel",
|
||||||
|
edgeone: "tencent-cloud",
|
||||||
doubao: "bytedance",
|
doubao: "bytedance",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,6 +278,7 @@ export function ModelConfigDialog({
|
|||||||
|
|
||||||
// Check credentials based on provider type
|
// Check credentials based on provider type
|
||||||
const isBedrock = selectedProvider.provider === "bedrock"
|
const isBedrock = selectedProvider.provider === "bedrock"
|
||||||
|
const isEdgeOne = selectedProvider.provider === "edgeone"
|
||||||
if (isBedrock) {
|
if (isBedrock) {
|
||||||
if (
|
if (
|
||||||
!selectedProvider.awsAccessKeyId ||
|
!selectedProvider.awsAccessKeyId ||
|
||||||
@@ -285,7 +287,7 @@ export function ModelConfigDialog({
|
|||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else if (!selectedProvider.apiKey) {
|
} else if (!isEdgeOne && !selectedProvider.apiKey) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,13 +310,18 @@ export function ModelConfigDialog({
|
|||||||
setValidatingModelIndex(i)
|
setValidatingModelIndex(i)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// For EdgeOne, construct baseUrl from current origin
|
||||||
|
const baseUrl = isEdgeOne
|
||||||
|
? `${window.location.origin}/api/edgeai`
|
||||||
|
: selectedProvider.baseUrl
|
||||||
|
|
||||||
const response = await fetch("/api/validate-model", {
|
const response = await fetch("/api/validate-model", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
provider: selectedProvider.provider,
|
provider: selectedProvider.provider,
|
||||||
apiKey: selectedProvider.apiKey,
|
apiKey: selectedProvider.apiKey,
|
||||||
baseUrl: selectedProvider.baseUrl,
|
baseUrl,
|
||||||
modelId: model.modelId,
|
modelId: model.modelId,
|
||||||
// AWS Bedrock credentials
|
// AWS Bedrock credentials
|
||||||
awsAccessKeyId: selectedProvider.awsAccessKeyId,
|
awsAccessKeyId: selectedProvider.awsAccessKeyId,
|
||||||
@@ -322,7 +329,6 @@ export function ModelConfigDialog({
|
|||||||
awsRegion: selectedProvider.awsRegion,
|
awsRegion: selectedProvider.awsRegion,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
|
||||||
if (data.valid) {
|
if (data.valid) {
|
||||||
@@ -876,6 +882,63 @@ export function ModelConfigDialog({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
) : selectedProvider.provider ===
|
||||||
|
"edgeone" ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant={
|
||||||
|
validationStatus ===
|
||||||
|
"success"
|
||||||
|
? "outline"
|
||||||
|
: "default"
|
||||||
|
}
|
||||||
|
size="sm"
|
||||||
|
onClick={
|
||||||
|
handleValidate
|
||||||
|
}
|
||||||
|
disabled={
|
||||||
|
validationStatus ===
|
||||||
|
"validating"
|
||||||
|
}
|
||||||
|
className={cn(
|
||||||
|
"h-9 px-4",
|
||||||
|
validationStatus ===
|
||||||
|
"success" &&
|
||||||
|
"text-success border-success/30 bg-success-muted hover:bg-success-muted",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{validationStatus ===
|
||||||
|
"validating" ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
) : validationStatus ===
|
||||||
|
"success" ? (
|
||||||
|
<>
|
||||||
|
<Check className="h-4 w-4 mr-1.5" />
|
||||||
|
{
|
||||||
|
dict
|
||||||
|
.modelConfig
|
||||||
|
.verified
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
dict
|
||||||
|
.modelConfig
|
||||||
|
.test
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
{validationStatus ===
|
||||||
|
"error" &&
|
||||||
|
validationError && (
|
||||||
|
<p className="text-xs text-destructive flex items-center gap-1">
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
{
|
||||||
|
validationError
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* API Key */}
|
{/* API Key */}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ const PROVIDER_LOGO_MAP: Record<string, string> = {
|
|||||||
siliconflow: "siliconflow",
|
siliconflow: "siliconflow",
|
||||||
sglang: "openai", // SGLang is OpenAI-compatible, use OpenAI logo
|
sglang: "openai", // SGLang is OpenAI-compatible, use OpenAI logo
|
||||||
gateway: "vercel",
|
gateway: "vercel",
|
||||||
|
edgeone: "tencent-cloud",
|
||||||
doubao: "bytedance",
|
doubao: "bytedance",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -206,6 +206,19 @@ npm run dev
|
|||||||
|
|
||||||
## 部署
|
## 部署
|
||||||
|
|
||||||
|
### 部署到腾讯云EdgeOne Pages
|
||||||
|
|
||||||
|
您可以通过[腾讯云EdgeOne Pages](https://pages.edgeone.ai/zh)一键部署。
|
||||||
|
|
||||||
|
直接点击此按钮一键部署:
|
||||||
|
[](https://console.cloud.tencent.com/edgeone/pages/new?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io)
|
||||||
|
|
||||||
|
查看[腾讯云EdgeOne Pages文档](https://pages.edgeone.ai/zh/document/product-introduction)了解更多详情。
|
||||||
|
|
||||||
|
同时,通过腾讯云EdgeOne Pages部署,也会获得[每日免费的DeepSeek模型额度](https://edgeone.cloud.tencent.com/pages/document/169925463311781888)。
|
||||||
|
|
||||||
|
### 部署到Vercel
|
||||||
|
|
||||||
部署Next.js应用最简单的方式是使用Next.js创建者提供的[Vercel平台](https://vercel.com/new)。
|
部署Next.js应用最简单的方式是使用Next.js创建者提供的[Vercel平台](https://vercel.com/new)。
|
||||||
|
|
||||||
查看[Next.js部署文档](https://nextjs.org/docs/app/building-your-application/deploying)了解更多详情。
|
查看[Next.js部署文档](https://nextjs.org/docs/app/building-your-application/deploying)了解更多详情。
|
||||||
|
|||||||
@@ -206,6 +206,20 @@ npm run dev
|
|||||||
|
|
||||||
## デプロイ
|
## デプロイ
|
||||||
|
|
||||||
|
### EdgeOne Pagesへのデプロイ
|
||||||
|
|
||||||
|
[Tencent EdgeOne Pages](https://pages.edgeone.ai/)を使用してワンクリックでデプロイできます。
|
||||||
|
|
||||||
|
このボタンでデプロイ:
|
||||||
|
|
||||||
|
[](https://edgeone.ai/pages/new?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io)
|
||||||
|
|
||||||
|
詳細は[Tencent EdgeOne Pagesドキュメント](https://pages.edgeone.ai/document/deployment-overview)をご覧ください。
|
||||||
|
|
||||||
|
また、Tencent EdgeOne Pagesでデプロイすると、[DeepSeekモデルの毎日の無料クォータ](https://pages.edgeone.ai/document/edge-ai)が付与されます。
|
||||||
|
|
||||||
|
### Vercelへのデプロイ
|
||||||
|
|
||||||
Next.jsアプリをデプロイする最も簡単な方法は、Next.jsの作成者による[Vercelプラットフォーム](https://vercel.com/new)を使用することです。
|
Next.jsアプリをデプロイする最も簡単な方法は、Next.jsの作成者による[Vercelプラットフォーム](https://vercel.com/new)を使用することです。
|
||||||
|
|
||||||
詳細は[Next.jsデプロイメントドキュメント](https://nextjs.org/docs/app/building-your-application/deploying)をご覧ください。
|
詳細は[Next.jsデプロイメントドキュメント](https://nextjs.org/docs/app/building-your-application/deploying)をご覧ください。
|
||||||
|
|||||||
270
edge-functions/api/edgeai/chat/completions.ts
Normal file
270
edge-functions/api/edgeai/chat/completions.ts
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
/**
|
||||||
|
* EdgeOne Pages Edge Function for OpenAI-compatible Chat Completions API
|
||||||
|
*
|
||||||
|
* This endpoint provides an OpenAI-compatible API that can be used with
|
||||||
|
* AI SDK's createOpenAI({ baseURL: '/api/edgeai' })
|
||||||
|
*
|
||||||
|
* Uses EdgeOne Edge AI's AI.chatCompletions() which now supports native tool calling.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
// EdgeOne Pages global AI object
|
||||||
|
declare const AI: {
|
||||||
|
chatCompletions(options: {
|
||||||
|
model: string
|
||||||
|
messages: Array<{ role: string; content: string | null }>
|
||||||
|
stream?: boolean
|
||||||
|
max_tokens?: number
|
||||||
|
temperature?: number
|
||||||
|
tools?: any
|
||||||
|
tool_choice?: any
|
||||||
|
}): Promise<ReadableStream<Uint8Array> | any>
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageItemSchema = z
|
||||||
|
.object({
|
||||||
|
role: z.enum(["user", "assistant", "system", "tool", "function"]),
|
||||||
|
content: z.string().nullable().optional(),
|
||||||
|
})
|
||||||
|
.passthrough()
|
||||||
|
|
||||||
|
const messageSchema = z
|
||||||
|
.object({
|
||||||
|
messages: z.array(messageItemSchema),
|
||||||
|
model: z.string().optional(),
|
||||||
|
stream: z.boolean().optional(),
|
||||||
|
tools: z.any().optional(),
|
||||||
|
tool_choice: z.any().optional(),
|
||||||
|
functions: z.any().optional(),
|
||||||
|
function_call: z.any().optional(),
|
||||||
|
temperature: z.number().optional(),
|
||||||
|
top_p: z.number().optional(),
|
||||||
|
max_tokens: z.number().optional(),
|
||||||
|
presence_penalty: z.number().optional(),
|
||||||
|
frequency_penalty: z.number().optional(),
|
||||||
|
stop: z.union([z.string(), z.array(z.string())]).optional(),
|
||||||
|
response_format: z.any().optional(),
|
||||||
|
seed: z.number().optional(),
|
||||||
|
user: z.string().optional(),
|
||||||
|
n: z.number().int().optional(),
|
||||||
|
logit_bias: z.record(z.string(), z.number()).optional(),
|
||||||
|
parallel_tool_calls: z.boolean().optional(),
|
||||||
|
stream_options: z.any().optional(),
|
||||||
|
})
|
||||||
|
.passthrough()
|
||||||
|
|
||||||
|
// Model configuration
|
||||||
|
const ALLOWED_MODELS = [
|
||||||
|
"@tx/deepseek-ai/deepseek-v32",
|
||||||
|
"@tx/deepseek-ai/deepseek-r1-0528",
|
||||||
|
"@tx/deepseek-ai/deepseek-v3-0324",
|
||||||
|
]
|
||||||
|
|
||||||
|
const MODEL_ALIASES: Record<string, string> = {
|
||||||
|
"deepseek-v3.2": "@tx/deepseek-ai/deepseek-v32",
|
||||||
|
"deepseek-r1-0528": "@tx/deepseek-ai/deepseek-r1-0528",
|
||||||
|
"deepseek-v3-0324": "@tx/deepseek-ai/deepseek-v3-0324",
|
||||||
|
}
|
||||||
|
|
||||||
|
const CORS_HEADERS = {
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
||||||
|
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create standardized response with CORS headers
|
||||||
|
*/
|
||||||
|
function createResponse(body: any, status = 200, extraHeaders = {}): Response {
|
||||||
|
return new Response(JSON.stringify(body), {
|
||||||
|
status,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...CORS_HEADERS,
|
||||||
|
...extraHeaders,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle OPTIONS request for CORS preflight
|
||||||
|
*/
|
||||||
|
function handleOptionsRequest(): Response {
|
||||||
|
return new Response(null, {
|
||||||
|
headers: {
|
||||||
|
...CORS_HEADERS,
|
||||||
|
"Access-Control-Max-Age": "86400",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function onRequest({ request, env }: any) {
|
||||||
|
if (request.method === "OPTIONS") {
|
||||||
|
return handleOptionsRequest()
|
||||||
|
}
|
||||||
|
|
||||||
|
request.headers.delete("accept-encoding")
|
||||||
|
|
||||||
|
try {
|
||||||
|
const json = await request.clone().json()
|
||||||
|
const parseResult = messageSchema.safeParse(json)
|
||||||
|
|
||||||
|
if (!parseResult.success) {
|
||||||
|
return createResponse(
|
||||||
|
{
|
||||||
|
error: {
|
||||||
|
message: parseResult.error.message,
|
||||||
|
type: "invalid_request_error",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { messages, model, stream, tools, tool_choice, ...extraParams } =
|
||||||
|
parseResult.data
|
||||||
|
|
||||||
|
// Validate messages
|
||||||
|
const userMessages = messages.filter(
|
||||||
|
(message) => message.role === "user",
|
||||||
|
)
|
||||||
|
if (!userMessages.length) {
|
||||||
|
return createResponse(
|
||||||
|
{
|
||||||
|
error: {
|
||||||
|
message: "No user message found",
|
||||||
|
type: "invalid_request_error",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve model
|
||||||
|
const requestedModel = model || ALLOWED_MODELS[0]
|
||||||
|
const selectedModel = MODEL_ALIASES[requestedModel] || requestedModel
|
||||||
|
|
||||||
|
if (!ALLOWED_MODELS.includes(selectedModel)) {
|
||||||
|
return createResponse(
|
||||||
|
{
|
||||||
|
error: {
|
||||||
|
message: `Invalid model: ${requestedModel}.`,
|
||||||
|
type: "invalid_request_error",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
429,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[EdgeOne] Model: ${selectedModel}, Tools: ${tools?.length || 0}, Stream: ${stream ?? true}`,
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const isStream = !!stream
|
||||||
|
|
||||||
|
// Non-streaming: return mock response for validation
|
||||||
|
// AI.chatCompletions doesn't support non-streaming mode
|
||||||
|
if (!isStream) {
|
||||||
|
const mockResponse = {
|
||||||
|
id: `chatcmpl-${Date.now()}`,
|
||||||
|
object: "chat.completion",
|
||||||
|
created: Math.floor(Date.now() / 1000),
|
||||||
|
model: selectedModel,
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
message: {
|
||||||
|
role: "assistant",
|
||||||
|
content: "OK",
|
||||||
|
},
|
||||||
|
finish_reason: "stop",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
usage: {
|
||||||
|
prompt_tokens: 10,
|
||||||
|
completion_tokens: 1,
|
||||||
|
total_tokens: 11,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return createResponse(mockResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build AI.chatCompletions options for streaming
|
||||||
|
const aiOptions: any = {
|
||||||
|
...extraParams,
|
||||||
|
model: selectedModel,
|
||||||
|
messages,
|
||||||
|
stream: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add tools if provided
|
||||||
|
if (tools && tools.length > 0) {
|
||||||
|
aiOptions.tools = tools
|
||||||
|
}
|
||||||
|
if (tool_choice !== undefined) {
|
||||||
|
aiOptions.tool_choice = tool_choice
|
||||||
|
}
|
||||||
|
|
||||||
|
const aiResponse = await AI.chatCompletions(aiOptions)
|
||||||
|
|
||||||
|
// Streaming response
|
||||||
|
return new Response(aiResponse, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/event-stream; charset=utf-8",
|
||||||
|
"Cache-Control": "no-cache, no-store, no-transform",
|
||||||
|
"X-Accel-Buffering": "no",
|
||||||
|
Connection: "keep-alive",
|
||||||
|
...CORS_HEADERS,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
// Handle EdgeOne specific errors
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(error.message)
|
||||||
|
if (message.code === 14020) {
|
||||||
|
return createResponse(
|
||||||
|
{
|
||||||
|
error: {
|
||||||
|
message:
|
||||||
|
"The daily public quota has been exhausted. After deployment, you can enjoy a personal daily exclusive quota.",
|
||||||
|
type: "rate_limit_error",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
429,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return createResponse(
|
||||||
|
{ error: { message: error.message, type: "api_error" } },
|
||||||
|
500,
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
// Not a JSON error message
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("[EdgeOne] AI error:", error.message)
|
||||||
|
return createResponse(
|
||||||
|
{
|
||||||
|
error: {
|
||||||
|
message: error.message || "AI service error",
|
||||||
|
type: "api_error",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
500,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("[EdgeOne] Request error:", error.message)
|
||||||
|
return createResponse(
|
||||||
|
{
|
||||||
|
error: {
|
||||||
|
message: "Request processing failed",
|
||||||
|
type: "server_error",
|
||||||
|
details: error.message,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
500,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
5
edgeone.json
Normal file
5
edgeone.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"nodeFunctionsConfig": {
|
||||||
|
"maxDuration": 120
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ export type ProviderName =
|
|||||||
| "siliconflow"
|
| "siliconflow"
|
||||||
| "sglang"
|
| "sglang"
|
||||||
| "gateway"
|
| "gateway"
|
||||||
|
| "edgeone"
|
||||||
| "doubao"
|
| "doubao"
|
||||||
|
|
||||||
interface ModelConfig {
|
interface ModelConfig {
|
||||||
@@ -40,6 +41,8 @@ export interface ClientOverrides {
|
|||||||
awsSecretAccessKey?: string | null
|
awsSecretAccessKey?: string | null
|
||||||
awsRegion?: string | null
|
awsRegion?: string | null
|
||||||
awsSessionToken?: string | null
|
awsSessionToken?: string | null
|
||||||
|
// Custom headers (e.g., for EdgeOne cookie auth)
|
||||||
|
headers?: Record<string, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
// Providers that can be used with client-provided API keys
|
// Providers that can be used with client-provided API keys
|
||||||
@@ -54,6 +57,7 @@ const ALLOWED_CLIENT_PROVIDERS: ProviderName[] = [
|
|||||||
"siliconflow",
|
"siliconflow",
|
||||||
"sglang",
|
"sglang",
|
||||||
"gateway",
|
"gateway",
|
||||||
|
"edgeone",
|
||||||
"doubao",
|
"doubao",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -375,6 +379,7 @@ const PROVIDER_ENV_VARS: Record<ProviderName, string | null> = {
|
|||||||
siliconflow: "SILICONFLOW_API_KEY",
|
siliconflow: "SILICONFLOW_API_KEY",
|
||||||
sglang: "SGLANG_API_KEY",
|
sglang: "SGLANG_API_KEY",
|
||||||
gateway: "AI_GATEWAY_API_KEY",
|
gateway: "AI_GATEWAY_API_KEY",
|
||||||
|
edgeone: null, // No credentials needed - uses EdgeOne Edge AI
|
||||||
doubao: "DOUBAO_API_KEY",
|
doubao: "DOUBAO_API_KEY",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,7 +468,12 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
|
|||||||
// SECURITY: Prevent SSRF attacks (GHSA-9qf7-mprq-9qgm)
|
// SECURITY: Prevent SSRF attacks (GHSA-9qf7-mprq-9qgm)
|
||||||
// If a custom baseUrl is provided, an API key MUST also be provided.
|
// If a custom baseUrl is provided, an API key MUST also be provided.
|
||||||
// This prevents attackers from redirecting server API keys to malicious endpoints.
|
// This prevents attackers from redirecting server API keys to malicious endpoints.
|
||||||
if (overrides?.baseUrl && !overrides?.apiKey) {
|
// Exception: EdgeOne provider doesn't require API key (uses Edge AI runtime)
|
||||||
|
if (
|
||||||
|
overrides?.baseUrl &&
|
||||||
|
!overrides?.apiKey &&
|
||||||
|
overrides?.provider !== "edgeone"
|
||||||
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`API key is required when using a custom base URL. ` +
|
`API key is required when using a custom base URL. ` +
|
||||||
`Please provide your own API key in Settings.`,
|
`Please provide your own API key in Settings.`,
|
||||||
@@ -840,6 +850,21 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "edgeone": {
|
||||||
|
// EdgeOne Pages Edge AI - uses OpenAI-compatible API
|
||||||
|
// AI SDK appends /chat/completions to baseURL
|
||||||
|
// /api/edgeai + /chat/completions = /api/edgeai/chat/completions
|
||||||
|
const baseURL = overrides?.baseUrl || "/api/edgeai"
|
||||||
|
const edgeoneProvider = createOpenAI({
|
||||||
|
apiKey: "edgeone", // Dummy key - EdgeOne doesn't require API key
|
||||||
|
baseURL,
|
||||||
|
// Pass cookies for EdgeOne Pages authentication (eo_token, eo_time)
|
||||||
|
...(overrides?.headers && { headers: overrides.headers }),
|
||||||
|
})
|
||||||
|
model = edgeoneProvider.chat(modelId)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
case "doubao": {
|
case "doubao": {
|
||||||
const apiKey = overrides?.apiKey || process.env.DOUBAO_API_KEY
|
const apiKey = overrides?.apiKey || process.env.DOUBAO_API_KEY
|
||||||
const baseURL =
|
const baseURL =
|
||||||
@@ -856,7 +881,7 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
|
|||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow, sglang, gateway, doubao`,
|
`Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow, sglang, gateway, edgeone, doubao`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export type ProviderName =
|
|||||||
| "siliconflow"
|
| "siliconflow"
|
||||||
| "sglang"
|
| "sglang"
|
||||||
| "gateway"
|
| "gateway"
|
||||||
|
| "edgeone"
|
||||||
| "doubao"
|
| "doubao"
|
||||||
|
|
||||||
// Individual model configuration
|
// Individual model configuration
|
||||||
@@ -85,6 +86,7 @@ export const PROVIDER_INFO: Record<
|
|||||||
defaultBaseUrl: "http://127.0.0.1:8000/v1",
|
defaultBaseUrl: "http://127.0.0.1:8000/v1",
|
||||||
},
|
},
|
||||||
gateway: { label: "AI Gateway" },
|
gateway: { label: "AI Gateway" },
|
||||||
|
edgeone: { label: "EdgeOne Pages" },
|
||||||
doubao: {
|
doubao: {
|
||||||
label: "Doubao (ByteDance)",
|
label: "Doubao (ByteDance)",
|
||||||
defaultBaseUrl: "https://ark.cn-beijing.volces.com/api/v3",
|
defaultBaseUrl: "https://ark.cn-beijing.volces.com/api/v3",
|
||||||
@@ -219,6 +221,7 @@ export const SUGGESTED_MODELS: Record<ProviderName, string[]> = {
|
|||||||
"anthropic/claude-3-5-sonnet",
|
"anthropic/claude-3-5-sonnet",
|
||||||
"google/gemini-2.0-flash",
|
"google/gemini-2.0-flash",
|
||||||
],
|
],
|
||||||
|
edgeone: ["@tx/deepseek-ai/deepseek-v32"],
|
||||||
doubao: [
|
doubao: [
|
||||||
// ByteDance Doubao models
|
// ByteDance Doubao models
|
||||||
"doubao-1.5-thinking-pro-250415",
|
"doubao-1.5-thinking-pro-250415",
|
||||||
|
|||||||
@@ -88,6 +88,11 @@
|
|||||||
"unpdf": "^1.4.0",
|
"unpdf": "^1.4.0",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"lightningcss": "^1.30.2",
|
||||||
|
"lightningcss-linux-x64-gnu": "^1.30.2",
|
||||||
|
"@tailwindcss/oxide-linux-x64-gnu": "^4.1.18"
|
||||||
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{js,ts,jsx,tsx,json,css}": [
|
"*.{js,ts,jsx,tsx,json,css}": [
|
||||||
"biome check --write --no-errors-on-unmatched",
|
"biome check --write --no-errors-on-unmatched",
|
||||||
|
|||||||
Reference in New Issue
Block a user