返回 oh-my-ppt
siliconflow.ts
根目录 / src / main / agent-runtime / provider / image / providers / siliconflow.ts
1 import log from 'electron-log/main.js'
2 import type {
3 ImageGenerationProviderAdapter,
4 ImageGenerationResult,
5 ResolvedImageModelConfig
6 } from '../types'
7 import { collectImageResults, joinUrl, readJsonResponse, readRecord, readString } from './utils'
8
9 const DEFAULT_BASE_URL = 'https://api.siliconflow.cn/v1'
10 const DEFAULT_ENDPOINT_PATH = '/images/generations'
11 const DEFAULT_MODEL = 'Tongyi-MAI/Z-Image-Turbo'
12 const LOG_TAG = 'siliconflow'
13
14 const QWEN_IMAGE_SIZE_MAP: Record<string, string> = {
15 '1:1': '1328x1328',
16 '16:9': '1664x928',
17 '9:16': '928x1664',
18 '4:3': '1472x1140',
19 '3:4': '1140x1472',
20 '3:2': '1584x1056',
21 '2:3': '1056x1584'
22 }
23
24 const KOLORS_SIZE_MAP: Record<string, string> = {
25 '1:1': '1024x1024',
26 '3:4': '960x1280',
27 '9:16': '720x1280',
28 '1:2': '720x1440'
29 }
30
31 const DEFAULT_SIZE_MAP: Record<string, string> = {
32 '1:1': '1024x1024',
33 '16:9': '1280x720',
34 '9:16': '720x1280',
35 '4:3': '1024x768',
36 '3:4': '768x1024'
37 }
38
39 const buildEndpoint = (config: ResolvedImageModelConfig): string => {
40 const endpoint = readString(config.modelConfig, 'endpoint')
41 if (endpoint) return endpoint
42 return joinUrl(readString(config.modelConfig, 'baseUrl') || DEFAULT_BASE_URL, DEFAULT_ENDPOINT_PATH)
43 }
44
45 const parseDimensionSize = (value: string): string => {
46 const match = /^\s*(\d{2,5})\s*[x*]\s*(\d{2,5})\s*$/i.exec(value)
47 if (!match) return ''
48 return `${Number(match[1])}x${Number(match[2])}`
49 }
50
51 const resolveSizeMap = (model: string): Record<string, string> => {
52 if (/qwen\/qwen-image/i.test(model)) return QWEN_IMAGE_SIZE_MAP
53 if (/kolors/i.test(model)) return KOLORS_SIZE_MAP
54 return DEFAULT_SIZE_MAP
55 }
56
57 const resolveImageSize = (config: ResolvedImageModelConfig, model: string, inputSize: string): string => {
58 const configuredSize =
59 readString(config.modelConfig, 'imageSize') ||
60 readString(config.modelConfig, 'image_size') ||
61 readString(config.modelConfig, 'size')
62 if (configuredSize) return parseDimensionSize(configuredSize) || configuredSize
63 return parseDimensionSize(inputSize) || resolveSizeMap(model)[inputSize] || inputSize
64 }
65
66 const readNumber = (config: ResolvedImageModelConfig, key: string): number | undefined => {
67 const value = Number(config.modelConfig[key])
68 return Number.isFinite(value) ? value : undefined
69 }
70
71 const buildOptionalParameters = (
72 config: ResolvedImageModelConfig,
73 input: Parameters<ImageGenerationProviderAdapter['generate']>[1],
74 model: string
75 ): Record<string, unknown> => {
76 const params: Record<string, unknown> = {}
77 if (input.negativePrompt) params.negative_prompt = input.negativePrompt
78 if (typeof input.seed === 'number') params.seed = input.seed
79
80 const numInferenceSteps = readNumber(config, 'numInferenceSteps') ?? readNumber(config, 'num_inference_steps')
81 if (numInferenceSteps !== undefined) params.num_inference_steps = numInferenceSteps
82
83 const guidanceScale = readNumber(config, 'guidanceScale') ?? readNumber(config, 'guidance_scale')
84 if (guidanceScale !== undefined) params.guidance_scale = guidanceScale
85
86 const cfg = readNumber(config, 'cfg')
87 if (cfg !== undefined) params.cfg = cfg
88
89 if (/kolors/i.test(model)) {
90 params.batch_size = 1
91 }
92 return params
93 }
94
95 const collectSiliconFlowImages = async (
96 payload: unknown,
97 signal?: AbortSignal
98 ): Promise<ImageGenerationResult[]> => {
99 const record = readRecord(payload)
100 return collectImageResults({ output: { images: Array.isArray(record.images) ? record.images : [] } }, signal)
101 }
102
103 const toErrorMessage = (error: unknown): string =>
104 error instanceof Error ? error.message : String(error)
105
106 export const siliconFlowAdapter: ImageGenerationProviderAdapter = {
107 async generate(config, input) {
108 const startedAt = Date.now()
109 const endpoint = buildEndpoint(config)
110 const model = readString(config.modelConfig, 'model') || DEFAULT_MODEL
111 const apiKey = readString(config.modelConfig, 'apiKey')
112 if (!apiKey) throw new Error('硅基流动需要 API Key。')
113
114 const imageSize = resolveImageSize(config, model, input.size)
115 const requestBody = readRecord(config.modelConfig.requestBody)
116 const headers = readRecord(config.modelConfig.headers) as Record<string, string>
117 const body = {
118 model,
119 prompt: input.prompt,
120 image_size: imageSize,
121 ...buildOptionalParameters(config, input, model),
122 ...requestBody
123 }
124
125 log.info(`[images:${LOG_TAG}] generation start`, {
126 configId: config.id,
127 configName: config.name,
128 model,
129 endpoint,
130 imageSize,
131 promptLength: input.prompt.length,
132 hasSeed: typeof input.seed === 'number',
133 requestBodyKeys: Object.keys(requestBody).sort()
134 })
135
136 try {
137 const response = await fetch(endpoint, {
138 method: 'POST',
139 signal: input.signal,
140 headers: {
141 authorization: `Bearer ${apiKey}`,
142 'content-type': 'application/json',
143 ...headers
144 },
145 body: JSON.stringify(body)
146 })
147 log.info(`[images:${LOG_TAG}] request end`, {
148 model,
149 status: response.status,
150 ok: response.ok,
151 elapsedMs: Date.now() - startedAt
152 })
153 const payload = await readJsonResponse(response)
154 const results = await collectSiliconFlowImages(payload, input.signal)
155 if (results.length === 0) throw new Error('硅基流动未返回图片')
156 log.info(`[images:${LOG_TAG}] generation completed`, {
157 model,
158 resultCount: results.length,
159 elapsedMs: Date.now() - startedAt
160 })
161 return results.slice(0, input.count)
162 } catch (error) {
163 log.error(`[images:${LOG_TAG}] generation failed`, {
164 model,
165 message: toErrorMessage(error),
166 elapsedMs: Date.now() - startedAt
167 })
168 throw error
169 }
170 }
171 }
172
172 lines TYPESCRIPT