完善文档页文本向量与重排序调用支持
This commit is contained in:
+19
-5
@@ -540,7 +540,7 @@ export function App() {
|
||||
try {
|
||||
const response = await createApiKey(token, {
|
||||
name: apiKeyForm.name,
|
||||
scopes: ['chat', 'image', 'video'],
|
||||
scopes: ['chat', 'embedding', 'rerank', 'image', 'video'],
|
||||
expiresAt: apiKeyForm.expiresAt ? new Date(apiKeyForm.expiresAt).toISOString() : undefined,
|
||||
});
|
||||
setApiKeySecret(response.secret);
|
||||
@@ -907,11 +907,20 @@ export function App() {
|
||||
|
||||
async function submitTask(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
const credential = apiKeySecret || token;
|
||||
const selectedApiKeySecret = selectedPlaygroundApiKeyId ? apiKeySecretsById[selectedPlaygroundApiKeyId] ?? '' : '';
|
||||
const fallbackApiKeySecret = apiKeys.find((item) => Boolean(apiKeySecretsById[item.id]))?.id;
|
||||
const credential = selectedApiKeySecret || (fallbackApiKeySecret ? apiKeySecretsById[fallbackApiKeySecret] : '') || apiKeySecret || token;
|
||||
const credentialLabel = selectedApiKeySecret || fallbackApiKeySecret || apiKeySecret ? '本地 API Key' : '当前 Access Token';
|
||||
setCoreState('loading');
|
||||
setCoreMessage('');
|
||||
try {
|
||||
const response = await runTask(credential, taskForm);
|
||||
if (response.localOnly) {
|
||||
setTaskResult(response.task);
|
||||
setCoreState('ready');
|
||||
setCoreMessage(`${taskForm.kind} 已通过 ${credentialLabel} 完成 simulation。`);
|
||||
return;
|
||||
}
|
||||
const syncTask = (detail: GatewayTask) => {
|
||||
setTaskResult(detail);
|
||||
setTasks((current) => [detail, ...current.filter((item) => item.id !== detail.id)]);
|
||||
@@ -921,7 +930,7 @@ export function App() {
|
||||
setTasks((current) => [detail, ...current.filter((item) => item.id !== detail.id)]);
|
||||
invalidateDataKeys('tasks', 'wallet', 'walletTransactions');
|
||||
setCoreState('ready');
|
||||
setCoreMessage(`${taskForm.kind} 已通过 ${apiKeySecret ? '本地 API Key' : '当前 Access Token'} 完成 simulation。`);
|
||||
setCoreMessage(`${taskForm.kind} 已通过 ${credentialLabel} 完成 simulation。`);
|
||||
} catch (err) {
|
||||
setCoreState('error');
|
||||
setCoreMessage(err instanceof Error ? err.message : '测试任务失败');
|
||||
@@ -1170,12 +1179,16 @@ export function App() {
|
||||
{activePage === 'docs' && (
|
||||
<ApiDocsPage
|
||||
activeDocSection={apiDocSection}
|
||||
apiKeySecret={apiKeySecret}
|
||||
apiKeySecretsById={apiKeySecretsById}
|
||||
apiKeys={apiKeys}
|
||||
canRun={isAuthenticated}
|
||||
coreMessage={coreMessage}
|
||||
coreState={coreState}
|
||||
selectedApiKeyId={selectedPlaygroundApiKeyId}
|
||||
taskForm={taskForm}
|
||||
taskResult={taskResult}
|
||||
onApiKeyChange={setSelectedPlaygroundApiKeyId}
|
||||
onCreateApiKey={openApiKeyCreation}
|
||||
onDocSectionChange={navigateApiDocSection}
|
||||
onLogin={showLogin}
|
||||
onSubmitTask={submitTask}
|
||||
@@ -1318,7 +1331,8 @@ function dataKeysForRoute(
|
||||
? ['modelCatalog']
|
||||
: ['publicCatalog'];
|
||||
}
|
||||
if (activePage === 'home' || activePage === 'docs') return [];
|
||||
if (activePage === 'docs') return isAuthenticated ? ['playgroundApiKeys'] : [];
|
||||
if (activePage === 'home') return [];
|
||||
if (!isAuthenticated) return [];
|
||||
|
||||
if (activePage === 'workspace') {
|
||||
|
||||
@@ -547,6 +547,39 @@ export async function createChatTask(
|
||||
});
|
||||
}
|
||||
|
||||
export async function createCompatibleChatCompletion(
|
||||
token: string,
|
||||
input: { model: string; messages: Array<Record<string, unknown>>; runMode?: string; simulation?: boolean; stream?: boolean },
|
||||
): Promise<Record<string, unknown>> {
|
||||
return request<Record<string, unknown>>('/v1/chat/completions', {
|
||||
body: input,
|
||||
method: 'POST',
|
||||
token,
|
||||
});
|
||||
}
|
||||
|
||||
export async function createEmbedding(
|
||||
token: string,
|
||||
input: { model: string; input: string | string[]; dimensions?: number; runMode?: string; simulation?: boolean },
|
||||
): Promise<Record<string, unknown>> {
|
||||
return request<Record<string, unknown>>('/v1/embeddings', {
|
||||
body: input,
|
||||
method: 'POST',
|
||||
token,
|
||||
});
|
||||
}
|
||||
|
||||
export async function createRerank(
|
||||
token: string,
|
||||
input: { model: string; query: string; documents: string[]; top_n?: number; runMode?: string; simulation?: boolean },
|
||||
): Promise<Record<string, unknown>> {
|
||||
return request<Record<string, unknown>>('/v1/reranks', {
|
||||
body: input,
|
||||
method: 'POST',
|
||||
token,
|
||||
});
|
||||
}
|
||||
|
||||
export async function streamChatCompletions(
|
||||
token: string,
|
||||
input: { model: string; messages: Array<Record<string, unknown>>; simulation?: boolean },
|
||||
|
||||
@@ -1,8 +1,45 @@
|
||||
import type { GatewayTask } from '@easyai-ai-gateway/contracts';
|
||||
import { createChatTask, createImageEditTask, createImageGenerationTask } from '../api';
|
||||
import { createCompatibleChatCompletion, createEmbedding, createImageEditTask, createImageGenerationTask, createRerank } from '../api';
|
||||
import type { TaskForm } from '../types';
|
||||
|
||||
export function runTask(token: string, task: TaskForm): Promise<{ task: GatewayTask; next: Record<string, string> }> {
|
||||
export interface RunTaskResponse {
|
||||
localOnly?: boolean;
|
||||
next?: Record<string, string>;
|
||||
task: GatewayTask;
|
||||
}
|
||||
|
||||
export async function runTask(token: string, task: TaskForm): Promise<RunTaskResponse> {
|
||||
if (task.kind === 'chat.completions') {
|
||||
const result = await createCompatibleChatCompletion(token, {
|
||||
model: task.model,
|
||||
runMode: 'simulation',
|
||||
simulation: true,
|
||||
stream: false,
|
||||
messages: [{ role: 'user', content: task.prompt }],
|
||||
});
|
||||
return { localOnly: true, task: compatibleTask(task, result) };
|
||||
}
|
||||
if (task.kind === 'embeddings') {
|
||||
const result = await createEmbedding(token, {
|
||||
model: task.model,
|
||||
input: embeddingInput(task.prompt),
|
||||
dimensions: task.dimensions,
|
||||
runMode: 'simulation',
|
||||
simulation: true,
|
||||
});
|
||||
return { localOnly: true, task: compatibleTask(task, result) };
|
||||
}
|
||||
if (task.kind === 'reranks') {
|
||||
const result = await createRerank(token, {
|
||||
model: task.model,
|
||||
query: task.prompt,
|
||||
documents: rerankDocuments(task.documents),
|
||||
top_n: task.topN,
|
||||
runMode: 'simulation',
|
||||
simulation: true,
|
||||
});
|
||||
return { localOnly: true, task: compatibleTask(task, result) };
|
||||
}
|
||||
if (task.kind === 'images.generations') {
|
||||
return createImageGenerationTask(token, {
|
||||
model: task.model,
|
||||
@@ -23,10 +60,78 @@ export function runTask(token: string, task: TaskForm): Promise<{ task: GatewayT
|
||||
simulation: true,
|
||||
});
|
||||
}
|
||||
return createChatTask(token, {
|
||||
throw new Error(`Unsupported task kind: ${task.kind}`);
|
||||
}
|
||||
|
||||
function compatibleTask(task: TaskForm, result: Record<string, unknown>): GatewayTask {
|
||||
const now = new Date().toISOString();
|
||||
return {
|
||||
id: `docs-${task.kind}-${Date.now()}`,
|
||||
asyncMode: false,
|
||||
createdAt: now,
|
||||
finishedAt: now,
|
||||
kind: task.kind,
|
||||
model: task.model,
|
||||
modelType: modelTypeForKind(task.kind),
|
||||
request: requestSnapshot(task),
|
||||
result,
|
||||
runMode: 'simulation',
|
||||
status: 'succeeded',
|
||||
updatedAt: now,
|
||||
userId: 'docs-runner',
|
||||
};
|
||||
}
|
||||
|
||||
function requestSnapshot(task: TaskForm): Record<string, unknown> {
|
||||
if (task.kind === 'embeddings') {
|
||||
return {
|
||||
model: task.model,
|
||||
input: embeddingInput(task.prompt),
|
||||
dimensions: task.dimensions,
|
||||
runMode: 'simulation',
|
||||
simulation: true,
|
||||
};
|
||||
}
|
||||
if (task.kind === 'reranks') {
|
||||
return {
|
||||
model: task.model,
|
||||
query: task.prompt,
|
||||
documents: rerankDocuments(task.documents),
|
||||
top_n: task.topN,
|
||||
runMode: 'simulation',
|
||||
simulation: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
model: task.model,
|
||||
messages: [{ role: 'user', content: task.prompt }],
|
||||
runMode: 'simulation',
|
||||
simulation: true,
|
||||
messages: [{ role: 'user', content: task.prompt }],
|
||||
});
|
||||
stream: false,
|
||||
};
|
||||
}
|
||||
|
||||
function embeddingInput(prompt: string) {
|
||||
const lines = splitLines(prompt);
|
||||
return lines.length > 1 ? lines : (lines[0] ?? prompt);
|
||||
}
|
||||
|
||||
function rerankDocuments(value?: string) {
|
||||
const documents = splitLines(value ?? '');
|
||||
return documents.length ? documents : ['AI Gateway 提供 OpenAI 兼容接口。', '图片生成任务支持异步队列。'];
|
||||
}
|
||||
|
||||
function splitLines(value: string) {
|
||||
return value
|
||||
.split(/\n+/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function modelTypeForKind(kind: TaskForm['kind']) {
|
||||
if (kind === 'embeddings') return 'text_embedding';
|
||||
if (kind === 'reranks') return 'text_rerank';
|
||||
if (kind === 'images.generations') return 'image_generate';
|
||||
if (kind === 'images.edits') return 'image_edit';
|
||||
return 'text_generate';
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ export const adminPages = [
|
||||
|
||||
export const apiDocPages = [
|
||||
{ title: '鉴权与限流', path: '/docs/auth', description: '本地账号、JWT、OpenAPI Key、TPM/RPM/并发限制和错误码。' },
|
||||
{ title: 'Chat / Responses', path: '/docs/api/chat', description: '对话、stream、结构化输出、取消请求和示例代码。' },
|
||||
{ title: 'Chat / Embeddings / Reranks', path: '/docs/api/chat', description: '对话、文本向量、重排序、鉴权和示例代码。' },
|
||||
{ title: '图片 / 视频', path: '/docs/api/media', description: '生图、图像编辑、生视频、任务进度和结果取回。' },
|
||||
{ title: '在线调用测试', path: '/docs/playground', description: '选择模型和 API Key,编辑参数,查看实时响应和 billings。' },
|
||||
];
|
||||
|
||||
@@ -1,33 +1,52 @@
|
||||
import { useMemo, type FormEvent } from 'react';
|
||||
import type { GatewayTask } from '@easyai-ai-gateway/contracts';
|
||||
import { BookOpen, Play, Search, Send } from 'lucide-react';
|
||||
import { useEffect, useMemo, type FormEvent } from 'react';
|
||||
import type { GatewayApiKey, GatewayTask } from '@easyai-ai-gateway/contracts';
|
||||
import { BookOpen, KeyRound, Play, Search, Send } from 'lucide-react';
|
||||
import { Badge, Button, Select, Textarea } from '../components/ui';
|
||||
import type { ApiDocSection, LoadState, TaskForm } from '../types';
|
||||
import type { ApiDocSection, LoadState, TaskForm, TaskKind } from '../types';
|
||||
import { ApiKeySelect, apiKeyNoticeText, resolveSelectedApiKeyId } from './playground-shared';
|
||||
|
||||
const docs: Array<{ key: ApiDocSection; group: string; method: string; path: string; title: string }> = [
|
||||
{ key: 'chat', group: '聊天(Chat)', method: 'POST', path: '/v1/chat/completions', title: 'Chat(聊天)' },
|
||||
{ key: 'imageGeneration', group: '图片', method: 'POST', path: '/v1/images/generations', title: '创建图片' },
|
||||
{ key: 'imageEdit', group: '图片', method: 'POST', path: '/v1/images/edits', title: '编辑图片' },
|
||||
{ key: 'pricing', group: '计费', method: 'POST', path: '/api/v1/pricing/estimate', title: '价格预估' },
|
||||
{ key: 'files', group: '文件', method: 'POST', path: '/v1/files/upload', title: '上传文件' },
|
||||
interface ApiDocItem {
|
||||
group: string;
|
||||
key: ApiDocSection;
|
||||
kind?: TaskKind;
|
||||
lead: string;
|
||||
method: string;
|
||||
path: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const docs: ApiDocItem[] = [
|
||||
{ key: 'chat', group: '文本', kind: 'chat.completions', method: 'POST', path: '/v1/chat/completions', title: 'Chat Completions', lead: 'OpenAI 兼容的对话接口,支持本地 API Key 授权、simulation 测试和非流式/流式响应。' },
|
||||
{ key: 'embeddings', group: '文本', kind: 'embeddings', method: 'POST', path: '/v1/embeddings', title: '文本向量 Embeddings', lead: 'OpenAI 兼容的文本向量接口,可直接用 input 数组或字符串生成 embedding,API Key 需要 embedding 权限。' },
|
||||
{ key: 'reranks', group: '文本', kind: 'reranks', method: 'POST', path: '/v1/reranks', title: '文本重排序 Reranks', lead: 'OpenAI 风格的重排序接口,传入 query 和 documents 后返回 relevance_score,API Key 需要 rerank 权限。' },
|
||||
{ key: 'imageGeneration', group: '图片', kind: 'images.generations', method: 'POST', path: '/v1/images/generations', title: '创建图片', lead: 'OpenAI 兼容的图片生成接口,支持 prompt、size、quality 和 simulation 测试。' },
|
||||
{ key: 'imageEdit', group: '图片', kind: 'images.edits', method: 'POST', path: '/v1/images/edits', title: '编辑图片', lead: 'OpenAI 兼容的图片编辑接口,支持 image、mask、prompt 和 simulation 测试。' },
|
||||
{ key: 'pricing', group: '计费', method: 'POST', path: '/api/v1/pricing/estimate', title: '价格预估', lead: '按请求体估算输入输出 token、模型倍率和折扣后的预估费用。' },
|
||||
{ key: 'files', group: '文件', method: 'POST', path: '/v1/files/upload', title: '上传文件', lead: '上传在线测试所需的图片、音频或视频资源,后续请求可复用返回的文件 URL。' },
|
||||
];
|
||||
|
||||
const guideItems = ['获取 Base URL 和 API Key', '通知设置-WebHook 参数介绍', '错误码', '测试模式'];
|
||||
|
||||
const taskKindOptions = [
|
||||
['chat.completions', 'Chat'],
|
||||
['embeddings', '文本向量'],
|
||||
['reranks', '重排序'],
|
||||
['images.generations', '生图'],
|
||||
['images.edits', '图像编辑'],
|
||||
] as const;
|
||||
|
||||
export function ApiDocsPage(props: {
|
||||
activeDocSection: ApiDocSection;
|
||||
apiKeySecret: string;
|
||||
apiKeySecretsById: Record<string, string>;
|
||||
apiKeys: GatewayApiKey[];
|
||||
canRun: boolean;
|
||||
coreMessage: string;
|
||||
coreState: LoadState;
|
||||
selectedApiKeyId: string;
|
||||
taskForm: TaskForm;
|
||||
taskResult: GatewayTask | null;
|
||||
onApiKeyChange: (apiKeyId: string) => void;
|
||||
onCreateApiKey: () => void;
|
||||
onLogin: () => void;
|
||||
onDocSectionChange: (value: ApiDocSection) => void;
|
||||
onSubmitTask: (event: FormEvent<HTMLFormElement>) => void;
|
||||
@@ -35,8 +54,16 @@ export function ApiDocsPage(props: {
|
||||
}) {
|
||||
const current = docs.find((item) => item.key === props.activeDocSection) ?? docs[0];
|
||||
const isFileDoc = current.key === 'files';
|
||||
const apiKeyNotice = apiKeyNoticeText(props.apiKeys, props.apiKeySecretsById);
|
||||
const activeApiKeyId = resolveSelectedApiKeyId(props.apiKeys, props.apiKeySecretsById, props.selectedApiKeyId);
|
||||
const bodyExample = useMemo(() => requestBodyExample(props.taskForm), [props.taskForm]);
|
||||
|
||||
useEffect(() => {
|
||||
if (current.kind && props.taskForm.kind !== current.kind) {
|
||||
props.onTaskFormChange(defaultTaskForKind(current.kind, props.taskForm));
|
||||
}
|
||||
}, [current.kind, props.taskForm.kind]);
|
||||
|
||||
function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
if (!props.canRun) {
|
||||
event.preventDefault();
|
||||
@@ -46,6 +73,21 @@ export function ApiDocsPage(props: {
|
||||
props.onSubmitTask(event);
|
||||
}
|
||||
|
||||
function handleDocClick(item: ApiDocItem) {
|
||||
if (item.kind) {
|
||||
props.onTaskFormChange(defaultTaskForKind(item.kind, props.taskForm));
|
||||
}
|
||||
props.onDocSectionChange(item.key);
|
||||
}
|
||||
|
||||
function handleKindChange(kind: TaskKind) {
|
||||
props.onTaskFormChange(defaultTaskForKind(kind, props.taskForm));
|
||||
const nextSection = docSectionForKind(kind);
|
||||
if (nextSection !== props.activeDocSection) {
|
||||
props.onDocSectionChange(nextSection);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="apiDocsShell">
|
||||
<aside className="docsSidebar">
|
||||
@@ -66,7 +108,7 @@ export function ApiDocsPage(props: {
|
||||
active: item.key === props.activeDocSection,
|
||||
method: item.method,
|
||||
title: item.title,
|
||||
onClick: () => props.onDocSectionChange(item.key),
|
||||
onClick: () => handleDocClick(item),
|
||||
}))}
|
||||
/>
|
||||
))}
|
||||
@@ -79,9 +121,7 @@ export function ApiDocsPage(props: {
|
||||
<Badge variant="warning">{current.method}</Badge>
|
||||
<code>{current.path}</code>
|
||||
</div>
|
||||
<p className="docsLead">
|
||||
保持与原 integration-platform / OpenAI 兼容接口一致,支持本地 API Key 和 server-main 接入 token。
|
||||
</p>
|
||||
<p className="docsLead">{current.lead}</p>
|
||||
|
||||
<section className="paramCard">
|
||||
<header>
|
||||
@@ -90,7 +130,7 @@ export function ApiDocsPage(props: {
|
||||
</header>
|
||||
<ParamRow name="Content-Type" type="string" required value={isFileDoc ? 'multipart/form-data' : 'application/json'} />
|
||||
<ParamRow name="Accept" type="string" required value="application/json" />
|
||||
<ParamRow name="Authorization" type="string" value="Bearer {{YOUR_API_KEY}}" />
|
||||
<ParamRow name="Authorization" type="string" required value="Bearer {{YOUR_API_KEY}},支持本地 API Key;管理接口仍使用 JWT。向量需 embedding 权限,重排序需 rerank 权限。" />
|
||||
</section>
|
||||
|
||||
<section className="paramCard">
|
||||
@@ -104,12 +144,9 @@ export function ApiDocsPage(props: {
|
||||
<ParamRow name="source" type="string" value="上传来源标记" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ParamRow name="model" type="string" required value="模型 ID 或别名" />
|
||||
<ParamRow name="messages / prompt" type="array|string" required value="对话消息或图片提示词" />
|
||||
<ParamRow name="simulation" type="boolean" value="测试模式开关" />
|
||||
<ParamRow name="stream" type="boolean" value="对话进度流式返回" />
|
||||
</>
|
||||
bodyParamRows(current.key).map((row) => (
|
||||
<ParamRow key={row.name} {...row} />
|
||||
))
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
@@ -127,9 +164,27 @@ export function ApiDocsPage(props: {
|
||||
<Badge variant="warning">POST</Badge>
|
||||
<span>{current.path}</span>
|
||||
</div>
|
||||
<label className="shLabel">
|
||||
API Key
|
||||
<ApiKeySelect
|
||||
apiKeySecretsById={props.apiKeySecretsById}
|
||||
apiKeys={props.apiKeys}
|
||||
selectedApiKeyId={activeApiKeyId}
|
||||
onApiKeyChange={props.onApiKeyChange}
|
||||
/>
|
||||
</label>
|
||||
{apiKeyNotice && (
|
||||
<div className="docsKeyNotice">
|
||||
<span>{apiKeyNotice}</span>
|
||||
<Button type="button" size="sm" variant="secondary" onClick={props.onCreateApiKey}>
|
||||
<KeyRound size={14} />
|
||||
创建 Key
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<label className="shLabel">
|
||||
能力
|
||||
<Select value={props.taskForm.kind} onChange={(event) => props.onTaskFormChange(defaultTaskForKind(event.target.value as TaskForm['kind'], props.taskForm))}>
|
||||
<Select value={props.taskForm.kind} onChange={(event) => handleKindChange(event.target.value as TaskKind)}>
|
||||
{taskKindOptions.map(([value, label]) => (
|
||||
<option value={value} key={value}>{label}</option>
|
||||
))}
|
||||
@@ -198,16 +253,39 @@ function groupDocs(items: typeof docs) {
|
||||
|
||||
function defaultTaskForKind(kind: TaskForm['kind'], current: TaskForm): TaskForm {
|
||||
if (kind === 'chat.completions') return { ...current, kind, model: 'gpt-4o-mini' };
|
||||
if (kind === 'embeddings') {
|
||||
return {
|
||||
...current,
|
||||
dimensions: current.dimensions ?? 4,
|
||||
kind,
|
||||
model: 'text-embedding-v4',
|
||||
prompt: current.prompt || 'AI Gateway 提供 OpenAI 兼容接口。',
|
||||
};
|
||||
}
|
||||
if (kind === 'reranks') {
|
||||
return {
|
||||
...current,
|
||||
documents: current.documents ?? 'AI Gateway 提供 OpenAI 兼容接口。\n图片生成任务支持异步队列。\n文本向量接口返回 embedding 数组。',
|
||||
kind,
|
||||
model: 'qwen3-rerank',
|
||||
prompt: current.prompt || 'OpenAI 兼容接口',
|
||||
topN: current.topN ?? 2,
|
||||
};
|
||||
}
|
||||
if (kind === 'images.edits') return { ...current, kind, image: current.image ?? 'https://example.com/source.png', mask: current.mask ?? 'https://example.com/mask.png', model: 'gpt-image-1' };
|
||||
return { ...current, kind, model: 'gpt-image-1' };
|
||||
}
|
||||
|
||||
function requestBodyExample(task: TaskForm) {
|
||||
const body = task.kind === 'chat.completions'
|
||||
? { model: task.model, messages: [{ role: 'user', content: task.prompt }], simulation: true, stream: true }
|
||||
: task.kind === 'images.edits'
|
||||
? { model: task.model, prompt: task.prompt, image: task.image, mask: task.mask, simulation: true }
|
||||
: { model: task.model, prompt: task.prompt, quality: 'medium', simulation: true, size: '1024x1024' };
|
||||
? { model: task.model, messages: [{ role: 'user', content: task.prompt }], runMode: 'simulation', simulation: true, stream: false }
|
||||
: task.kind === 'embeddings'
|
||||
? { model: task.model, input: embeddingInputExample(task.prompt), dimensions: task.dimensions ?? 4, runMode: 'simulation', simulation: true }
|
||||
: task.kind === 'reranks'
|
||||
? { model: task.model, query: task.prompt, documents: rerankDocumentsExample(task.documents), top_n: task.topN ?? 2, runMode: 'simulation', simulation: true }
|
||||
: task.kind === 'images.edits'
|
||||
? { model: task.model, prompt: task.prompt, image: task.image, mask: task.mask, runMode: 'simulation', simulation: true }
|
||||
: { model: task.model, prompt: task.prompt, quality: 'medium', runMode: 'simulation', simulation: true, size: '1024x1024' };
|
||||
return JSON.stringify(body, null, 2);
|
||||
}
|
||||
|
||||
@@ -219,15 +297,84 @@ function parseBody(value: string, current: TaskForm): TaskForm {
|
||||
messages?: Array<{ content?: string }>;
|
||||
model?: string;
|
||||
prompt?: string;
|
||||
input?: string | string[];
|
||||
query?: string;
|
||||
documents?: string[];
|
||||
top_n?: number;
|
||||
dimensions?: number;
|
||||
};
|
||||
return {
|
||||
...current,
|
||||
dimensions: numberOrCurrent(body.dimensions, current.dimensions),
|
||||
documents: Array.isArray(body.documents) ? body.documents.join('\n') : current.documents,
|
||||
image: body.image ?? current.image,
|
||||
mask: body.mask ?? current.mask,
|
||||
model: body.model ?? current.model,
|
||||
prompt: body.prompt ?? body.messages?.[0]?.content ?? current.prompt,
|
||||
prompt: body.prompt ?? body.query ?? inputText(body.input) ?? body.messages?.[0]?.content ?? current.prompt,
|
||||
topN: numberOrCurrent(body.top_n, current.topN),
|
||||
};
|
||||
} catch {
|
||||
return current;
|
||||
}
|
||||
}
|
||||
|
||||
function bodyParamRows(section: ApiDocSection) {
|
||||
if (section === 'embeddings') {
|
||||
return [
|
||||
{ name: 'model', type: 'string', required: true, value: '模型 ID 或别名,例如 text-embedding-v4' },
|
||||
{ name: 'input', type: 'string|array', required: true, value: '需要向量化的文本或文本数组' },
|
||||
{ name: 'dimensions', type: 'number', value: '可选向量维度,需模型支持' },
|
||||
{ name: 'runMode / simulation', type: 'string|boolean', value: '在线测试时使用 simulation,不消耗真实上游额度' },
|
||||
];
|
||||
}
|
||||
if (section === 'reranks') {
|
||||
return [
|
||||
{ name: 'model', type: 'string', required: true, value: '模型 ID 或别名,例如 qwen3-rerank' },
|
||||
{ name: 'query', type: 'string', required: true, value: '用于相关性排序的查询文本' },
|
||||
{ name: 'documents', type: 'array', required: true, value: '候选文档文本数组,至少 1 条' },
|
||||
{ name: 'top_n', type: 'number', value: '返回前 N 条结果' },
|
||||
{ name: 'runMode / simulation', type: 'string|boolean', value: '在线测试时使用 simulation,不消耗真实上游额度' },
|
||||
];
|
||||
}
|
||||
return [
|
||||
{ name: 'model', type: 'string', required: true, value: '模型 ID 或别名' },
|
||||
{ name: 'messages / prompt', type: 'array|string', required: true, value: '对话消息或图片提示词' },
|
||||
{ name: 'simulation', type: 'boolean', value: '测试模式开关' },
|
||||
{ name: 'stream', type: 'boolean', value: '对话进度流式返回' },
|
||||
];
|
||||
}
|
||||
|
||||
function docSectionForKind(kind: TaskKind): ApiDocSection {
|
||||
if (kind === 'embeddings') return 'embeddings';
|
||||
if (kind === 'reranks') return 'reranks';
|
||||
if (kind === 'images.generations') return 'imageGeneration';
|
||||
if (kind === 'images.edits') return 'imageEdit';
|
||||
return 'chat';
|
||||
}
|
||||
|
||||
function embeddingInputExample(prompt: string) {
|
||||
const lines = splitLines(prompt);
|
||||
return lines.length > 1 ? lines : (lines[0] ?? prompt);
|
||||
}
|
||||
|
||||
function rerankDocumentsExample(value?: string) {
|
||||
return splitLines(value ?? '').length
|
||||
? splitLines(value ?? '')
|
||||
: ['AI Gateway 提供 OpenAI 兼容接口。', '图片生成任务支持异步队列。'];
|
||||
}
|
||||
|
||||
function splitLines(value: string) {
|
||||
return value
|
||||
.split(/\n+/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function inputText(value: string | string[] | undefined) {
|
||||
if (Array.isArray(value)) return value.join('\n');
|
||||
return value;
|
||||
}
|
||||
|
||||
function numberOrCurrent(value: unknown, current?: number) {
|
||||
return typeof value === 'number' && Number.isFinite(value) ? value : current;
|
||||
}
|
||||
|
||||
@@ -252,6 +252,7 @@ function modelTypeMatchesCapability(value: string, capability: string) {
|
||||
function canonicalCapabilityValue(value: string) {
|
||||
const type = value.trim().toLowerCase().replaceAll('-', '_');
|
||||
if (type === 'embedding') return 'text_embedding';
|
||||
if (type === 'rerank' || type === 'reranks') return 'text_rerank';
|
||||
if (type === 'model') return 'model_3d';
|
||||
return type;
|
||||
}
|
||||
@@ -265,6 +266,7 @@ function capabilityValueForTag(tag: string) {
|
||||
推理: 'reasoning',
|
||||
结构化输出: 'structured_output',
|
||||
数字人: 'digital_human',
|
||||
重排序: 'text_rerank',
|
||||
'3D 模型': 'model_3d',
|
||||
'文生 3D': 'text_to_model',
|
||||
'图生 3D': 'image_to_model',
|
||||
|
||||
@@ -49,6 +49,7 @@ export type PlatformModelTypeDefinition = {
|
||||
export const platformModelTypeDefinitions: PlatformModelTypeDefinition[] = [
|
||||
{ key: 'text_generate', label: '文本生成', group: '文本', area: 'text' },
|
||||
{ key: 'text_embedding', label: '文本向量', group: '文本', area: 'embedding' },
|
||||
{ key: 'text_rerank', label: '重排序', group: '文本', area: 'text' },
|
||||
{ key: 'image_generate', label: '图像生成', group: '图像', area: 'image' },
|
||||
{ key: 'image_edit', label: '图像编辑', group: '图像', area: 'image' },
|
||||
{ key: 'image_analysis', label: '图像理解', group: '理解', area: 'text' },
|
||||
@@ -469,7 +470,7 @@ function boolFrom(value: unknown) {
|
||||
}
|
||||
|
||||
function looksLikeCapabilityType(key: string) {
|
||||
return modelTypeDefinitionMap.has(key) || key.includes('_') || ['chat', 'image', 'video', 'audio', 'embedding', 'music', 'digital_human', 'model_3d'].includes(key);
|
||||
return modelTypeDefinitionMap.has(key) || key.includes('_') || ['chat', 'image', 'video', 'audio', 'embedding', 'rerank', 'music', 'digital_human', 'model_3d'].includes(key);
|
||||
}
|
||||
|
||||
function inferArea(type: string): CapabilityConfigArea {
|
||||
@@ -484,7 +485,7 @@ function inferArea(type: string): CapabilityConfigArea {
|
||||
}
|
||||
|
||||
function isTextLike(type: string) {
|
||||
return ['text_generate', 'image_analysis', 'video_understanding', 'audio_understanding', 'omni', 'tools_call'].includes(type);
|
||||
return ['text_generate', 'text_rerank', 'image_analysis', 'video_understanding', 'audio_understanding', 'omni', 'tools_call'].includes(type);
|
||||
}
|
||||
|
||||
function isImageLike(type: string) {
|
||||
|
||||
@@ -44,6 +44,8 @@ const adminPaths: Record<AdminSection, string> = {
|
||||
|
||||
const docsPaths: Record<ApiDocSection, string> = {
|
||||
chat: '/docs/chat',
|
||||
embeddings: '/docs/embeddings',
|
||||
reranks: '/docs/reranks',
|
||||
imageGeneration: '/docs/images/generations',
|
||||
imageEdit: '/docs/images/edits',
|
||||
pricing: '/docs/pricing',
|
||||
@@ -134,6 +136,8 @@ function parseAdminSection(path: string): AdminSection {
|
||||
function parseDocSection(path: string): ApiDocSection {
|
||||
if (path === '/docs') return 'chat';
|
||||
if (path === '/docs/api/chat') return 'chat';
|
||||
if (path === '/docs/api/embeddings') return 'embeddings';
|
||||
if (path === '/docs/api/reranks') return 'reranks';
|
||||
if (path === '/docs/api/media') return 'imageGeneration';
|
||||
if (path === '/docs/playground') return 'chat';
|
||||
return docsSections[path] ?? 'chat';
|
||||
|
||||
@@ -98,6 +98,22 @@
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.docsKeyNotice {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
border: 1px solid #fde68a;
|
||||
border-radius: 8px;
|
||||
background: #fffbeb;
|
||||
color: #92400e;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.docsKeyNotice .shButton {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.docsLead {
|
||||
margin: 18px 0 24px;
|
||||
color: var(--text-soft);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
export type LoadState = 'idle' | 'loading' | 'ready' | 'error';
|
||||
export type AuthMode = 'login' | 'register' | 'external';
|
||||
export type TaskKind = 'chat.completions' | 'images.generations' | 'images.edits';
|
||||
export type TaskKind = 'chat.completions' | 'embeddings' | 'reranks' | 'images.generations' | 'images.edits';
|
||||
export type PageKey = 'home' | 'playground' | 'models' | 'workspace' | 'admin' | 'docs';
|
||||
export type PlaygroundMode = 'chat' | 'image' | 'video';
|
||||
export type WorkspaceSection = 'overview' | 'billing' | 'apiKeys' | 'tasks' | 'transactions';
|
||||
export type ApiDocSection = 'chat' | 'imageGeneration' | 'imageEdit' | 'pricing' | 'files';
|
||||
export type ApiDocSection = 'chat' | 'embeddings' | 'reranks' | 'imageGeneration' | 'imageEdit' | 'pricing' | 'files';
|
||||
export type AdminSection =
|
||||
| 'overview'
|
||||
| 'globalModels'
|
||||
@@ -37,8 +37,11 @@ export interface TaskForm {
|
||||
kind: TaskKind;
|
||||
model: string;
|
||||
prompt: string;
|
||||
dimensions?: number;
|
||||
documents?: string;
|
||||
image?: string;
|
||||
mask?: string;
|
||||
topN?: number;
|
||||
}
|
||||
|
||||
export interface ApiKeyForm {
|
||||
|
||||
Reference in New Issue
Block a user