feat(web): 完善 API 文档与异步任务说明

This commit is contained in:
wangbo 2026-07-17 15:42:46 +08:00
parent 2d6c16fec0
commit cc3bbeccc2
9 changed files with 1182 additions and 101 deletions

View File

@ -1,8 +1,10 @@
import { afterEach, describe, expect, it, vi } from 'vitest'; import { afterEach, describe, expect, it, vi } from 'vitest';
import { import {
createResponse,
deleteOIDCBrowserSession, deleteOIDCBrowserSession,
GatewayApiError, GatewayApiError,
gatewayErrorMessage, gatewayErrorMessage,
getAPITask,
getCurrentUser, getCurrentUser,
getOpsManagementSkillMetadata, getOpsManagementSkillMetadata,
OIDC_BROWSER_SESSION_CREDENTIAL, OIDC_BROWSER_SESSION_CREDENTIAL,
@ -96,3 +98,39 @@ describe('Public Agent resources', () => {
expect(new Headers(init.headers).has('Authorization')).toBe(false); expect(new Headers(init.headers).has('Authorization')).toBe(false);
}); });
}); });
describe('API documentation runner transports', () => {
afterEach(() => {
vi.unstubAllGlobals();
});
it('runs Responses against the public OpenAI-compatible endpoint', async () => {
const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify({ id: 'resp-test', object: 'response' }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}));
vi.stubGlobal('fetch', fetchMock);
await createResponse('sk-test', { model: 'gpt-test', input: 'hello', store: true, stream: false });
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
expect(url).toContain('/v1/responses');
expect(init.method).toBe('POST');
expect(new Headers(init.headers).get('Authorization')).toBe('Bearer sk-test');
expect(JSON.parse(String(init.body))).toMatchObject({ model: 'gpt-test', input: 'hello', store: true, stream: false });
});
it('retrieves a task from the documented API path', async () => {
const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify({ id: 'task-123', status: 'running' }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}));
vi.stubGlobal('fetch', fetchMock);
await getAPITask('sk-test', 'task-123');
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
expect(url).toContain('/api/v1/tasks/task-123');
expect(init.method).toBe('GET');
});
});

View File

