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