初始化模版工程
This commit is contained in:
225
llm/client.ts
Normal file
225
llm/client.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
import type {
|
||||
AiSearchParams,
|
||||
AiSearchResponse,
|
||||
AiSearchResult,
|
||||
ChatParams,
|
||||
ChatResponse,
|
||||
Choice,
|
||||
ChoiceMessage,
|
||||
ImageParams,
|
||||
ImageResult,
|
||||
RerankParams,
|
||||
RerankResponse,
|
||||
Usage,
|
||||
VideoParams,
|
||||
VideoTaskResult,
|
||||
} from './models'
|
||||
|
||||
export class SkillsClient {
|
||||
private readonly apiKey: string;
|
||||
private readonly baseUrl: string;
|
||||
|
||||
constructor(options: { apiKey: string; baseUrl: string }) {
|
||||
this.apiKey = options.apiKey;
|
||||
this.baseUrl = options.baseUrl;
|
||||
}
|
||||
|
||||
private headers(): Record<string, string> {
|
||||
return {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
}
|
||||
|
||||
private url(path: string): string {
|
||||
return `${this.baseUrl}/skills/v1/${path}`;
|
||||
}
|
||||
|
||||
private async post<T>(path: string, body: unknown): Promise<T> {
|
||||
const resp = await fetch(this.url(path), {
|
||||
method: 'POST',
|
||||
headers: this.headers(),
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
throw new Error(`HTTP ${resp.status}: ${await resp.text()}`);
|
||||
}
|
||||
return resp.json() as Promise<T>;
|
||||
}
|
||||
|
||||
// ────────── Chat ──────────
|
||||
|
||||
private buildChatBody(params: ChatParams): Record<string, unknown> {
|
||||
const body: Record<string, unknown> = {
|
||||
model: params.model ?? 'gpt-5.1',
|
||||
messages: params.messages,
|
||||
stream: params.stream ?? false,
|
||||
};
|
||||
if (params.maxTokens != null) body.maxTokens = params.maxTokens;
|
||||
if (params.temperature != null) body.temperature = params.temperature;
|
||||
if (params.topP != null) body.topP = params.topP;
|
||||
if (params.responseFormat != null) body.responseFormat = params.responseFormat;
|
||||
if (params.tools != null) body.tools = params.tools;
|
||||
if (params.toolChoice != null) body.toolChoice = params.toolChoice;
|
||||
return body;
|
||||
}
|
||||
|
||||
/** 非流式聊天 */
|
||||
async chat(params: ChatParams): Promise<ChatResponse> {
|
||||
const resp = await fetch(this.url('chat/completions'), {
|
||||
method: 'POST',
|
||||
headers: this.headers(),
|
||||
body: JSON.stringify(this.buildChatBody({ ...params, stream: false })),
|
||||
});
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${await resp.text()}`);
|
||||
return parseChatResponse(await resp.json() as Record<string, unknown>);
|
||||
}
|
||||
|
||||
/** 流式聊天 (SSE) */
|
||||
async *chatStream(params: ChatParams): AsyncGenerator<ChatResponse> {
|
||||
const resp = await fetch(this.url('chat/completions'), {
|
||||
method: 'POST',
|
||||
headers: this.headers(),
|
||||
body: JSON.stringify(this.buildChatBody({ ...params, stream: true })),
|
||||
});
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${await resp.text()}`);
|
||||
if (!resp.body) throw new Error('Response body is null');
|
||||
|
||||
yield* parseSSEStream(resp.body, parseChatResponse);
|
||||
}
|
||||
|
||||
// ────────── Image ──────────
|
||||
|
||||
/** 图片生成 */
|
||||
async imageGenerate(params: ImageParams): Promise<ImageResult> {
|
||||
return this.post<ImageResult>('image/generate', {
|
||||
model: params.model ?? 'Nano Banana Pro',
|
||||
...params,
|
||||
});
|
||||
}
|
||||
|
||||
// ────────── Video ──────────
|
||||
|
||||
/** 创建视频生成任务 */
|
||||
async videoCreateTask(params: VideoParams): Promise<VideoTaskResult> {
|
||||
return this.post<VideoTaskResult>('video/tasks', {
|
||||
model: params.model ?? 'Doubao-Seedance-1.5-pro',
|
||||
...params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 查询视频生成任务状态 */
|
||||
async videoGetTask(params: Pick<VideoParams, 'taskId' | 'model'>): Promise<VideoTaskResult> {
|
||||
return this.post<VideoTaskResult>('video/tasks/query', {
|
||||
model: params.model ?? 'Doubao-Seedance-1.5-pro',
|
||||
taskId: params.taskId,
|
||||
});
|
||||
}
|
||||
|
||||
// ────────── AI Search ──────────
|
||||
|
||||
/** AI 搜索(非流式) */
|
||||
async aiSearch(params: AiSearchParams): Promise<AiSearchResult> {
|
||||
return this.post<AiSearchResult>('aiSearch', {
|
||||
model: params.model ?? 'aiSearch',
|
||||
...params,
|
||||
stream: false,
|
||||
});
|
||||
}
|
||||
|
||||
/** AI 搜索(流式 SSE) */
|
||||
async *aiSearchStream(params: AiSearchParams): AsyncGenerator<AiSearchResponse> {
|
||||
const resp = await fetch(this.url('aiSearch'), {
|
||||
method: 'POST',
|
||||
headers: this.headers(),
|
||||
body: JSON.stringify({
|
||||
model: params.model ?? 'aiSearch',
|
||||
...params,
|
||||
stream: true,
|
||||
}),
|
||||
});
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${await resp.text()}`);
|
||||
if (!resp.body) throw new Error('Response body is null');
|
||||
|
||||
yield* parseSSEStream(resp.body, (data) => (data.data ?? data) as AiSearchResponse);
|
||||
}
|
||||
|
||||
// ────────── Rerank ──────────
|
||||
|
||||
/** 重排 */
|
||||
async rerank(params: RerankParams): Promise<RerankResponse> {
|
||||
return this.post<RerankResponse>('rerank', {
|
||||
model: params.model ?? 'qwen3-vl-rerank',
|
||||
...params,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ────────── helpers ──────────
|
||||
|
||||
async function* parseSSEStream<T>(
|
||||
body: ReadableStream<Uint8Array>,
|
||||
parse: (data: Record<string, unknown>) => T,
|
||||
): AsyncGenerator<T> {
|
||||
const decoder = new TextDecoder();
|
||||
const reader = body.getReader();
|
||||
let buffer = '';
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() ?? '';
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || !trimmed.startsWith('data:')) continue;
|
||||
const dataStr = trimmed.slice(5).trim();
|
||||
if (dataStr === '[DONE]') return;
|
||||
try {
|
||||
yield parse(JSON.parse(dataStr) as Record<string, unknown>);
|
||||
} catch { /* skip malformed lines */ }
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
function parseChatResponse(data: Record<string, unknown>): ChatResponse {
|
||||
const rawChoices = (data.choices as Record<string, unknown>[] | undefined) ?? [];
|
||||
const choices: Choice[] = rawChoices.map((c) => {
|
||||
const msgData = c.message as Record<string, unknown> | undefined;
|
||||
const deltaData = c.delta as Record<string, unknown> | undefined;
|
||||
const toMsg = (d: Record<string, unknown>): ChoiceMessage => ({
|
||||
role: d.role as string | undefined,
|
||||
content: d.content as string | undefined,
|
||||
reasoningContent: d.reasoningContent as string | undefined,
|
||||
toolCalls: d.toolCalls as unknown[] | undefined,
|
||||
});
|
||||
return {
|
||||
message: msgData ? toMsg(msgData) : undefined,
|
||||
delta: deltaData ? toMsg(deltaData) : undefined,
|
||||
finishReason: (c.finishReason ?? c.finish_reason) as string | undefined,
|
||||
};
|
||||
});
|
||||
|
||||
const usageData = data.usage as Record<string, unknown> | undefined;
|
||||
const usage: Usage | undefined = usageData
|
||||
? {
|
||||
promptTokens: (usageData.promptTokens ?? usageData.prompt_tokens) as number | undefined,
|
||||
completionTokens: (usageData.completionTokens ?? usageData.completion_tokens) as number | undefined,
|
||||
totalTokens: (usageData.totalTokens ?? usageData.total_tokens) as number | undefined,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
id: data.id as string | undefined,
|
||||
model: data.model as string | undefined,
|
||||
success: data.success as boolean | undefined,
|
||||
errorMessage: (data.errorMessage ?? data.error_message) as string | undefined,
|
||||
choices,
|
||||
usage,
|
||||
};
|
||||
}
|
||||
29
llm/index.ts
Normal file
29
llm/index.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
export { SkillsClient } from './client'
|
||||
export type {
|
||||
// Chat
|
||||
Message,
|
||||
ChatParams,
|
||||
ChatResponse,
|
||||
Choice,
|
||||
ChoiceMessage,
|
||||
Usage,
|
||||
// Image
|
||||
ImageParams,
|
||||
ImageResult,
|
||||
// Video
|
||||
VideoParams,
|
||||
VideoParameters,
|
||||
VideoItem,
|
||||
VideoUsage,
|
||||
VideoResponse,
|
||||
VideoTaskResult,
|
||||
// AI Search
|
||||
AiSearchParams,
|
||||
AiSearchMessage,
|
||||
AiSearchResponse,
|
||||
AiSearchResult,
|
||||
// Rerank
|
||||
RerankParams,
|
||||
RerankItem,
|
||||
RerankResponse,
|
||||
} from './models'
|
||||
189
llm/models.ts
Normal file
189
llm/models.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
// ────────── Chat ──────────
|
||||
|
||||
export interface Message {
|
||||
role: 'system' | 'user' | 'assistant' | 'tool';
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface ChatParams {
|
||||
model?: string;
|
||||
messages: Message[];
|
||||
stream?: boolean;
|
||||
maxTokens?: number;
|
||||
temperature?: number;
|
||||
topP?: number;
|
||||
responseFormat?: Record<string, unknown>;
|
||||
tools?: unknown[];
|
||||
toolChoice?: unknown;
|
||||
}
|
||||
|
||||
export interface ChoiceMessage {
|
||||
role?: string;
|
||||
content?: string;
|
||||
reasoningContent?: string;
|
||||
toolCalls?: unknown[];
|
||||
}
|
||||
|
||||
export interface Choice {
|
||||
message?: ChoiceMessage;
|
||||
delta?: ChoiceMessage;
|
||||
finishReason?: string;
|
||||
}
|
||||
|
||||
export interface Usage {
|
||||
promptTokens?: number;
|
||||
completionTokens?: number;
|
||||
totalTokens?: number;
|
||||
}
|
||||
|
||||
export interface ChatResponse {
|
||||
id?: string;
|
||||
model?: string;
|
||||
success?: boolean;
|
||||
errorMessage?: string;
|
||||
choices: Choice[];
|
||||
usage?: Usage;
|
||||
}
|
||||
|
||||
// ────────── Image ──────────
|
||||
|
||||
export interface ImageParams {
|
||||
model?: string;
|
||||
prompt: string;
|
||||
imageUrlList?: string[];
|
||||
size?: string;
|
||||
quantityGenerated?: number;
|
||||
watermark?: boolean;
|
||||
resolution?: string;
|
||||
pollingWaitTime?: number;
|
||||
}
|
||||
|
||||
export interface ImageResult {
|
||||
success: boolean;
|
||||
data?: {
|
||||
imageType?: string;
|
||||
imageList?: string[];
|
||||
watermark?: boolean;
|
||||
};
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
// ────────── Video ──────────
|
||||
|
||||
export interface VideoParameters {
|
||||
resolution?: string;
|
||||
aspectRatio?: string;
|
||||
duration?: number;
|
||||
frames?: number;
|
||||
generateAudio?: boolean;
|
||||
watermark?: boolean;
|
||||
enhancePrompt?: boolean;
|
||||
seed?: number;
|
||||
draft?: boolean;
|
||||
serviceTier?: string;
|
||||
}
|
||||
|
||||
export interface VideoParams {
|
||||
model?: string;
|
||||
taskId?: string;
|
||||
prompt?: string;
|
||||
firstFrame?: string;
|
||||
lastFrame?: string;
|
||||
referenceImages?: unknown[];
|
||||
audioUrl?: string;
|
||||
videoUrl?: string;
|
||||
parameters?: VideoParameters;
|
||||
callbackUrl?: string;
|
||||
}
|
||||
|
||||
export interface VideoItem {
|
||||
videoUrl?: string;
|
||||
duration?: number;
|
||||
frames?: number;
|
||||
fps?: number;
|
||||
resolution?: string;
|
||||
aspectRatio?: string;
|
||||
hasAudio?: boolean;
|
||||
}
|
||||
|
||||
export interface VideoUsage {
|
||||
videoCount?: number;
|
||||
totalTokens?: number;
|
||||
resolution?: string;
|
||||
duration?: number;
|
||||
audio?: boolean;
|
||||
frames?: number;
|
||||
}
|
||||
|
||||
export interface VideoResponse {
|
||||
success: boolean;
|
||||
model?: string;
|
||||
errorMessage?: string;
|
||||
requestId?: string;
|
||||
taskId?: string;
|
||||
/** pending | running | succeeded | failed | cancelled | expired */
|
||||
status?: string;
|
||||
statusMessage?: string;
|
||||
videos?: VideoItem[];
|
||||
videoUsage?: VideoUsage;
|
||||
}
|
||||
|
||||
export interface VideoTaskResult {
|
||||
success: boolean;
|
||||
data?: VideoResponse;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
// ────────── AI Search ──────────
|
||||
|
||||
export interface AiSearchParams {
|
||||
model?: string;
|
||||
query: string;
|
||||
stream?: boolean;
|
||||
freshness?: string;
|
||||
}
|
||||
|
||||
export interface AiSearchMessage {
|
||||
type?: string;
|
||||
contentType?: string;
|
||||
content?: string;
|
||||
role?: string;
|
||||
}
|
||||
|
||||
export interface AiSearchResponse {
|
||||
success: boolean;
|
||||
isFinish?: boolean;
|
||||
index?: number;
|
||||
message?: AiSearchMessage;
|
||||
messages?: AiSearchMessage[];
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export interface AiSearchResult {
|
||||
success: boolean;
|
||||
data?: AiSearchResponse;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
// ────────── Rerank ──────────
|
||||
|
||||
export interface RerankParams {
|
||||
model?: string;
|
||||
query: string;
|
||||
documents: string[];
|
||||
topN?: number;
|
||||
returnDocument?: boolean;
|
||||
}
|
||||
|
||||
export interface RerankItem {
|
||||
relevanceScore?: number;
|
||||
index?: number;
|
||||
paragraph?: string;
|
||||
document?: unknown;
|
||||
}
|
||||
|
||||
export interface RerankResponse {
|
||||
success: boolean;
|
||||
data?: RerankItem[];
|
||||
errorMessage?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user