@ -605,6 +605,26 @@ export async function createCompatibleChatCompletion(
}); });
} }
export async function createResponse(
token: string,
input: {
model: string;
input: unknown;
instructions?: string;
previous_response_id?: string;
runMode?: string;
simulation?: boolean;
store?: boolean;
stream?: boolean;
},
): Promise<Record<string, unknown>> {
return request<Record<string, unknown>>('/v1/responses', {
body: input,
method: 'POST',
token,
});
}
export async function createEmbedding( export async function createEmbedding(
token: string, token: string,
input: { model: string; input: string | string[]; dimensions?: number; runMode?: string; simulation?: boolean }, input: { model: string; input: string | string[]; dimensions?: number; runMode?: string; simulation?: boolean },
@ -818,6 +838,8 @@ export interface VideoGenerationParams {
mode?: 'std' | 'pro'; mode?: 'std' | 'pro';
negative_prompt?: string; negative_prompt?: string;
cfg_scale?: number; cfg_scale?: number;
runMode?: string;
simulation?: boolean;
} }
export async function createVideoGenerationTask( export async function createVideoGenerationTask(
@ -881,6 +903,10 @@ export async function getTask(token: string, taskId: string): Promise<GatewayTas
return request<GatewayTask>(`/api/workspace/tasks/${taskId}`, { token }); return request<GatewayTask>(`/api/workspace/tasks/${taskId}`, { token });
} }
export async function getAPITask(token: string, taskId: string): Promise<GatewayTask> {
return request<GatewayTask>(`/api/v1/tasks/${taskId}`, { token });
}
export async function listTaskParamPreprocessing( export async function listTaskParamPreprocessing(
token: string, token: string,
taskId: string, taskId: string,

View File

@ -1,5 +1,14 @@
import type { GatewayTask } from '@easyai-ai-gateway/contracts'; import type { GatewayTask } from '@easyai-ai-gateway/contracts';
import { createCompatibleChatCompletion, createEmbedding, createImageEditTask, createImageGenerationTask, createRerank } from '../api'; import {
createCompatibleChatCompletion,
createEmbedding,
createImageEditTask,
createImageGenerationTask,
createRerank,
createResponse,
createVideoGenerationTask,
getAPITask,
} from '../api';
import type { TaskForm } from '../types'; import type { TaskForm } from '../types';
export interface RunTaskResponse { export interface RunTaskResponse {
@ -19,6 +28,19 @@ export async function runTask(token: string, task: TaskForm): Promise<RunTaskRes
}); });
return { localOnly: true, task: compatibleTask(task, result) }; return { localOnly: true, task: compatibleTask(task, result) };
} }
if (task.kind === 'responses') {
const result = await createResponse(token, {
model: task.model,
input: task.prompt,
instructions: task.instructions,
previous_response_id: task.previousResponseId,
runMode: 'simulation',
simulation: true,
store: true,
stream: false,
});
return { localOnly: true, task: compatibleTask(task, result) };
}
if (task.kind === 'embeddings') { if (task.kind === 'embeddings') {
const result = await createEmbedding(token, { const result = await createEmbedding(token, {
model: task.model, model: task.model,
@ -60,6 +82,24 @@ export async function runTask(token: string, task: TaskForm): Promise<RunTaskRes
simulation: true, simulation: true,
}); });
} }
if (task.kind === 'videos.generations') {
return createVideoGenerationTask(token, {
model: task.model,
content: [{ type: 'text', text: task.prompt }],
aspect_ratio: task.aspectRatio ?? '16:9',
resolution: task.resolution ?? '720p',
duration: task.duration ?? 5,
audio: task.outputAudio ?? true,
runMode: 'simulation',
simulation: true,
});
}
if (task.kind === 'tasks.retrieve') {
const taskId = task.taskId?.trim();
if (!taskId) throw new Error('请输入要取回的 Task ID');
const result = await getAPITask(token, taskId);
return { localOnly: true, task: compatibleTask(task, result as unknown as Record<string, unknown>) };
}
throw new Error(`Unsupported task kind: ${task.kind}`); throw new Error(`Unsupported task kind: ${task.kind}`);
} }
@ -83,6 +123,18 @@ function compatibleTask(task: TaskForm, result: Record<string, unknown>): Gatewa
} }
function requestSnapshot(task: TaskForm): Record<string, unknown> { function requestSnapshot(task: TaskForm): Record<string, unknown> {
if (task.kind === 'responses') {
return {
model: task.model,
input: task.prompt,
instructions: task.instructions,
previous_response_id: task.previousResponseId,
runMode: 'simulation',
simulation: true,
store: true,
stream: false,
};
}
if (task.kind === 'embeddings') { if (task.kind === 'embeddings') {
return { return {
model: task.model, model: task.model,
@ -102,6 +154,19 @@ function requestSnapshot(task: TaskForm): Record<string, unknown> {
simulation: true, simulation: true,
}; };
} }
if (task.kind === 'videos.generations') {
return {
model: task.model,
content: [{ type: 'text', text: task.prompt }],
aspect_ratio: task.aspectRatio ?? '16:9',
resolution: task.resolution ?? '720p',
duration: task.duration ?? 5,
audio: task.outputAudio ?? true,
runMode: 'simulation',
simulation: true,
};
}
if (task.kind === 'tasks.retrieve') return { taskId: task.taskId };
return { return {
model: task.model, model: task.model,
messages: [{ role: 'user', content: task.prompt }], messages: [{ role: 'user', content: task.prompt }],
@ -133,5 +198,7 @@ function modelTypeForKind(kind: TaskForm['kind']) {
if (kind === 'reranks') return 'text_rerank'; if (kind === 'reranks') return 'text_rerank';
if (kind === 'images.generations') return 'image_generate'; if (kind === 'images.generations') return 'image_generate';
if (kind === 'images.edits') return 'image_edit'; if (kind === 'images.edits') return 'image_edit';
if (kind === 'videos.generations') return 'video_generate';
if (kind === 'tasks.retrieve') return 'task';
return 'text_generate'; return 'text_generate';
} }

View File

@ -0,0 +1,107 @@
import { renderToStaticMarkup } from 'react-dom/server';
import { describe, expect, it, vi } from 'vitest';
import type { ApiDocSection, TaskForm } from '../types';
import { ApiDocsPage } from './ApiDocsPage';
describe('ApiDocsPage extended task documentation', () => {
it.each([
['guideBaseUrl', '前往创建 API Key'],
['guideWebhook', 'TASK_PROGRESS_CALLBACK_ENABLED'],
['guideErrors', '400 invalid_parameter'],
['guideTesting', 'simulated=true'],
] as const)('renders the clickable guide page %s', (section, expectedContent) => {
const html = renderDocs(section, { kind: 'chat.completions', model: 'gpt-4o-mini', prompt: '你好' });
expect(html).toContain('data-active="true"');
expect(html).toContain(expectedContent);
expect(html).toContain('指南导航');
expect(html).not.toContain('/v1/chat/completions');
});
it('documents the OpenAI-compatible Responses request and continuity fields', () => {
const html = renderDocs('responses', { kind: 'responses', model: 'gpt-4o-mini', prompt: '你好' });
expect(html).toContain('/v1/responses');
expect(html).toContain('previous_response_id');
expect(html).toContain('X-Async');
expect(html).toContain('连续对话与状态归属');
});
it('documents video generation parameters and async retrieval guidance', () => {
const html = renderDocs('videoGeneration', { kind: 'videos.generations', model: '豆包Seedance-2.0', prompt: '雪山日出' });
expect(html).toContain('/api/v1/videos/generations');
expect(html).toContain('aspect_ratio');
expect(html).toContain('reference_image');
expect(html).toContain('任务取回接口');
});
it('documents async mode as a body-independent capability', () => {
const html = renderDocs('asyncMode', { kind: 'chat.completions', model: 'gpt-4o-mini', prompt: '你好' });
expect(html).toContain('X-Async');
expect(html).toContain('原请求 Header');
expect(html).toContain('同步与异步的区别');
expect(html).toContain('异步受理响应');
expect(html).not.toContain('/api/v1/videos/generations');
expect(html).not.toContain('Body 参数');
expect(html).not.toContain('视频模型 ID');
});
it.each(['chat', 'responses', 'embeddings', 'reranks', 'imageGeneration', 'imageEdit', 'videoGeneration'] as const)(
'shows the shared X-Async switch on the %s task API',
(section) => {
const html = renderDocs(section, { kind: 'chat.completions', model: 'gpt-4o-mini', prompt: '你好' });
expect(html).toContain('X-Async');
},
);
it('renders nested objects and object arrays recursively', () => {
const html = renderDocs('videoGeneration', { kind: 'videos.generations', model: '豆包Seedance-2.0', prompt: '雪山日出' });
expect(html).toContain('array&lt;object&gt;');
expect(html).toContain('data-depth="1"');
expect(html).toContain('data-depth="4"');
expect(html).toContain('inline_element');
expect(html).toContain('refer_images');
expect(html).toContain('slot_key');
});
it('renders task retrieval as GET with a path parameter instead of a request body', () => {
const html = renderDocs('taskRetrieve', { kind: 'tasks.retrieve', model: 'task', prompt: '', taskId: 'task-123' });
expect(html).toContain('GET');
expect(html).toContain('/api/v1/tasks/task-123');
expect(html).toContain('Path 参数');
expect(html).toContain('响应 Body');
expect(html).toContain('result');
expect(html).toContain('billingSummary');
expect(html).toContain('attempts');
expect(html).toContain('成功响应示例');
expect(html).toContain('Task ID');
expect(html).not.toContain('请求 Body');
});
});
function renderDocs(activeDocSection: ApiDocSection, taskForm: TaskForm) {
return renderToStaticMarkup(
<ApiDocsPage
activeDocSection={activeDocSection}
apiKeySecretsById={{}}
apiKeys={[]}
canRun
coreMessage=""
coreState="idle"
selectedApiKeyId=""
taskForm={taskForm}
taskResult={null}
onApiKeyChange={vi.fn()}
onCreateApiKey={vi.fn()}
onDocSectionChange={vi.fn()}
onLogin={vi.fn()}
onSubmitTask={vi.fn()}
onTaskFormChange={vi.fn()}
/>,
);
}

View File

@ -1,7 +1,7 @@
import { useEffect, useMemo, useState, type FormEvent } from 'react'; import { Fragment, useEffect, useMemo, useState, type CSSProperties, type FormEvent, type ReactNode } from 'react';
import type { GatewayApiKey, GatewaySkillBundleMetadata, GatewayTask } from '@easyai-ai-gateway/contracts'; import type { GatewayApiKey, GatewaySkillBundleMetadata, GatewayTask } from '@easyai-ai-gateway/contracts';
import { BookOpen, Download, ExternalLink, FileJson, KeyRound, Play, Search, Send, Wrench } from 'lucide-react'; import { BookOpen, Download, ExternalLink, FileJson, KeyRound, Play, Search, Send, Wrench } from 'lucide-react';
import { Badge, Button, Select, Textarea } from '../components/ui'; import { Badge, Button, Input, Select, Textarea } from '../components/ui';
import { getOpsManagementSkillMetadata, resolveApiAssetUrl } from '../api'; import { getOpsManagementSkillMetadata, resolveApiAssetUrl } from '../api';
import type { ApiDocSection, LoadState, TaskForm, TaskKind } from '../types'; import type { ApiDocSection, LoadState, TaskForm, TaskKind } from '../types';
import { ApiKeySelect, apiKeyNoticeText, resolveSelectedApiKeyId } from './playground-shared'; import { ApiKeySelect, apiKeyNoticeText, resolveSelectedApiKeyId } from './playground-shared';
@ -11,29 +11,58 @@ interface ApiDocItem {
key: ApiDocSection; key: ApiDocSection;
kind?: TaskKind; kind?: TaskKind;
lead: string; lead: string;
method: string; method?: 'GET' | 'POST';
path: string; path?: string;
title: string; title: string;
} }
const docs: ApiDocItem[] = [ interface ApiDocParam {
children?: ApiDocParam[];
name: string;
required?: boolean;
type: string;
value: string;
}
type ApiGuideSection = Extract<ApiDocSection, 'guideBaseUrl' | 'guideWebhook' | 'guideErrors' | 'guideTesting'>;
interface ApiGuideItem {
group: '指南';
key: ApiGuideSection;
lead: string;
title: string;
}
export const apiDocs: ApiDocItem[] = [
{ key: 'chat', group: '文本', kind: 'chat.completions', method: 'POST', path: '/v1/chat/completions', title: 'Chat Completions', lead: 'OpenAI 兼容的对话接口,支持本地 API Key 授权、simulation 测试和非流式/流式响应。' }, { key: 'chat', group: '文本', kind: 'chat.completions', method: 'POST', path: '/v1/chat/completions', title: 'Chat Completions', lead: 'OpenAI 兼容的对话接口,支持本地 API Key 授权、simulation 测试和非流式/流式响应。' },
{ key: 'responses', group: '文本', kind: 'responses', method: 'POST', path: '/v1/responses', title: 'Responses', lead: 'OpenAI 兼容的 Responses 接口,原生支持 input、previous_response_id、工具调用和流式输出不支持原生 Responses 的模型会由网关转换到 Chat Completions。' },
{ key: 'embeddings', group: '文本', kind: 'embeddings', method: 'POST', path: '/v1/embeddings', title: '文本向量 Embeddings', lead: 'OpenAI 兼容的文本向量接口,可直接用 input 数组或字符串生成 embeddingAPI Key 需要 embedding 权限。' }, { key: 'embeddings', group: '文本', kind: 'embeddings', method: 'POST', path: '/v1/embeddings', title: '文本向量 Embeddings', lead: 'OpenAI 兼容的文本向量接口,可直接用 input 数组或字符串生成 embeddingAPI Key 需要 embedding 权限。' },
{ key: 'reranks', group: '文本', kind: 'reranks', method: 'POST', path: '/v1/reranks', title: '文本重排序 Reranks', lead: 'OpenAI 风格的重排序接口,传入 query 和 documents 后返回 relevance_scoreAPI Key 需要 rerank 权限。' }, { key: 'reranks', group: '文本', kind: 'reranks', method: 'POST', path: '/v1/reranks', title: '文本重排序 Reranks', lead: 'OpenAI 风格的重排序接口,传入 query 和 documents 后返回 relevance_scoreAPI Key 需要 rerank 权限。' },
{ key: 'imageGeneration', group: '图片', kind: 'images.generations', method: 'POST', path: '/v1/images/generations', title: '创建图片', lead: 'OpenAI 兼容的图片生成接口,支持 prompt、size、quality 和 simulation 测试。' }, { 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: 'imageEdit', group: '图片', kind: 'images.edits', method: 'POST', path: '/v1/images/edits', title: '编辑图片', lead: 'OpenAI 兼容的图片编辑接口,支持 image、mask、prompt 和 simulation 测试。' },
{ key: 'videoGeneration', group: '视频', kind: 'videos.generations', method: 'POST', path: '/api/v1/videos/generations', title: '生成视频', lead: '视频生成任务接口,支持文生视频、首尾帧、图片/视频/音频参考,以及时长、分辨率、画幅和声音等模型能力参数。' },
{ key: 'asyncMode', group: '异步任务', title: '异步模式', lead: '所有 AI 任务创建接口使用同一种异步开启方式:保留原接口和原请求 Body只需增加 X-Async: true。' },
{ key: 'taskRetrieve', group: '异步任务', kind: 'tasks.retrieve', method: 'GET', path: '/api/v1/tasks/{taskID}', title: '取回任务', lead: '使用异步提交返回的 taskId 查询任务状态、结果、错误、用量、计费和执行尝试queued、running、submitting 为进行中状态。' },
{ key: 'pricing', group: '计费', method: 'POST', path: '/api/v1/pricing/estimate', title: '价格预估', lead: '按请求体估算输入输出 token、模型倍率和折扣后的预估费用。' }, { 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。' }, { key: 'files', group: '文件', method: 'POST', path: '/v1/files/upload', title: '上传文件', lead: '上传在线测试所需的图片、音频或视频资源,后续请求可复用返回的文件 URL。' },
]; ];
const guideItems = ['获取 Base URL 和 API Key', '通知设置-WebHook 参数介绍', '错误码', '测试模式']; const guideItems: ApiGuideItem[] = [
{ key: 'guideBaseUrl', group: '指南', title: '获取 Base URL 和 API Key', lead: '确认当前部署环境的网关地址,创建 API Key并使用 Bearer 鉴权调用公开接口。' },
{ key: 'guideWebhook', group: '指南', title: '通知设置-WebHook 参数介绍', lead: '了解任务进度 WebHook 的启用方式、回调结构、可靠投递机制和公开 API 的状态取回方式。' },
{ key: 'guideErrors', group: '指南', title: '错误码', lead: '根据 HTTP 状态、error.code、requestId 和异步任务错误字段快速定位请求失败原因。' },
{ key: 'guideTesting', group: '指南', title: '测试模式', lead: '使用 simulation 完整验证鉴权、路由、队列、限流、进度和结果归一,而不向真实供应商提交任务。' },
];
const taskKindOptions = [ const taskKindOptions = [
['chat.completions', 'Chat'], ['chat.completions', 'Chat'],
['responses', 'Responses'],
['embeddings', '文本向量'], ['embeddings', '文本向量'],
['reranks', '重排序'], ['reranks', '重排序'],
['images.generations', '生图'], ['images.generations', '生图'],
['images.edits', '图像编辑'], ['images.edits', '图像编辑'],
['videos.generations', '视频生成'],
['tasks.retrieve', '取回任务'],
] as const; ] as const;
const defaultOpsSkillMetadata: GatewaySkillBundleMetadata = { const defaultOpsSkillMetadata: GatewaySkillBundleMetadata = {
@ -64,18 +93,31 @@ export function ApiDocsPage(props: {
onSubmitTask: (event: FormEvent<HTMLFormElement>) => void; onSubmitTask: (event: FormEvent<HTMLFormElement>) => void;
onTaskFormChange: (value: TaskForm) => void; onTaskFormChange: (value: TaskForm) => void;
}) { }) {
const current = docs.find((item) => item.key === props.activeDocSection) ?? docs[0]; const activeGuide = guideItems.find((item) => item.key === props.activeDocSection);
const currentApiDoc = apiDocs.find((item) => item.key === props.activeDocSection) ?? (activeGuide ? undefined : apiDocs[0]);
const current = activeGuide ?? currentApiDoc ?? apiDocs[0];
const [opsSkillMetadata, setOpsSkillMetadata] = useState<GatewaySkillBundleMetadata>(defaultOpsSkillMetadata); const [opsSkillMetadata, setOpsSkillMetadata] = useState<GatewaySkillBundleMetadata>(defaultOpsSkillMetadata);
const isFileDoc = current.key === 'files'; const isFileDoc = currentApiDoc?.key === 'files';
const isTaskRetrieveDoc = currentApiDoc?.key === 'taskRetrieve';
const isAsyncModeDoc = currentApiDoc?.key === 'asyncMode';
const runnerAvailable = Boolean(currentApiDoc?.kind && currentApiDoc.method && currentApiDoc.path);
const apiKeyNotice = apiKeyNoticeText(props.apiKeys, props.apiKeySecretsById); const apiKeyNotice = apiKeyNoticeText(props.apiKeys, props.apiKeySecretsById);
const activeApiKeyId = resolveSelectedApiKeyId(props.apiKeys, props.apiKeySecretsById, props.selectedApiKeyId); const activeApiKeyId = resolveSelectedApiKeyId(props.apiKeys, props.apiKeySecretsById, props.selectedApiKeyId);
const bodyExample = useMemo(() => requestBodyExample(props.taskForm), [props.taskForm]); const bodyExample = useMemo(
() => requestBodyExample(props.taskForm, currentApiDoc?.key ?? 'chat'),
[currentApiDoc?.key, props.taskForm],
);
const runnerPath = currentApiDoc?.path
? isTaskRetrieveDoc
? currentApiDoc.path.replace('{taskID}', props.taskForm.taskId?.trim() || '{taskID}')
: currentApiDoc.path
: '';
useEffect(() => { useEffect(() => {
if (current.kind && props.taskForm.kind !== current.kind) { if (currentApiDoc?.kind && props.taskForm.kind !== currentApiDoc.kind) {
props.onTaskFormChange(defaultTaskForKind(current.kind, props.taskForm)); props.onTaskFormChange(defaultTaskForDoc(currentApiDoc.kind, props.taskForm, props.taskResult));
} }
}, [current.kind, props.taskForm.kind]); }, [currentApiDoc?.kind, props.taskForm.kind, props.taskResult?.id]);
useEffect(() => { useEffect(() => {
let active = true; let active = true;
@ -92,6 +134,10 @@ export function ApiDocsPage(props: {
}, []); }, []);
function handleSubmit(event: FormEvent<HTMLFormElement>) { function handleSubmit(event: FormEvent<HTMLFormElement>) {
if (!runnerAvailable) {
event.preventDefault();
return;
}
if (!props.canRun) { if (!props.canRun) {
event.preventDefault(); event.preventDefault();
props.onLogin(); props.onLogin();
@ -102,13 +148,17 @@ export function ApiDocsPage(props: {
function handleDocClick(item: ApiDocItem) { function handleDocClick(item: ApiDocItem) {
if (item.kind) { if (item.kind) {
props.onTaskFormChange(defaultTaskForKind(item.kind, props.taskForm)); props.onTaskFormChange(defaultTaskForDoc(item.kind, props.taskForm, props.taskResult));
} }
props.onDocSectionChange(item.key); props.onDocSectionChange(item.key);
} }
function handleGuideClick(item: ApiGuideItem) {
props.onDocSectionChange(item.key);
}
function handleKindChange(kind: TaskKind) { function handleKindChange(kind: TaskKind) {
props.onTaskFormChange(defaultTaskForKind(kind, props.taskForm)); props.onTaskFormChange(defaultTaskForDoc(kind, props.taskForm, props.taskResult));
const nextSection = docSectionForKind(kind); const nextSection = docSectionForKind(kind);
if (nextSection !== props.activeDocSection) { if (nextSection !== props.activeDocSection) {
props.onDocSectionChange(nextSection); props.onDocSectionChange(nextSection);
@ -126,8 +176,15 @@ export function ApiDocsPage(props: {
<Search size={15} /> <Search size={15} />
<input placeholder="搜索" /> <input placeholder="搜索" />
</label> </label>
<DocsGroup title="指南" items={guideItems.map((title) => ({ title }))} /> <DocsGroup
{groupDocs(docs).map((group) => ( title="指南"
items={guideItems.map((item) => ({
active: item.key === props.activeDocSection,
title: item.title,
onClick: () => handleGuideClick(item),
}))}
/>
{groupDocs(apiDocs).map((group) => (
<DocsGroup <DocsGroup
key={group.title} key={group.title}
title={group.title} title={group.title}
@ -144,54 +201,70 @@ export function ApiDocsPage(props: {
<main className="docsArticle"> <main className="docsArticle">
<p className="eyebrow">{current.group}</p> <p className="eyebrow">{current.group}</p>
<h1>{current.title}</h1> <h1>{current.title}</h1>
{currentApiDoc?.method && currentApiDoc.path && (
<div className="endpointBar"> <div className="endpointBar">
<Badge variant="warning">{current.method}</Badge> <MethodBadge method={currentApiDoc.method} />
<code>{current.path}</code> <code>{currentApiDoc.path}</code>
</div> </div>
)}
<p className="docsLead">{current.lead}</p> <p className="docsLead">{current.lead}</p>
<OpsManagementSkillCard metadata={opsSkillMetadata} /> {isAsyncModeDoc ? (
<DocDetails section="asyncMode" />
) : currentApiDoc ? (
<>
{!isTaskRetrieveDoc && <OpsManagementSkillCard metadata={opsSkillMetadata} />}
<section className="paramCard"> <section className="paramCard">
<header> <header>
<h2>Header </h2> <h2>Header </h2>
<Button type="button" variant="secondary" size="sm"></Button> <Button type="button" variant="secondary" size="sm"></Button>
</header> </header>
<ParamRow name="Content-Type" type="string" required value={isFileDoc ? 'multipart/form-data' : 'application/json'} /> {currentApiDoc.method !== 'GET' && <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="Accept" type="string" required value="application/json" />
<ParamRow name="Authorization" type="string" required value="Bearer {{YOUR_API_KEY}},支持本地 API Key管理接口仍使用 JWT。向量需 embedding 权限,重排序需 rerank 权限。" /> <ParamRow name="Authorization" type="string" required value={authorizationDescription(currentApiDoc.key)} />
{supportsAsyncMode(currentApiDoc.key) && (
<ParamRow name="X-Async" type="boolean" value="设为 true 后立即返回 202 和 taskId省略时按接口默认方式执行。" />
)}
</section> </section>
<section className="paramCard"> <section className="paramCard">
<header> <header>
<h2>Body </h2> <h2>{isTaskRetrieveDoc ? 'Path 参数' : 'Body 参数'}</h2>
<Badge variant="outline">application/json</Badge> {!isTaskRetrieveDoc && <Badge variant="outline">{isFileDoc ? 'multipart/form-data' : 'application/json'}</Badge>}
</header> </header>
{isFileDoc ? ( {isTaskRetrieveDoc ? (
<ParamRow name="taskID" type="string" required value="异步提交响应中的 taskId也可直接使用 next.detail 返回的地址。" />
) : isFileDoc ? (
<> <>
<ParamRow name="file" type="file" required value="multipart 文件字段" /> <ParamRow name="file" type="file" required value="multipart 文件字段" />
<ParamRow name="source" type="string" value="上传来源标记" /> <ParamRow name="source" type="string" value="上传来源标记" />
</> </>
) : ( ) : (
bodyParamRows(current.key).map((row) => ( <ParamRows rows={bodyParamRows(currentApiDoc.key)} />
<ParamRow key={row.name} {...row} />
))
)} )}
</section> </section>
{isTaskRetrieveDoc && <TaskResponseSchema />}
<DocDetails section={currentApiDoc.key} />
</>
) : activeGuide ? (
<GuideDetails onCreateApiKey={props.onCreateApiKey} section={activeGuide.key} />
) : null}
</main> </main>
<aside className="docsRunner"> <aside className="docsRunner">
<form onSubmit={handleSubmit}> {currentApiDoc?.method && currentApiDoc.path && !isAsyncModeDoc ? <form onSubmit={handleSubmit}>
<header> <header>
<strong>线</strong> <strong>线</strong>
<Button type="submit" size="sm" disabled={props.canRun && props.coreState === 'loading'}> <Button type="submit" size="sm" disabled={!runnerAvailable || (props.canRun && props.coreState === 'loading')}>
<Send size={14} /> <Send size={14} />
{props.canRun ? '发送' : '登录'} {!runnerAvailable ? '暂不支持' : props.canRun ? '发送' : '登录'}
</Button> </Button>
</header> </header>
<div className="runnerEndpoint"> <div className="runnerEndpoint">
<Badge variant="warning">POST</Badge> <MethodBadge method={currentApiDoc.method} />
<span>{current.path}</span> <span>{runnerPath}</span>
</div> </div>
<label className="shLabel"> <label className="shLabel">
API Key API Key
@ -211,6 +284,8 @@ export function ApiDocsPage(props: {
</Button> </Button>
</div> </div>
)} )}
{runnerAvailable ? (
<>
<label className="shLabel"> <label className="shLabel">
<Select value={props.taskForm.kind} onChange={(event) => handleKindChange(event.target.value as TaskKind)}> <Select value={props.taskForm.kind} onChange={(event) => handleKindChange(event.target.value as TaskKind)}>
@ -219,26 +294,50 @@ export function ApiDocsPage(props: {
))} ))}
</Select> </Select>
</label> </label>
{isTaskRetrieveDoc ? (
<label className="shLabel">
Task ID
<Input
placeholder="输入异步提交返回的 taskId"
value={props.taskForm.taskId ?? ''}
onChange={(event) => props.onTaskFormChange({ ...props.taskForm, taskId: event.target.value })}
/>
</label>
) : (
<label className="shLabel"> <label className="shLabel">
Body Body
<Textarea value={bodyExample} onChange={(event) => props.onTaskFormChange(parseBody(event.target.value, props.taskForm))} /> <Textarea value={bodyExample} onChange={(event) => props.onTaskFormChange(parseBody(event.target.value, props.taskForm))} />
</label> </label>
)}
<Button type="submit" disabled={props.canRun && props.coreState === 'loading'}> <Button type="submit" disabled={props.canRun && props.coreState === 'loading'}>
<Play size={15} /> <Play size={15} />
{!props.canRun ? '登录后运行' : props.coreState === 'loading' ? '运行中' : '运行测试'} {!props.canRun ? '登录后运行' : props.coreState === 'loading' ? '运行中' : '运行测试'}
</Button> </Button>
</form> </>
<section className="runnerResult"> ) : (
<div className="docsRunnerUnavailable">线使 Swagger </div>
)}
</form> : (
<section className="docsGuideAside">
<header><strong>{isAsyncModeDoc ? '能力说明' : '指南导航'}</strong></header>
<div>
<span></span>
<strong>{current.title}</strong>
<p>{isAsyncModeDoc ? '异步模式不改变原接口的请求参数,只改变任务提交后的等待和响应方式。' : '使用左侧目录切换其他指南或 API 接口,浏览器前进、后退可保持当前章节。'}</p>
</div>
</section>
)}
{currentApiDoc?.method && currentApiDoc.path && !isAsyncModeDoc && <section className="runnerResult">
<strong></strong> <strong></strong>
{props.taskResult ? ( {props.taskResult ? (
<pre>{JSON.stringify(props.taskResult.result ?? props.taskResult, null, 2)}</pre> <pre>{JSON.stringify(isTaskRetrieveDoc ? props.taskResult : props.taskResult.result ?? props.taskResult, null, 2)}</pre>
) : ( ) : (
<div className="emptyState"> <div className="emptyState">
<span></span> <span></span>
</div> </div>
)} )}
{props.coreMessage && <p>{props.coreMessage}</p>} {props.coreMessage && <p>{props.coreMessage}</p>}
</section> </section>}
</aside> </aside>
</div> </div>
); );
@ -303,7 +402,7 @@ function skillModuleLabel(module: string) {
} }
function DocsGroup(props: { function DocsGroup(props: {
items: Array<{ active?: boolean; method?: string; onClick?: () => void; title: string }>; items: Array<{ active?: boolean; method?: ApiDocItem['method']; onClick?: () => void; title: string }>;
title: string; title: string;
}) { }) {
return ( return (
@ -312,17 +411,41 @@ function DocsGroup(props: {
{props.items.map((item) => ( {props.items.map((item) => (
<button type="button" data-active={item.active} key={item.title} onClick={item.onClick}> <button type="button" data-active={item.active} key={item.title} onClick={item.onClick}>
<span>{item.title}</span> <span>{item.title}</span>
{item.method && <Badge variant="warning">{item.method}</Badge>} {item.method && <MethodBadge method={item.method} />}
</button> </button>
))} ))}
</section> </section>
); );
} }
function ParamRow(props: { name: string; required?: boolean; type: string; value: string }) { function ParamRows(props: { depth?: number; parentPath?: string; rows: ApiDocParam[] }) {
const depth = props.depth ?? 0;
const parentPath = props.parentPath ?? '';
return props.rows.map((row) => {
const path = parentPath ? `${parentPath}.${row.name}` : row.name;
return ( return (
<div className="paramRow"> <Fragment key={path}>
<ParamRow {...row} depth={depth} />
{row.children?.length ? (
<ParamRows depth={depth + 1} parentPath={path} rows={row.children} />
) : null}
</Fragment>
);
});
}
function ParamRow(props: ApiDocParam & { depth?: number }) {
const depth = props.depth ?? 0;
const nameStyle: CSSProperties = depth ? { paddingLeft: `${depth * 20}px` } : {};
const markerStyle: CSSProperties = depth ? { left: `${(depth - 1) * 20 + 3}px` } : {};
return (
<div className="paramRow" data-depth={depth} data-nested={depth > 0 ? 'true' : undefined}>
<div className="paramName" style={nameStyle}>
{depth > 0 && <span aria-hidden="true" className="paramTreeMarker" style={markerStyle} />}
<code>{props.name}</code> <code>{props.name}</code>
</div>
<span>{props.type}</span> <span>{props.type}</span>
<p>{props.value}</p> <p>{props.value}</p>
<em>{props.required ? '必需' : '可选'}</em> <em>{props.required ? '必需' : '可选'}</em>
@ -330,16 +453,284 @@ function ParamRow(props: { name: string; required?: boolean; type: string; value
); );
} }
function groupDocs(items: typeof docs) { function GuideDetails(props: { onCreateApiKey: () => void; section: ApiGuideSection }) {
const groups = new Map<string, typeof docs>(); if (props.section === 'guideBaseUrl') {
return (
<>
<GuideSection title="1. 确认 Base URL">
<p>Base URL Gateway Base URL </p>
<pre>{`export EASYAI_BASE_URL="https://your-gateway.example.com"\ncurl "$EASYAI_BASE_URL/healthz"`}</pre>
</GuideSection>
<GuideSection title="2. 创建并使用 API Key">
<p> API Key Key chatembeddingrerankimage video </p>
<div className="docsGuideActions">
<Button type="button" size="sm" onClick={props.onCreateApiKey}>
<KeyRound size={14} />
API Key
</Button>
</div>
<pre>{`Authorization: Bearer sk-...\nContent-Type: application/json\nAccept: application/json`}</pre>
</GuideSection>
</>
);
}
if (props.section === 'guideWebhook') {
return (
<>
<GuideSection title="任务进度 WebHook">
<p>Gateway WebHook </p>
<pre>{`TASK_PROGRESS_CALLBACK_ENABLED=true\nTASK_PROGRESS_CALLBACK_URL=https://service.example.com/internal/task-progress`}</pre>
<p> callback outbox outbox </p>
</GuideSection>
<GuideSection title="回调 Body">
<pre>{`{\n "taskId": "9f4d8f3d-...",\n "seq": 3,\n "eventType": "progress",\n "status": "running",\n "phase": "generating",\n "progress": 0.6,\n "message": "任务处理中",\n "payload": {},\n "simulated": false,\n "createdAt": "2026-07-17T07:00:00Z"\n}`}</pre>
<p> API WebHook使 <code>next.detail</code> 使 <code>next.events</code> </p>
</GuideSection>
</>
);
}
if (props.section === 'guideErrors') {
return (
<>
<GuideSection title="同步请求错误结构">
<pre>{`{\n "error": {\n "message": "api key scope does not allow this capability",\n "status": 403,\n "code": "forbidden"\n }\n}`}</pre>
<p> HTTP <code>error.code</code> <code>error.message</code> <code>requestId</code> 便</p>
</GuideSection>
<section className="paramCard docsDetailCard">
<header><h2></h2></header>
<div className="docsStatusGrid">
<code>400 invalid_parameter</code><p></p>
<code>401 unauthorized</code><p> API Key</p>
<code>403 forbidden</code><p>API Key 访</p>
<code>404 not_found</code><p></p>
<code>429 rate_limit</code><p> RPMTPM 退</p>
<code>5xx provider_failed</code><p>Gateway retryableattempts requestId </p>
</div>
</section>
<GuideSection title="异步任务失败">
<p> <code>failed</code> <code>errorCode</code><code>errorMessage</code><code>requestId</code> <code>attempts</code></p>
</GuideSection>
</>
);
}
return (
<>
<GuideSection title="启用请求级测试模式">
<p> Body <code>runMode: simulation</code> <code>simulation: true</code>线使</p>
<pre>{`{\n "model": "gpt-4o-mini",\n "messages": [{ "role": "user", "content": "你好" }],\n "runMode": "simulation",\n "simulation": true\n}`}</pre>
</GuideSection>
<GuideSection title="测试模式的执行边界">
<p> Gateway </p>
<p> <code>simulated=true</code> </p>
</GuideSection>
</>
);
}
function GuideSection(props: { children: ReactNode; title: string }) {
return (
<section className="paramCard docsDetailCard">
<header><h2>{props.title}</h2></header>
<div className="docsDetailContent">{props.children}</div>
</section>
);
}
function TaskResponseSchema() {
return (
<section className="paramCard">
<header>
<h2> Body</h2>
<Badge variant="outline">application/json</Badge>
</header>
<ParamRows rows={taskResponseParamRows()} />
</section>
);
}
function DocDetails(props: { section: ApiDocSection }) {
if (props.section === 'responses') {
return (
<section className="paramCard docsDetailCard">
<header><h2></h2></header>
<div className="docsDetailContent">
<p> <code>previous_response_id</code> Responses <code>response.id</code></p>
<p> <code>previous_response_id</code> input </p>
<pre>{`{
"model": "gpt-4o-mini",
"input": "继续上一轮回答",
"previous_response_id": "resp_..."
}`}</pre>
</div>
</section>
);
}
if (props.section === 'videoGeneration') {
return (
<section className="paramCard docsDetailCard">
<header><h2></h2></header>
<div className="docsDetailContent">
<p><code>image_url</code> 使 <code>first_frame</code><code>last_frame</code><code>reference_image</code><code>video_url</code> <code>audio_url</code> </p>
<p> <code>X-Async: true</code>使 <code>next.detail</code> </p>
</div>
</section>
);
}
if (props.section === 'asyncMode') {
return (
<>
<section className="paramCard">
<header><h2></h2></header>
<ParamRow name="X-Async" type="boolean" required value="在原请求 Header 中设置为 true请求 URL、鉴权方式和 Body 均保持原接口定义不变。" />
</section>
<section className="paramCard docsDetailCard">
<header><h2></h2></header>
<div className="docsStatusGrid">
<code> Body</code><p>使</p>
<code> X-Async</code><p> JSON SSE </p>
<code>X-Async: true</code><p> HTTP 202 taskId </p>
<code></code><p> AI 使 Body </p>
</div>
</section>
<section className="paramCard docsDetailCard">
<header><h2></h2></header>
<div className="docsDetailContent">
<p> <code>taskId</code><code>next.detail</code> <code>next.events</code> </p>
<pre>{`HTTP/1.1 202 Accepted
{
"taskId": "9f4d8f3d-...",
"task": {
"id": "9f4d8f3d-...",
"status": "queued",
"asyncMode": true
},
"next": {
"detail": "/api/v1/tasks/9f4d8f3d-...",
"events": "/api/v1/tasks/9f4d8f3d-.../events"
}
}`}</pre>
</div>
</section>
</>
);
}
if (props.section === 'taskRetrieve') {
return (
<>
<section className="paramCard docsDetailCard">
<header><h2></h2></header>
<div className="docsStatusGrid">
<code>queued / submitting / running</code><p> taskId <code>result</code> </p>
<code>succeeded</code><p> <code>result</code> <code>usage</code><code>billings</code></p>
<code>failed</code><p> <code>errorCode</code><code>errorMessage</code><code>requestId</code> <code>attempts</code></p>
<code>cancelled</code><p> <code>submitted</code> </p>
</div>
</section>
<section className="paramCard docsDetailCard">
<header><h2></h2></header>
<div className="docsDetailContent">
<p><code>result</code> </p>
<pre>{`{
"id": "9f4d8f3d-...",
"kind": "images.generations",
"model": "image-model",
"asyncMode": true,
"status": "succeeded",
"result": {
"data": [{ "url": "https://example.com/result.png" }]
},
"usage": { "totalTokens": 0 },
"billings": [{
"resourceType": "image",
"unit": "image",
"quantity": 1,
"amount": 0.1,
"currency": "resource",
"simulated": false
}],
"billingSummary": {
"lineCount": 1,
"totalAmount": 0.1,
"currency": "resource",
"simulated": false
},
"finalChargeAmount": 0.1,
"attempts": [{
"attemptNo": 1,
"provider": "provider-name",
"status": "succeeded",
"retryable": false,
"responseDurationMs": 1280
}],
"createdAt": "2026-07-17T07:00:00Z",
"updatedAt": "2026-07-17T07:00:02Z",
"finishedAt": "2026-07-17T07:00:02Z"
}`}</pre>
</div>
</section>
</>
);
}
return null;
}
function authorizationDescription(section: ApiDocSection) {
const scope = section === 'embeddings'
? 'embedding'
: section === 'reranks'
? 'rerank'
: section === 'videoGeneration' || section === 'asyncMode'
? 'video'
: section === 'chat' || section === 'responses'
? 'chat'
: '';
return `Bearer {{YOUR_API_KEY}},支持本地 API Key管理接口仍使用 JWT。${scope ? `当前接口需要 ${scope} 权限。` : ''}`;
}
function supportsAsyncMode(section: ApiDocSection) {
return section === 'chat'
|| section === 'responses'
|| section === 'embeddings'
|| section === 'reranks'
|| section === 'imageGeneration'
|| section === 'imageEdit'
|| section === 'videoGeneration';
}
function MethodBadge(props: { method: NonNullable<ApiDocItem['method']> }) {
return <Badge variant={props.method === 'GET' ? 'success' : 'warning'}>{props.method}</Badge>;
}
function groupDocs(items: typeof apiDocs) {
const groups = new Map<string, typeof apiDocs>();
for (const item of items) { for (const item of items) {
groups.set(item.group, [...(groups.get(item.group) ?? []), item]); groups.set(item.group, [...(groups.get(item.group) ?? []), item]);
} }
return Array.from(groups, ([title, groupItems]) => ({ title, items: groupItems })); return Array.from(groups, ([title, groupItems]) => ({ title, items: groupItems }));
} }
function defaultTaskForDoc(kind: TaskForm['kind'], current: TaskForm, taskResult: GatewayTask | null): TaskForm {
const next = defaultTaskForKind(kind, current);
if (kind === 'tasks.retrieve' && !next.taskId && taskResult && !taskResult.id.startsWith('docs-')) {
return { ...next, taskId: taskResult.id };
}
return next;
}
function defaultTaskForKind(kind: TaskForm['kind'], current: TaskForm): TaskForm { function defaultTaskForKind(kind: TaskForm['kind'], current: TaskForm): TaskForm {
if (kind === 'chat.completions') return { ...current, kind, model: 'gpt-4o-mini' }; if (kind === 'chat.completions') return { ...current, kind, model: 'gpt-4o-mini' };
if (kind === 'responses') {
return {
...current,
instructions: current.instructions ?? '回答保持简洁。',
kind,
model: 'gpt-4o-mini',
prompt: current.kind === kind ? current.prompt : '用一句话说明 Responses API 的用途。',
};
}
if (kind === 'embeddings') { if (kind === 'embeddings') {
return { return {
...current, ...current,
@ -360,18 +751,55 @@ function defaultTaskForKind(kind: TaskForm['kind'], current: TaskForm): TaskForm
}; };
} }
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' }; 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' }; if (kind === 'images.generations') return { ...current, kind, model: 'gpt-image-1' };
if (kind === 'videos.generations') {
return {
...current,
aspectRatio: current.aspectRatio ?? '16:9',
duration: current.duration ?? 5,
kind,
model: '豆包Seedance-2.0',
outputAudio: current.outputAudio ?? true,
prompt: current.kind === kind ? current.prompt : '电影感航拍镜头掠过日出时的雪山。',
resolution: current.resolution ?? '720p',
};
}
return { ...current, kind, model: 'task' };
} }
function requestBodyExample(task: TaskForm) { function requestBodyExample(task: TaskForm, section: ApiDocSection) {
const body = task.kind === 'chat.completions' const body = task.kind === 'chat.completions'
? { model: task.model, messages: [{ role: 'user', content: task.prompt }], runMode: 'simulation', simulation: true, stream: false } ? { model: task.model, messages: [{ role: 'user', content: task.prompt }], runMode: 'simulation', simulation: true, stream: false }
: task.kind === 'responses'
? {
model: task.model,
input: task.prompt,
instructions: task.instructions,
previous_response_id: task.previousResponseId || undefined,
store: true,
stream: false,
runMode: 'simulation',
simulation: true,
}
: task.kind === 'embeddings' : task.kind === 'embeddings'
? { model: task.model, input: embeddingInputExample(task.prompt), dimensions: task.dimensions ?? 4, runMode: 'simulation', simulation: true } ? { model: task.model, input: embeddingInputExample(task.prompt), dimensions: task.dimensions ?? 4, runMode: 'simulation', simulation: true }
: task.kind === 'reranks' : task.kind === 'reranks'
? { model: task.model, query: task.prompt, documents: rerankDocumentsExample(task.documents), top_n: task.topN ?? 2, runMode: 'simulation', simulation: true } ? { model: task.model, query: task.prompt, documents: rerankDocumentsExample(task.documents), top_n: task.topN ?? 2, runMode: 'simulation', simulation: true }
: task.kind === 'images.edits' : 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, image: task.image, mask: task.mask, runMode: 'simulation', simulation: true }
: task.kind === 'videos.generations'
? {
model: task.model,
content: [{ type: 'text', text: task.prompt }],
duration: task.duration ?? 5,
resolution: task.resolution ?? '720p',
aspect_ratio: task.aspectRatio ?? '16:9',
audio: task.outputAudio ?? true,
runMode: 'simulation',
simulation: true,
}
: section === 'pricing'
? { kind: 'chat.completions', model: 'gpt-4o-mini', messages: [{ role: 'user', content: '你好' }], max_tokens: 512 }
: { model: task.model, prompt: task.prompt, quality: 'medium', runMode: 'simulation', simulation: true, size: '1024x1024' }; : { model: task.model, prompt: task.prompt, quality: 'medium', runMode: 'simulation', simulation: true, size: '1024x1024' };
return JSON.stringify(body, null, 2); return JSON.stringify(body, null, 2);
} }
@ -380,24 +808,37 @@ function parseBody(value: string, current: TaskForm): TaskForm {
try { try {
const body = JSON.parse(value) as { const body = JSON.parse(value) as {
image?: string; image?: string;
aspect_ratio?: string;
audio?: boolean;
content?: Array<{ text?: string; type?: string }>;
duration?: number;
instructions?: string;
mask?: string; mask?: string;
messages?: Array<{ content?: string }>; messages?: Array<{ content?: string }>;
model?: string; model?: string;
previous_response_id?: string;
prompt?: string; prompt?: string;
input?: string | string[]; input?: unknown;
query?: string; query?: string;
documents?: string[]; documents?: string[];
resolution?: string;
top_n?: number; top_n?: number;
dimensions?: number; dimensions?: number;
}; };
return { return {
...current, ...current,
aspectRatio: body.aspect_ratio ?? current.aspectRatio,
dimensions: numberOrCurrent(body.dimensions, current.dimensions), dimensions: numberOrCurrent(body.dimensions, current.dimensions),
documents: Array.isArray(body.documents) ? body.documents.join('\n') : current.documents, documents: Array.isArray(body.documents) ? body.documents.join('\n') : current.documents,
duration: numberOrCurrent(body.duration, current.duration),
image: body.image ?? current.image, image: body.image ?? current.image,
instructions: body.instructions ?? current.instructions,
mask: body.mask ?? current.mask, mask: body.mask ?? current.mask,
model: body.model ?? current.model, model: body.model ?? current.model,
prompt: body.prompt ?? body.query ?? inputText(body.input) ?? body.messages?.[0]?.content ?? current.prompt, outputAudio: typeof body.audio === 'boolean' ? body.audio : current.outputAudio,
previousResponseId: body.previous_response_id ?? current.previousResponseId,
prompt: body.prompt ?? body.query ?? inputText(body.input) ?? contentText(body.content) ?? body.messages?.[0]?.content ?? current.prompt,
resolution: body.resolution ?? current.resolution,
topN: numberOrCurrent(body.top_n, current.topN), topN: numberOrCurrent(body.top_n, current.topN),
}; };
} catch { } catch {
@ -405,7 +846,96 @@ function parseBody(value: string, current: TaskForm): TaskForm {
} }
} }
function bodyParamRows(section: ApiDocSection) { function taskResponseParamRows(): ApiDocParam[] {
return [
{ name: 'id', type: 'string', required: true, value: 'Gateway 任务 ID与异步受理响应中的 taskId 相同' },
{ name: 'kind', type: 'string', required: true, value: '原始任务类型,例如 chat.completions、images.generations 或 videos.generations' },
{ name: 'model', type: 'string', required: true, value: '请求使用的模型 ID 或别名' },
{ name: 'status', type: 'string', required: true, value: 'queued、submitting、running、succeeded、failed 或 cancelled' },
{ name: 'asyncMode', type: 'boolean', required: true, value: '是否以异步模式提交' },
{ name: 'cancellable', type: 'boolean', value: '当前状态是否允许取消' },
{ name: 'submitted', type: 'boolean', value: '任务是否已经提交到上游供应商' },
{ name: 'message', type: 'string', value: '当前状态或取消能力的补充说明' },
{ name: 'requestId', type: 'string', value: '上游请求 ID用于日志和供应商问题定位' },
{ name: 'result', type: 'object', value: '任务成功后的归一化结果;内部结构与原始文本、向量、重排序、图片、视频或音频接口一致' },
{
name: 'usage',
type: 'object',
value: '归一化用量数据;媒体任务可能使用能力相关的用量字段',
children: [
{ name: 'inputTokens', type: 'number', value: '输入 token 数' },
{ name: 'outputTokens', type: 'number', value: '输出 token 数' },
{ name: 'totalTokens', type: 'number', value: '总 token 数' },
],
},
{
name: 'billings',
type: 'array<object>',
value: '逐项计费明细',
children: [
{ name: 'resourceType', type: 'string', required: true, value: '计费资源类型,例如 text_input、image 或 video' },
{ name: 'unit', type: 'string', required: true, value: '计费单位,例如 1k_tokens、image 或 5s_video' },
{ name: 'quantity', type: 'number', required: true, value: '计费数量' },
{ name: 'amount', type: 'number', required: true, value: '本计费项金额' },
{ name: 'currency', type: 'string', required: true, value: '金额单位,默认 resource' },
{ name: 'discountFactor', type: 'number', value: '实际应用的折扣系数' },
{ name: 'simulated', type: 'boolean', required: true, value: '是否为模拟计费' },
],
},
{
name: 'billingSummary',
type: 'object',
value: '计费汇总',
children: [
{ name: 'lineCount', type: 'number', required: true, value: '计费明细条数' },
{ name: 'totalAmount', type: 'number', required: true, value: '汇总金额' },
{ name: 'amountByCurrency', type: 'object', required: true, value: '按金额单位汇总的金额映射' },
{ name: 'currency', type: 'string', required: true, value: '单一金额单位或 mixed' },
{ name: 'simulated', type: 'boolean', required: true, value: '是否为模拟计费汇总' },
],
},
{ name: 'finalChargeAmount', type: 'number', required: true, value: '最终实际扣费金额;模拟任务不会真实扣费' },
{
name: 'attempts',
type: 'array<object>',
value: '执行尝试记录;发生重试或切换平台时可能包含多项',
children: [
{ name: 'attemptNo', type: 'number', required: true, value: '尝试序号,从 1 开始' },
{ name: 'provider', type: 'string', value: '供应商标识' },
{ name: 'modelName', type: 'string', value: 'Gateway 模型名称' },
{ name: 'providerModelName', type: 'string', value: '实际上游模型名称' },
{ name: 'status', type: 'string', required: true, value: '本次尝试状态' },
{ name: 'retryable', type: 'boolean', required: true, value: '失败后是否允许重试' },
{ name: 'simulated', type: 'boolean', required: true, value: '是否由 SimulationClient 执行' },
{ name: 'requestId', type: 'string', value: '本次尝试的上游请求 ID' },
{ name: 'responseDurationMs', type: 'number', required: true, value: '本次尝试的响应耗时(毫秒)' },
{ name: 'errorCode', type: 'string', value: '本次尝试失败时的错误码' },
{ name: 'errorMessage', type: 'string', value: '本次尝试失败时的错误信息' },
],
},
{ name: 'errorCode', type: 'string', value: '任务最终失败时的错误码' },
{ name: 'errorMessage', type: 'string', value: '任务最终失败时的错误信息' },
{ name: 'responseDurationMs', type: 'number', required: true, value: '任务响应耗时(毫秒)' },
{ name: 'createdAt', type: 'string', required: true, value: '任务创建时间ISO 8601 格式' },
{ name: 'updatedAt', type: 'string', required: true, value: '任务最后更新时间ISO 8601 格式' },
{ name: 'finishedAt', type: 'string', value: '任务进入终态的时间ISO 8601 格式' },
];
}
function bodyParamRows(section: ApiDocSection): ApiDocParam[] {
if (section === 'responses') {
return [
{ name: 'model', type: 'string', required: true, value: '模型 ID 或别名;网关优先选择支持 openai_responses 的平台模型' },
{ name: 'input', type: 'string|array', required: true, value: '本轮输入,可为文本或 Responses input item 数组' },
{ name: 'instructions', type: 'string', value: '本轮系统级指令' },
{ name: 'previous_response_id', type: 'string', value: '上一轮 response.id用于由客户端显式管理连续对话' },
{ name: 'store', type: 'boolean', value: '是否保存 Responses 链,默认 true' },
{ name: 'stream', type: 'boolean', value: '设为 true 时返回 Responses SSE 事件流' },
{ name: 'tools / tool_choice', type: 'array|object', value: '函数工具定义与选择策略Chat 回退只支持 function tools' },
{ name: 'reasoning / text', type: 'object', value: '推理强度、文本格式和结构化输出配置' },
{ name: 'background', type: 'boolean', value: 'OpenAI 协议字段Gateway 的持久化异步任务请使用 X-Async' },
];
}
if (section === 'embeddings') { if (section === 'embeddings') {
return [ return [
{ name: 'model', type: 'string', required: true, value: '模型 ID 或别名,例如 text-embedding-v4' }, { name: 'model', type: 'string', required: true, value: '模型 ID 或别名,例如 text-embedding-v4' },
@ -423,19 +953,131 @@ function bodyParamRows(section: ApiDocSection) {
{ name: 'runMode / simulation', type: 'string|boolean', value: '在线测试时使用 simulation不消耗真实上游额度' }, { name: 'runMode / simulation', type: 'string|boolean', value: '在线测试时使用 simulation不消耗真实上游额度' },
]; ];
} }
if (section === 'videoGeneration') {
return [
{ name: 'model', type: 'string', required: true, value: '视频模型 ID 或别名,例如 豆包Seedance-2.0' },
{
name: 'content',
type: 'array<object>',
required: true,
value: '文本和参考素材对象数组;每一项由 type 决定应填写的内容字段和 role',
children: [
{ name: 'type', type: 'string', required: true, value: '内容类型text、image_url、video_url、audio_url 或 element' },
{ name: 'text', type: 'string', value: 'type=text 时的提示词;配合 role=shot_prompt 可描述指定分镜' },
{
name: 'image_url',
type: 'object',
value: 'type=image_url 时的图片素材',
children: [
{ name: 'url', type: 'string', required: true, value: '可公开访问或已上传的图片 URL' },
],
},
{
name: 'video_url',
type: 'object',
value: 'type=video_url 时的视频素材及参考方式',
children: [
{ name: 'url', type: 'string', required: true, value: '可公开访问或已上传的视频 URL' },
{ name: 'mime_type', type: 'string', value: '视频 MIME 类型,例如 video/mp4' },
{ name: 'refer_type', type: 'string', value: '参考方式feature特征参考或 base基础视频' },
{ name: 'keep_original_sound', type: 'string', value: '是否保留原声yes 或 no' },
],
},
{
name: 'audio_url',
type: 'object',
value: 'type=audio_url 时的音频素材',
children: [
{ name: 'url', type: 'string', required: true, value: '可公开访问或已上传的音频 URL' },
{ name: 'mime_type', type: 'string', value: '音频 MIME 类型,例如 audio/mpeg' },
],
},
{ name: 'role', type: 'string', value: '素材角色,例如 first_frame、last_frame、reference_image、reference_video、reference_audio、element 或 shot_prompt' },
{ name: 'shot_index', type: 'number', value: '素材或提示词对应的分镜序号' },
{ name: 'duration', type: 'number', value: '当前分镜或素材的时长(秒)' },
{ name: 'name', type: 'string', value: '素材或元素名称' },
{
name: 'element',
type: 'object',
value: 'type=element 时使用的系统元素或内联元素',
children: [
{ name: 'system_element_id', type: 'string', value: '已存在的系统元素 ID' },
{
name: 'inline_element',
type: 'object',
value: '随请求提交的内联元素定义',
children: [
{ name: 'name', type: 'string', required: true, value: '内联元素名称' },
{ name: 'description', type: 'string', value: '内联元素描述' },
{ name: 'frontal_image_url', type: 'string', required: true, value: '元素正面参考图 URL' },
{
name: 'refer_images',
type: 'array<object>',
required: true,
value: '元素的补充参考图片对象数组',
children: [
{ name: 'url', type: 'string', required: true, value: '参考图片 URL' },
{ name: 'slot_key', type: 'string', value: '参考图槽位标识' },
],
},
{ name: 'tags', type: 'array<string>', value: '元素标签数组' },
],
},
],
},
],
},
{ name: 'duration', type: 'number', value: '视频时长(秒),允许范围取决于模型能力' },
{ name: 'resolution', type: 'string', value: '输出分辨率,例如 480p、720p、1080p' },
{ name: 'aspect_ratio', type: 'string', value: '画幅,例如 16:9、9:16、1:1 或 adaptive' },
{ name: 'audio', type: 'boolean', value: '是否生成或保留声音,需模型支持 output_audio' },
{
name: 'audio_list',
type: 'array<object>',
value: '额外音频素材对象数组',
children: [
{ name: 'url', type: 'string', value: '音频 URL' },
{ name: 'audio_url', type: 'string', value: '音频 URL 的兼容字段' },
{ name: 'name', type: 'string', value: '音频名称' },
],
},
{ name: 'prompt', type: 'string', value: '纯文生视频的快捷字段;网关会转换为 content 文本项' },
{ name: 'runMode / simulation', type: 'string|boolean', value: '在线测试时使用 simulation不触达真实视频供应商' },
];
}
if (section === 'pricing') {
return [
{ name: 'kind', type: 'string', value: '任务类型,默认 chat.completions' },
{ name: 'model', type: 'string', required: true, value: '需要估价的模型 ID 或别名' },
{ name: 'messages / prompt', type: 'array|string', value: '用于估算输入规模的请求内容' },
{ name: 'max_tokens / n', type: 'number', value: '预期输出 token 或媒体数量' },
];
}
if (section === 'chat') {
return [ return [
{ name: 'model', type: 'string', required: true, value: '模型 ID 或别名' }, { name: 'model', type: 'string', required: true, value: '模型 ID 或别名' },
{ name: 'messages / prompt', type: 'array|string', required: true, value: '对话消息或图片提示词' }, { name: 'messages', type: 'array', required: true, value: 'OpenAI Chat 消息数组' },
{ name: 'stream', type: 'boolean', value: '设为 true 时返回 Chat Completions SSE' },
{ name: 'tools / tool_choice', type: 'array|object', value: '函数工具定义与选择策略' },
{ name: 'simulation', type: 'boolean', value: '测试模式开关' },
];
}
return [
{ name: 'model', type: 'string', required: true, value: '模型 ID 或别名' },
{ name: 'prompt', type: 'string', required: true, value: '图片生成或编辑提示词' },
{ name: 'image / mask', type: 'string|array', value: '图像编辑输入与可选遮罩' },
{ name: 'simulation', type: 'boolean', value: '测试模式开关' }, { name: 'simulation', type: 'boolean', value: '测试模式开关' },
{ name: 'stream', type: 'boolean', value: '对话进度流式返回' },
]; ];
} }
function docSectionForKind(kind: TaskKind): ApiDocSection { function docSectionForKind(kind: TaskKind): ApiDocSection {
if (kind === 'responses') return 'responses';
if (kind === 'embeddings') return 'embeddings'; if (kind === 'embeddings') return 'embeddings';
if (kind === 'reranks') return 'reranks'; if (kind === 'reranks') return 'reranks';
if (kind === 'images.generations') return 'imageGeneration'; if (kind === 'images.generations') return 'imageGeneration';
if (kind === 'images.edits') return 'imageEdit'; if (kind === 'images.edits') return 'imageEdit';
if (kind === 'videos.generations') return 'videoGeneration';
if (kind === 'tasks.retrieve') return 'taskRetrieve';
return 'chat'; return 'chat';
} }
@ -457,9 +1099,17 @@ function splitLines(value: string) {
.filter(Boolean); .filter(Boolean);
} }
function inputText(value: string | string[] | undefined) { function inputText(value: unknown) {
if (Array.isArray(value)) return value.join('\n'); if (typeof value === 'string') return value;
return value; if (Array.isArray(value)) {
const texts = value.map((item) => typeof item === 'string' ? item : '').filter(Boolean);
return texts.length ? texts.join('\n') : undefined;
}
return undefined;
}
function contentText(value?: Array<{ text?: string; type?: string }>) {
return value?.find((item) => item.type === 'text' && item.text)?.text;
} }
function numberOrCurrent(value: unknown, current?: number) { function numberOrCurrent(value: unknown, current?: number) {

View File

@ -0,0 +1,27 @@
import { describe, expect, it } from 'vitest';
import { parseAppRoute, pathForApiDocSection } from './routing';
describe('API documentation routes', () => {
it.each([
['guideBaseUrl', '/docs/auth'],
['guideWebhook', '/docs/webhook'],
['guideErrors', '/docs/errors'],
['guideTesting', '/docs/testing'],
['responses', '/docs/responses'],
['videoGeneration', '/docs/videos/generations'],
['asyncMode', '/docs/async'],
['taskRetrieve', '/docs/tasks/retrieve'],
] as const)('maps %s to a stable documentation URL', (section, path) => {
expect(pathForApiDocSection(section)).toBe(path);
expect(parseAppRoute(path).apiDocSection).toBe(section);
});
it.each([
['/docs/playground', 'guideTesting'],
['/docs/api/responses', 'responses'],
['/docs/api/videos', 'videoGeneration'],
['/docs/api/tasks', 'taskRetrieve'],
] as const)('keeps the legacy documentation URL %s working', (path, section) => {
expect(parseAppRoute(path).apiDocSection).toBe(section);
});
});

View File

@ -43,11 +43,19 @@ const adminPaths: Record<AdminSection, string> = {
}; };
const docsPaths: Record<ApiDocSection, string> = { const docsPaths: Record<ApiDocSection, string> = {
guideBaseUrl: '/docs/auth',
guideWebhook: '/docs/webhook',
guideErrors: '/docs/errors',
guideTesting: '/docs/testing',
chat: '/docs/chat', chat: '/docs/chat',
responses: '/docs/responses',
embeddings: '/docs/embeddings', embeddings: '/docs/embeddings',
reranks: '/docs/reranks', reranks: '/docs/reranks',
imageGeneration: '/docs/images/generations', imageGeneration: '/docs/images/generations',
imageEdit: '/docs/images/edits', imageEdit: '/docs/images/edits',
videoGeneration: '/docs/videos/generations',
asyncMode: '/docs/async',
taskRetrieve: '/docs/tasks/retrieve',
pricing: '/docs/pricing', pricing: '/docs/pricing',
files: '/docs/files', files: '/docs/files',
}; };
@ -63,8 +71,9 @@ const adminSections = reverseMap(adminPaths);
const docsSections = reverseMap(docsPaths); const docsSections = reverseMap(docsPaths);
const playgroundSections = reverseMap(playgroundPaths); const playgroundSections = reverseMap(playgroundPaths);
export function parseAppRoute(input = `${window.location.pathname}${window.location.search}`): AppRouteState { export function parseAppRoute(input = currentLocationPath()): AppRouteState {
const url = new URL(input, window.location.origin); const origin = typeof window === 'undefined' ? 'http://localhost' : window.location.origin;
const url = new URL(input, origin);
const path = normalizePath(url.pathname); const path = normalizePath(url.pathname);
const workspaceTaskQuery = parseWorkspaceTaskQuery(url.searchParams); const workspaceTaskQuery = parseWorkspaceTaskQuery(url.searchParams);
if (path === '/') return { ...defaultRouteState }; if (path === '/') return { ...defaultRouteState };
@ -84,6 +93,11 @@ export function parseAppRoute(input = `${window.location.pathname}${window.locat
return { ...defaultRouteState }; return { ...defaultRouteState };
} }
function currentLocationPath() {
if (typeof window === 'undefined') return '/';
return `${window.location.pathname}${window.location.search}`;
}
export function pathForPage(page: PageKey, route: AppRouteState): string { export function pathForPage(page: PageKey, route: AppRouteState): string {
if (page === 'playground') return pathForPlaygroundMode(route.playgroundMode); if (page === 'playground') return pathForPlaygroundMode(route.playgroundMode);
if (page === 'models') return '/models'; if (page === 'models') return '/models';
@ -135,11 +149,14 @@ function parseAdminSection(path: string): AdminSection {
function parseDocSection(path: string): ApiDocSection { function parseDocSection(path: string): ApiDocSection {
if (path === '/docs') return 'chat'; if (path === '/docs') return 'chat';
if (path === '/docs/playground') return 'guideTesting';
if (path === '/docs/api/chat') return 'chat'; if (path === '/docs/api/chat') return 'chat';
if (path === '/docs/api/responses') return 'responses';
if (path === '/docs/api/embeddings') return 'embeddings'; if (path === '/docs/api/embeddings') return 'embeddings';
if (path === '/docs/api/reranks') return 'reranks'; if (path === '/docs/api/reranks') return 'reranks';
if (path === '/docs/api/media') return 'imageGeneration'; if (path === '/docs/api/media') return 'imageGeneration';
if (path === '/docs/playground') return 'chat'; if (path === '/docs/api/videos') return 'videoGeneration';
if (path === '/docs/api/tasks') return 'taskRetrieve';
return docsSections[path] ?? 'chat'; return docsSections[path] ?? 'chat';
} }

View File

@ -218,6 +218,7 @@
.paramRow { .paramRow {
display: grid; display: grid;
grid-template-columns: 150px 110px minmax(0, 1fr) 50px; grid-template-columns: 150px 110px minmax(0, 1fr) 50px;
align-items: start;
gap: 12px; gap: 12px;
padding: 13px 16px; padding: 13px 16px;
border-bottom: 1px solid #f0f2f5; border-bottom: 1px solid #f0f2f5;
@ -225,11 +226,91 @@
font-size: 0.8125rem; font-size: 0.8125rem;
} }
.paramRow[data-nested='true'] {
background: #fbfcfe;
}
.paramName {
position: relative;
min-width: 0;
}
.paramName code {
overflow-wrap: anywhere;
}
.paramTreeMarker {
position: absolute;
top: -13px;
width: 12px;
height: 23px;
border-bottom: 1px solid #cfd7e3;
border-left: 1px solid #cfd7e3;
border-radius: 0 0 0 4px;
}
.paramRow em { .paramRow em {
color: #a16207; color: #a16207;
font-style: normal; font-style: normal;
} }
.docsDetailContent {
display: grid;
gap: 12px;
padding: 16px;
color: var(--text-soft);
font-size: 0.875rem;
line-height: 1.7;
}
.docsDetailContent p {
margin: 0;
}
.docsDetailContent pre {
overflow: auto;
margin: 0;
padding: 14px;
border-radius: 8px;
background: #f7f8fa;
color: var(--text-normal);
font-size: 0.75rem;
line-height: 1.6;
}
.docsSteps {
display: grid;
gap: 8px;
margin: 0;
padding-left: 20px;
}
.docsGuideActions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
.docsStatusGrid {
display: grid;
grid-template-columns: minmax(190px, auto) minmax(0, 1fr);
gap: 0;
}
.docsStatusGrid > * {
margin: 0;
padding: 13px 16px;
border-bottom: 1px solid #f0f2f5;
color: var(--text-soft);
font-size: 0.8125rem;
line-height: 1.6;
}
.docsStatusGrid > code {
color: var(--text-normal);
}
.docsRunner { .docsRunner {
border-left: 1px solid var(--border); border-left: 1px solid var(--border);
background: #fff; background: #fff;
@ -241,6 +322,40 @@
padding: 0 16px 16px; padding: 0 16px 16px;
} }
.docsRunnerUnavailable {
padding: 14px;
border: 1px dashed var(--border);
border-radius: 8px;
background: var(--surface-muted);
color: var(--text-soft);
font-size: 0.8125rem;
line-height: 1.6;
}
.docsGuideAside header {
display: flex;
align-items: center;
min-height: 58px;
}
.docsGuideAside > div {
display: grid;
gap: 10px;
padding: 18px 16px;
color: var(--text-soft);
font-size: 0.8125rem;
line-height: 1.6;
}
.docsGuideAside span,
.docsGuideAside p {
margin: 0;
}
.docsGuideAside strong {
color: var(--text-normal);
}
.runnerResult { .runnerResult {
display: grid; display: grid;
gap: 12px; gap: 12px;
@ -279,6 +394,10 @@
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.docsStatusGrid {
grid-template-columns: 1fr;
}
.agentResourceCard { .agentResourceCard {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }

View File

@ -1,10 +1,33 @@
export type LoadState = 'idle' | 'loading' | 'ready' | 'error'; export type LoadState = 'idle' | 'loading' | 'ready' | 'error';
export type AuthMode = 'login' | 'register' | 'external'; export type AuthMode = 'login' | 'register' | 'external';
export type TaskKind = 'chat.completions' | 'embeddings' | 'reranks' | 'images.generations' | 'images.edits'; export type TaskKind =
| 'chat.completions'
| 'responses'
| 'embeddings'
| 'reranks'
| 'images.generations'
| 'images.edits'
| 'videos.generations'
| 'tasks.retrieve';
export type PageKey = 'home' | 'playground' | 'models' | 'workspace' | 'admin' | 'docs'; export type PageKey = 'home' | 'playground' | 'models' | 'workspace' | 'admin' | 'docs';
export type PlaygroundMode = 'chat' | 'image' | 'video'; export type PlaygroundMode = 'chat' | 'image' | 'video';
export type WorkspaceSection = 'overview' | 'billing' | 'apiKeys' | 'tasks' | 'transactions'; export type WorkspaceSection = 'overview' | 'billing' | 'apiKeys' | 'tasks' | 'transactions';
export type ApiDocSection = 'chat' | 'embeddings' | 'reranks' | 'imageGeneration' | 'imageEdit' | 'pricing' | 'files'; export type ApiDocSection =
| 'guideBaseUrl'
| 'guideWebhook'
| 'guideErrors'
| 'guideTesting'
| 'chat'
| 'responses'
| 'embeddings'
| 'reranks'
| 'imageGeneration'
| 'imageEdit'
| 'videoGeneration'
| 'asyncMode'
| 'taskRetrieve'
| 'pricing'
| 'files';
export type AdminSection = export type AdminSection =
| 'overview' | 'overview'
| 'globalModels' | 'globalModels'
@ -37,10 +60,17 @@ export interface TaskForm {
kind: TaskKind; kind: TaskKind;
model: string; model: string;
prompt: string; prompt: string;
aspectRatio?: string;
dimensions?: number; dimensions?: number;
documents?: string; documents?: string;
duration?: number;
image?: string; image?: string;
instructions?: string;
mask?: string; mask?: string;
outputAudio?: boolean;
previousResponseId?: string;
resolution?: string;
taskId?: string;
topN?: number; topN?: number;
} }