返回 AiToEarn
openai.service.ts
根目录 / project / aitoearn-backend / apps / aitoearn-ai / src / core / ai / libs / openai / openai.service.ts
1 import { AIMessageChunk, BaseMessage } from '@langchain/core/messages'
2 import { ChatOpenAI, OpenAIChatInput } from '@langchain/openai'
3 import { Injectable, Logger, Optional } from '@nestjs/common'
4 import OpenAI from 'openai'
5 import { AiAvailabilityService } from '../../../ai-availability'
6 import { RelayMediaResolverService } from '../../relay-media'
7 import { OpenaiConfig } from './openai.config'
8 import { SoraCharacterResponse, SoraCreateCharacterRequest } from './openai.interface'
9
10 @Injectable()
11 export class OpenaiService {
12 private readonly logger = new Logger(OpenaiService.name)
13 private readonly openAI: OpenAI
14 private readonly chatOpenAI: ChatOpenAI
15
16 constructor(
17 private readonly config: OpenaiConfig,
18 private readonly aiAvailability: AiAvailabilityService,
19 @Optional() private readonly relayMediaResolver?: RelayMediaResolverService,
20 ) {
21 this.openAI = this._createOpenAIClient()
22 this.chatOpenAI = this._createChatModel({})
23 }
24
25 private async withAvailability<T>(operation: string, fn: () => Promise<T>, model?: string): Promise<T> {
26 return this.aiAvailability.execute(
27 { provider: 'openai', operation, model },
28 fn,
29 )
30 }
31
32 private _createOpenAIClient(): OpenAI {
33 return new OpenAI({
34 apiKey: this.config.apiKey,
35 baseURL: this.config.baseUrl,
36 timeout: this.config.timeout,
37 })
38 }
39
40 private _createChatModel(options: Partial<OpenAIChatInput>): ChatOpenAI {
41 return new ChatOpenAI({
42 ...options,
43 maxRetries: 1,
44 timeout: options.timeout ?? this.config.timeout,
45 apiKey: options.apiKey ?? this.config.apiKey,
46 configuration: {
47 baseURL: this.config.baseUrl,
48 },
49 streaming: true,
50 })
51 }
52
53 async createChatCompletionStream(options: Partial<OpenAIChatInput> & {
54 model: string
55 messages: BaseMessage[]
56 }) {
57 const {
58 messages,
59 } = options
60
61 const chatModel = this._createChatModel(options)
62 return await chatModel.stream(messages, options)
63 }
64
65 async createRawStream(options: OpenAI.Chat.ChatCompletionCreateParamsStreaming) {
66 return this.withAvailability('createRawStream', async () => {
67 const resolvedOptions = await this.resolveRelayJson(options)
68 return this.openAI.chat.completions.create(resolvedOptions)
69 }, options.model)
70 }
71
72 async createRawCompletion(options: OpenAI.Chat.ChatCompletionCreateParamsNonStreaming): Promise<OpenAI.Chat.ChatCompletion> {
73 return this.withAvailability('createRawCompletion', async () => {
74 const resolvedOptions = await this.resolveRelayJson(options)
75 return this.openAI.chat.completions.create(resolvedOptions)
76 }, options.model)
77 }
78
79 async createChatCompletion(options: Partial<OpenAIChatInput> & {
80 model: string
81 messages: BaseMessage[]
82 }): Promise<AIMessageChunk> {
83 return this.withAvailability('createChatCompletion', async () => {
84 const stream = await this.createChatCompletionStream(options)
85 let result: AIMessageChunk | undefined
86
87 for await (const chunk of stream) {
88 if (result) {
89 result = result.concat(chunk)
90 }
91 else {
92 result = chunk
93 }
94 }
95
96 return result!
97 }, options.model)
98 }
99
100 async createImageGeneration(options: Omit<OpenAI.Images.ImageGenerateParams, 'user' | 'stream'>): Promise<OpenAI.Images.ImagesResponse> {
101 return this.withAvailability('createImageGeneration', async () => {
102 const resolvedOptions = await this.resolveRelayJson(options)
103 return this.openAI.images.generate(resolvedOptions)
104 }, options.model ?? undefined)
105 }
106
107 async createImageEdit(options: Omit<OpenAI.Images.ImageEditParams, 'user' | 'stream'>): Promise<OpenAI.Images.ImagesResponse> {
108 return this.withAvailability('createImageEdit', async () => {
109 return this.openAI.images.edit(options)
110 }, options.model ?? undefined)
111 }
112
113 async createImageVariation(options: Omit<OpenAI.Images.ImageCreateVariationParams, 'user'>): Promise<OpenAI.Images.ImagesResponse> {
114 return this.withAvailability('createImageVariation', async () => {
115 return this.openAI.images.createVariation(options)
116 })
117 }
118
119 private normalizeVideoTimestamp(video: OpenAI.Videos.Video): OpenAI.Videos.Video {
120 // 判断阈值:10000000000 对应 2001-09-09
121 // 大于此值则认为是毫秒值,需转换为秒值
122 if (video.created_at > 10000000000) {
123 return {
124 ...video,
125 created_at: Math.floor(video.created_at / 1000),
126 }
127 }
128 return video
129 }
130
131 async createVideo(params: OpenAI.VideoCreateParams): Promise<OpenAI.Videos.Video> {
132 return this.withAvailability('createVideo', async () => {
133 const video = await this.openAI.videos.create(params)
134 return this.normalizeVideoTimestamp(video)
135 }, params.model)
136 }
137
138 async retrieveVideo(videoId: string): Promise<OpenAI.Videos.Video> {
139 return this.withAvailability('retrieveVideo', async () => {
140 const video = await this.openAI.videos.retrieve(videoId)
141 return this.normalizeVideoTimestamp(video)
142 })
143 }
144
145 async listVideos(params?: OpenAI.VideoListParams): Promise<OpenAI.Videos.VideosPage> {
146 return this.withAvailability('listVideos', async () => {
147 const result = await this.openAI.videos.list(params)
148 result.data = result.data.map(video => this.normalizeVideoTimestamp(video))
149 return result
150 })
151 }
152
153 async deleteVideo(videoId: string): Promise<OpenAI.Videos.VideoDeleteResponse> {
154 return this.withAvailability('deleteVideo', async () => {
155 return this.openAI.videos.delete(videoId)
156 })
157 }
158
159 async downloadVideoContent(videoId: string, variant?: 'video' | 'thumbnail' | 'spritesheet'): Promise<Response> {
160 return this.withAvailability('downloadVideoContent', async () => {
161 return this.openAI.videos.downloadContent(videoId, { variant })
162 })
163 }
164
165 async remixVideo(videoId: string, prompt: string): Promise<OpenAI.Videos.Video> {
166 return this.withAvailability('remixVideo', async () => {
167 const video = await this.openAI.videos.remix(videoId, { prompt })
168 return this.normalizeVideoTimestamp(video)
169 })
170 }
171
172 async createCharacter(params: SoraCreateCharacterRequest): Promise<SoraCharacterResponse> {
173 return this.withAvailability('createCharacter', async () => {
174 const response = await this.openAI.videos.create(params as unknown as OpenAI.VideoCreateParams)
175 return response as unknown as SoraCharacterResponse
176 })
177 }
178
179 async getCharacter(characterId: string): Promise<SoraCharacterResponse> {
180 return this.withAvailability('getCharacter', async () => {
181 const response = await this.openAI.videos.retrieve(characterId)
182 return response as unknown as SoraCharacterResponse
183 })
184 }
185
186 private async resolveRelayJson<T>(value: T): Promise<T> {
187 if (!this.relayMediaResolver) {
188 return value
189 }
190 return await this.relayMediaResolver.resolveJson(value)
191 }
192 }
193
193 lines TYPESCRIPT