11import type { ClientOptions } from 'openai'
22import type { RequestInit } from 'undici'
3- import type { APIMODEL , AuditConfig , Config , KeyConfig , SearchResult , UserInfo } from '../storage/model'
3+ import type { APIMODEL , AuditConfig , Config , ImageUsageItem , KeyConfig , SearchResult , UserInfo } from '../storage/model'
44import type { TextAuditService } from '../utils/textAudit'
55import type { ChatMessage , RequestOptions } from './types'
66import { tavily } from '@tavily/core'
@@ -19,6 +19,59 @@ function renderSystemMessage(template: string, currentTime: string): string {
1919 return template . replace ( / \{ c u r r e n t _ t i m e \} / g, currentTime )
2020}
2121
22+ /**
23+ * 根据图片的size和quality计算token
24+ * @param size 图片尺寸,如 '1024x1024', '1024x1536', '1536x1024'
25+ * @param quality 图片质量,如 'low', 'medium', 'high'
26+ * @returns token数量,如果无法匹配则返回0
27+ */
28+ function calculateImageTokens ( size : string | undefined , quality : string | undefined ) : number {
29+ if ( ! size || ! quality ) {
30+ return 0
31+ }
32+
33+ // 标准化quality
34+ const normalizedQuality = quality . toLowerCase ( )
35+ // 标准化size,判断是square、portrait还是landscape
36+ const sizeLower = size . toLowerCase ( )
37+ let sizeType : 'square' | 'portrait' | 'landscape' | null = null
38+
39+ if ( sizeLower === '1024x1024' ) {
40+ sizeType = 'square'
41+ }
42+ else if ( sizeLower === '1024x1536' ) {
43+ sizeType = 'portrait'
44+ }
45+ else if ( sizeLower === '1536x1024' ) {
46+ sizeType = 'landscape'
47+ }
48+
49+ if ( ! sizeType ) {
50+ return 0
51+ }
52+
53+ // Token计算表
54+ const tokenMap : Record < string , Record < string , number > > = {
55+ low : {
56+ square : 272 ,
57+ portrait : 408 ,
58+ landscape : 400 ,
59+ } ,
60+ medium : {
61+ square : 1056 ,
62+ portrait : 1584 ,
63+ landscape : 1568 ,
64+ } ,
65+ high : {
66+ square : 4160 ,
67+ portrait : 6240 ,
68+ landscape : 6208 ,
69+ } ,
70+ }
71+
72+ return tokenMap [ normalizedQuality ] ?. [ sizeType ] || 0
73+ }
74+
2275const ErrorCodeMessage : Record < string , string > = {
2376 401 : '[OpenAI] 提供错误的API密钥 | Incorrect API key provided' ,
2477 403 : '[OpenAI] 服务器拒绝访问,请稍后再试 | Server refused to access, please try again later' ,
@@ -275,6 +328,27 @@ search result: <search_result>${searchResultContent}</search_result>`,
275328 }
276329 }
277330
331+ // 如果 tools 中有 image_generation,并且 keyConfig 中有配置,则使用 keyConfig 中的配置
332+ let finalTools = tools
333+ if ( tools && tools . length > 0 ) {
334+ finalTools = tools . map ( ( tool : any ) => {
335+ if ( tool . type === 'image_generation' ) {
336+ // 从 keyConfig 读取配置,如果不存在则使用默认值
337+ const inputFidelity = key . inputFidelity || tool . input_fidelity || 'high'
338+ const quality = key . quality || tool . quality || 'high'
339+ const model = key . imageModel || tool . model || 'gpt-image-1.5'
340+
341+ return {
342+ type : 'image_generation' ,
343+ input_fidelity : inputFidelity ,
344+ quality,
345+ model,
346+ }
347+ }
348+ return tool
349+ } )
350+ }
351+
278352 const stream = await openai . responses . create (
279353 {
280354 model,
@@ -283,7 +357,7 @@ search result: <search_result>${searchResultContent}</search_result>`,
283357 reasoning,
284358 store : options . room . toolsEnabled ,
285359 stream : true ,
286- ...( tools && tools . length > 0 && { tools : tools as any } ) ,
360+ ...( finalTools && finalTools . length > 0 && { tools : finalTools as any } ) ,
287361 // 如果有图片代表是编辑当前图片,不传递该参数,否则传递该参数
288362 ...( previousResponseId && uploadFileKeys . length === 0 && { previous_response_id : previousResponseId } ) ,
289363 } ,
@@ -299,6 +373,7 @@ search result: <search_result>${searchResultContent}</search_result>`,
299373 const usage = new UsageResponse ( )
300374 const toolCalls : Array < { type : string , result ?: any } > = [ ]
301375 let editImageId : string | undefined
376+ let imageUsageList : ImageUsageItem [ ] = [ ]
302377
303378 // 心跳机制:防止生图等长时间操作时连接超时
304379 let heartbeatInterval : NodeJS . Timeout | null = null
@@ -341,26 +416,52 @@ search result: <search_result>${searchResultContent}</search_result>`,
341416 usage . prompt_tokens = resp . usage . input_tokens
342417 usage . completion_tokens = resp . usage . output_tokens
343418 usage . total_tokens = resp . usage . total_tokens
419+ // 重置图片使用列表
420+ imageUsageList = [ ]
421+
422+ // 获取主模型和图片生成模型
423+ const mainModel = resp . model
424+ const imageModel = resp . tools && Array . isArray ( resp . tools ) && resp . tools . length > 0
425+ ? ( resp . tools . find ( ( tool : any ) => tool . type === 'image_generation' ) as any ) ?. model
426+ : undefined
344427
345428 // Extract tool calls from response
346429 if ( resp . output && Array . isArray ( resp . output ) ) {
347430 editImageId = responseId
348431 for ( const output of resp . output ) {
349- if ( output . type === 'image_generation_call' && output . result ) {
350- const base64Data = output . result
351- const fileIdentifier = await saveBase64ToFile ( base64Data )
352-
353- if ( fileIdentifier ) {
354- toolCalls . push ( {
355- type : 'image_generation' ,
356- result : fileIdentifier , // 文件名或S3 URL,前端会自动处理
357- } )
358- }
359- else {
360- toolCalls . push ( {
361- type : 'image_generation' ,
362- result : base64Data ,
363- } )
432+ if ( output . type === 'image_generation_call' ) {
433+ // 记录图片使用信息:size、quality、模型
434+ const imageOutput = output as any
435+ const imageSize = imageOutput . size
436+ const imageQuality = imageOutput . quality
437+ const imageTokens = calculateImageTokens ( imageSize , imageQuality )
438+
439+ imageUsageList . push ( {
440+ size : imageSize ,
441+ quality : imageQuality ,
442+ model : imageModel ,
443+ mainModel,
444+ tokens : imageTokens ,
445+ data_type : 'output' ,
446+ } )
447+
448+ // 处理图片结果
449+ if ( output . result ) {
450+ const base64Data = output . result
451+ const fileIdentifier = await saveBase64ToFile ( base64Data )
452+
453+ if ( fileIdentifier ) {
454+ toolCalls . push ( {
455+ type : 'image_generation' ,
456+ result : fileIdentifier , // 文件名或S3 URL,前端会自动处理
457+ } )
458+ }
459+ else {
460+ toolCalls . push ( {
461+ type : 'image_generation' ,
462+ result : base64Data ,
463+ } )
464+ }
364465 }
365466 }
366467 }
@@ -384,6 +485,7 @@ search result: <search_result>${searchResultContent}</search_result>`,
384485 detail : { usage } ,
385486 ...( toolCalls . length > 0 && { tool_calls : toolCalls } ) ,
386487 ...( editImageId && { editImageId } ) ,
488+ ...( imageUsageList . length > 0 && { image_usage : imageUsageList } ) ,
387489 }
388490 return sendResponse ( { type : 'Success' , data : response } )
389491 }
0 commit comments