@dickpy/dsh-imagegen 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -201
- package/README.md +203 -182
- package/cordis.patch.yml +8 -8
- package/docs/images/multi-model-comparison.png +0 -0
- package/lib/client.js +1103 -837
- package/lib/client.js.map +1 -1
- package/lib/index.js +265 -135
- package/package.json +70 -68
- package/src/agent-image-tools.ts +418 -418
- package/src/client/ImageGenPanel.tsx +1699 -1508
- package/src/client/SettingsCard.tsx +936 -957
- package/src/client/TemplateLibrary.tsx +336 -336
- package/src/client/api.ts +193 -193
- package/src/client/channels-form.ts +263 -263
- package/src/client/controller.ts +46 -46
- package/src/client/conversation-sync.ts +14 -0
- package/src/client/css-modules.d.ts +5 -5
- package/src/client/helpers.ts +33 -33
- package/src/client/image-toolview.module.css +73 -73
- package/src/client/image-toolview.tsx +169 -158
- package/src/client/index.ts +32 -22
- package/src/client/locales.ts +610 -594
- package/src/client/mount.tsx +185 -96
- package/src/client/panel.module.css +1713 -1445
- package/src/client/settings-card.module.css +1023 -1023
- package/src/client/settings-form.ts +336 -336
- package/src/client/settings-scope.ts +298 -298
- package/src/client/sidebar-entry.ts +148 -102
- package/src/client/templates.module.css +453 -453
- package/src/engine.ts +520 -478
- package/src/gallery-store.ts +286 -286
- package/src/generation-runtime.ts +79 -75
- package/src/history-store.ts +250 -244
- package/src/image-format.ts +11 -11
- package/src/image-models.ts +19 -19
- package/src/index.ts +318 -318
- package/src/model-catalog.ts +115 -98
- package/src/presets.ts +71 -63
- package/src/prompt-enhancer.ts +137 -79
- package/src/protocol.ts +338 -326
- package/src/routes.ts +916 -906
- package/src/task-queue.ts +113 -103
- package/src/templates/cases.json +10196 -10196
- package/src/templates-store.ts +278 -278
- package/src/updater.ts +117 -117
package/src/model-catalog.ts
CHANGED
|
@@ -1,98 +1,115 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Model catalog: protocol-family detection and capability annotation shared by
|
|
3
|
-
* the engine, the settings card, and the panel. Known id patterns map onto the
|
|
4
|
-
* OpenAI-compatible image protocol families this plugin shapes requests for;
|
|
5
|
-
* anything unrecognized falls back to the generic OpenAI protocol (best
|
|
6
|
-
* effort) and is flagged as unknown so the UI can warn without blocking.
|
|
7
|
-
*
|
|
8
|
-
* Framework-free (pure data + regex), safe for the client bundle to inline.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
export type ModelFamily = 'gpt-image' | 'dall-e' | 'grok' | 'nanobanana' | 'seedream' | 'unknown'
|
|
12
|
-
|
|
13
|
-
/** Capability/identity annotation for one model id. */
|
|
14
|
-
export interface ModelCatalogEntry {
|
|
15
|
-
/** Family the request is shaped for. */
|
|
16
|
-
family: ModelFamily
|
|
17
|
-
/** Short badge label (en). */
|
|
18
|
-
label: string
|
|
19
|
-
/** Short badge label (zh). */
|
|
20
|
-
labelZh: string
|
|
21
|
-
/** Whether the family is known (false = best-effort OpenAI protocol). */
|
|
22
|
-
known: boolean
|
|
23
|
-
/** Whether the family supports image-to-image edits. */
|
|
24
|
-
supportsEdit: boolean
|
|
25
|
-
/** Whether the family natively takes aspect-ratio dials. */
|
|
26
|
-
supportsAspectRatio: boolean
|
|
27
|
-
/** Quality tiers the family interprets natively. */
|
|
28
|
-
qualityTiers: string[]
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const ENTRIES: Record<Exclude<ModelFamily, 'unknown'>, Omit<ModelCatalogEntry, 'family'>> = {
|
|
32
|
-
'gpt-image': {
|
|
33
|
-
label: 'gpt-image',
|
|
34
|
-
labelZh: 'GPT 图像',
|
|
35
|
-
known: true,
|
|
36
|
-
supportsEdit: true,
|
|
37
|
-
supportsAspectRatio: false,
|
|
38
|
-
qualityTiers: ['1K', '2K', '4K'],
|
|
39
|
-
},
|
|
40
|
-
'dall-e': {
|
|
41
|
-
label: 'DALL·E',
|
|
42
|
-
labelZh: 'DALL·E',
|
|
43
|
-
known: true,
|
|
44
|
-
supportsEdit: true,
|
|
45
|
-
supportsAspectRatio: false,
|
|
46
|
-
qualityTiers: ['auto'],
|
|
47
|
-
},
|
|
48
|
-
grok: {
|
|
49
|
-
label: 'grok',
|
|
50
|
-
labelZh: 'Grok',
|
|
51
|
-
known: true,
|
|
52
|
-
supportsEdit: true,
|
|
53
|
-
supportsAspectRatio: true,
|
|
54
|
-
qualityTiers: ['1K', '2K'],
|
|
55
|
-
},
|
|
56
|
-
nanobanana: {
|
|
57
|
-
label: 'nanobanana',
|
|
58
|
-
labelZh: 'Nano Banana',
|
|
59
|
-
known: true,
|
|
60
|
-
supportsEdit: true,
|
|
61
|
-
supportsAspectRatio: true,
|
|
62
|
-
qualityTiers: ['1K', '2K', '4K'],
|
|
63
|
-
},
|
|
64
|
-
seedream: {
|
|
65
|
-
label: 'seedream',
|
|
66
|
-
labelZh: 'Seedream',
|
|
67
|
-
known: true,
|
|
68
|
-
supportsEdit: true,
|
|
69
|
-
supportsAspectRatio: true,
|
|
70
|
-
qualityTiers: ['1K', '2K'],
|
|
71
|
-
},
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Model catalog: protocol-family detection and capability annotation shared by
|
|
3
|
+
* the engine, the settings card, and the panel. Known id patterns map onto the
|
|
4
|
+
* OpenAI-compatible image protocol families this plugin shapes requests for;
|
|
5
|
+
* anything unrecognized falls back to the generic OpenAI protocol (best
|
|
6
|
+
* effort) and is flagged as unknown so the UI can warn without blocking.
|
|
7
|
+
*
|
|
8
|
+
* Framework-free (pure data + regex), safe for the client bundle to inline.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type ModelFamily = 'gpt-image' | 'dall-e' | 'grok' | 'nanobanana' | 'seedream' | 'zhipu' | 'unknown'
|
|
12
|
+
|
|
13
|
+
/** Capability/identity annotation for one model id. */
|
|
14
|
+
export interface ModelCatalogEntry {
|
|
15
|
+
/** Family the request is shaped for. */
|
|
16
|
+
family: ModelFamily
|
|
17
|
+
/** Short badge label (en). */
|
|
18
|
+
label: string
|
|
19
|
+
/** Short badge label (zh). */
|
|
20
|
+
labelZh: string
|
|
21
|
+
/** Whether the family is known (false = best-effort OpenAI protocol). */
|
|
22
|
+
known: boolean
|
|
23
|
+
/** Whether the family supports image-to-image edits. */
|
|
24
|
+
supportsEdit: boolean
|
|
25
|
+
/** Whether the family natively takes aspect-ratio dials. */
|
|
26
|
+
supportsAspectRatio: boolean
|
|
27
|
+
/** Quality tiers the family interprets natively. */
|
|
28
|
+
qualityTiers: string[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const ENTRIES: Record<Exclude<ModelFamily, 'unknown'>, Omit<ModelCatalogEntry, 'family'>> = {
|
|
32
|
+
'gpt-image': {
|
|
33
|
+
label: 'gpt-image',
|
|
34
|
+
labelZh: 'GPT 图像',
|
|
35
|
+
known: true,
|
|
36
|
+
supportsEdit: true,
|
|
37
|
+
supportsAspectRatio: false,
|
|
38
|
+
qualityTiers: ['1K', '2K', '4K'],
|
|
39
|
+
},
|
|
40
|
+
'dall-e': {
|
|
41
|
+
label: 'DALL·E',
|
|
42
|
+
labelZh: 'DALL·E',
|
|
43
|
+
known: true,
|
|
44
|
+
supportsEdit: true,
|
|
45
|
+
supportsAspectRatio: false,
|
|
46
|
+
qualityTiers: ['auto'],
|
|
47
|
+
},
|
|
48
|
+
grok: {
|
|
49
|
+
label: 'grok',
|
|
50
|
+
labelZh: 'Grok',
|
|
51
|
+
known: true,
|
|
52
|
+
supportsEdit: true,
|
|
53
|
+
supportsAspectRatio: true,
|
|
54
|
+
qualityTiers: ['1K', '2K'],
|
|
55
|
+
},
|
|
56
|
+
nanobanana: {
|
|
57
|
+
label: 'nanobanana',
|
|
58
|
+
labelZh: 'Nano Banana',
|
|
59
|
+
known: true,
|
|
60
|
+
supportsEdit: true,
|
|
61
|
+
supportsAspectRatio: true,
|
|
62
|
+
qualityTiers: ['1K', '2K', '4K'],
|
|
63
|
+
},
|
|
64
|
+
seedream: {
|
|
65
|
+
label: 'seedream',
|
|
66
|
+
labelZh: 'Seedream',
|
|
67
|
+
known: true,
|
|
68
|
+
supportsEdit: true,
|
|
69
|
+
supportsAspectRatio: true,
|
|
70
|
+
qualityTiers: ['1K', '2K'],
|
|
71
|
+
},
|
|
72
|
+
zhipu: {
|
|
73
|
+
label: 'GLM-Image',
|
|
74
|
+
labelZh: '智谱图像',
|
|
75
|
+
known: true,
|
|
76
|
+
supportsEdit: false,
|
|
77
|
+
supportsAspectRatio: false,
|
|
78
|
+
qualityTiers: ['HD'],
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Official Gemini image ids served by Nano Banana gateways. */
|
|
83
|
+
const NANOBANANA_GEMINI_IDS = new Set([
|
|
84
|
+
'gemini-3-pro-image',
|
|
85
|
+
'gemini-3-pro-image-preview',
|
|
86
|
+
'gemini-3.1-flash-image',
|
|
87
|
+
'gemini-3.1-flash-image-preview',
|
|
88
|
+
'gemini-3.1-flash-lite-image',
|
|
89
|
+
'gemini-2.5-flash-image',
|
|
90
|
+
])
|
|
91
|
+
|
|
92
|
+
/** Classify one upstream model id into its request-shaping family. */
|
|
93
|
+
export function describeModel(model: string): ModelCatalogEntry {
|
|
94
|
+
const id = model.trim()
|
|
95
|
+
if (/^gpt-image/i.test(id)) return { family: 'gpt-image', ...ENTRIES['gpt-image'] }
|
|
96
|
+
if (/^dall-e/i.test(id)) return { family: 'dall-e', ...ENTRIES['dall-e'] }
|
|
97
|
+
if (/^grok-imagine(?:-|$)/.test(id)) return { family: 'grok', ...ENTRIES.grok }
|
|
98
|
+
if (/^nanobanana/i.test(id) || NANOBANANA_GEMINI_IDS.has(id)) return { family: 'nanobanana', ...ENTRIES.nanobanana }
|
|
99
|
+
if (/^(?:doubao-)?seedream/i.test(id)) return { family: 'seedream', ...ENTRIES.seedream }
|
|
100
|
+
if (/^(?:glm-image|cogview(?:-|$))/i.test(id)) return { family: 'zhipu', ...ENTRIES.zhipu }
|
|
101
|
+
return { family: 'unknown', label: 'unknown', labelZh: '未知协议', known: false, supportsEdit: true, supportsAspectRatio: false, qualityTiers: [] }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Conservative fallback for providers whose /models response only has ids.
|
|
105
|
+
* Metadata-aware filtering lives in prompt-enhancer.ts; this catches common
|
|
106
|
+
* image model naming conventions without treating every unknown model as an
|
|
107
|
+
* image model. */
|
|
108
|
+
export function isLikelyImageModelId(model: string): boolean {
|
|
109
|
+
return /(?:^|[-_.])(?:image|img|diffusion|flux|cogview|imagen|seedream|nanobanana|grok-imagine|dall-e|stable-diffusion|sdxl|pixart|kolors|ideogram|midjourney|recraft|hunyuan|jimeng|wanx|hidream|playground)(?:$|[-_.])/i.test(model.trim())
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The family a model id routes its request through. */
|
|
113
|
+
export function modelFamily(model: string): ModelFamily {
|
|
114
|
+
return describeModel(model).family
|
|
115
|
+
}
|
package/src/presets.ts
CHANGED
|
@@ -1,63 +1,71 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Built-in provider catalog (presets). A preset is an official or well-known
|
|
3
|
-
* OpenAI-compatible endpoint with its known model list, so the user only fills
|
|
4
|
-
* in the API key. The list ships with the package and is served to the
|
|
5
|
-
* settings card through a host route, so it can later be refreshed online like
|
|
6
|
-
* the template library.
|
|
7
|
-
*
|
|
8
|
-
* Framework-free (pure data), safe for the host routes to serve directly.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { ModelMapping } from './protocol.ts'
|
|
12
|
-
|
|
13
|
-
/** One built-in provider the settings card can instantiate a channel from. */
|
|
14
|
-
export interface PresetProvider {
|
|
15
|
-
/** Stable preset id stored on channels created from it ('' = custom). */
|
|
16
|
-
id: string
|
|
17
|
-
/** Display name shown in the picker (also the channel's default name). */
|
|
18
|
-
name: string
|
|
19
|
-
/** Official base URL prefilled into the channel. */
|
|
20
|
-
apiUrl: string
|
|
21
|
-
/** One-line description shown in the picker. */
|
|
22
|
-
hint: string
|
|
23
|
-
/** Known model list prefilled into the channel's model catalog. */
|
|
24
|
-
models: ModelMapping[]
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export const IMAGE_PRESETS: PresetProvider[] = [
|
|
28
|
-
{
|
|
29
|
-
id: 'volc-ark-seedream',
|
|
30
|
-
name: '字节 · 火山方舟(Seedream)',
|
|
31
|
-
apiUrl: 'https://ark.cn-beijing.volces.com/api/v3',
|
|
32
|
-
hint: '字节跳动官方 Seedream 文生图/图生图入口',
|
|
33
|
-
models: [
|
|
34
|
-
{ alias: 'seedream-5.0-pro', id: 'seedream-5.0-pro' },
|
|
35
|
-
{ alias: 'seedream-5.0', id: 'seedream-5.0' },
|
|
36
|
-
{ alias: 'seedream-4.0', id: 'seedream-4.0' },
|
|
37
|
-
],
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
id: 'openai-official',
|
|
41
|
-
name: 'OpenAI 官方',
|
|
42
|
-
apiUrl: 'https://api.openai.com/v1',
|
|
43
|
-
hint: 'OpenAI
|
|
44
|
-
models: [
|
|
45
|
-
{ alias: 'gpt-image-2', id: 'gpt-image-2' },
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Built-in provider catalog (presets). A preset is an official or well-known
|
|
3
|
+
* OpenAI-compatible endpoint with its known model list, so the user only fills
|
|
4
|
+
* in the API key. The list ships with the package and is served to the
|
|
5
|
+
* settings card through a host route, so it can later be refreshed online like
|
|
6
|
+
* the template library.
|
|
7
|
+
*
|
|
8
|
+
* Framework-free (pure data), safe for the host routes to serve directly.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ModelMapping } from './protocol.ts'
|
|
12
|
+
|
|
13
|
+
/** One built-in provider the settings card can instantiate a channel from. */
|
|
14
|
+
export interface PresetProvider {
|
|
15
|
+
/** Stable preset id stored on channels created from it ('' = custom). */
|
|
16
|
+
id: string
|
|
17
|
+
/** Display name shown in the picker (also the channel's default name). */
|
|
18
|
+
name: string
|
|
19
|
+
/** Official base URL prefilled into the channel. */
|
|
20
|
+
apiUrl: string
|
|
21
|
+
/** One-line description shown in the picker. */
|
|
22
|
+
hint: string
|
|
23
|
+
/** Known model list prefilled into the channel's model catalog. */
|
|
24
|
+
models: ModelMapping[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const IMAGE_PRESETS: PresetProvider[] = [
|
|
28
|
+
{
|
|
29
|
+
id: 'volc-ark-seedream',
|
|
30
|
+
name: '字节 · 火山方舟(Seedream)',
|
|
31
|
+
apiUrl: 'https://ark.cn-beijing.volces.com/api/v3',
|
|
32
|
+
hint: '字节跳动官方 Seedream 文生图/图生图入口',
|
|
33
|
+
models: [
|
|
34
|
+
{ alias: 'seedream-5.0-pro', id: 'seedream-5.0-pro' },
|
|
35
|
+
{ alias: 'seedream-5.0', id: 'seedream-5.0' },
|
|
36
|
+
{ alias: 'seedream-4.0', id: 'seedream-4.0' },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'openai-official',
|
|
41
|
+
name: 'OpenAI 官方',
|
|
42
|
+
apiUrl: 'https://api.openai.com/v1',
|
|
43
|
+
hint: 'OpenAI 官方图像生成接口',
|
|
44
|
+
models: [
|
|
45
|
+
{ alias: 'gpt-image-2', id: 'gpt-image-2' },
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'zhipu-official',
|
|
50
|
+
name: '智谱 AI 官方',
|
|
51
|
+
apiUrl: 'https://open.bigmodel.cn/api/paas/v4',
|
|
52
|
+
hint: '智谱官方 GLM-Image 图像生成接口',
|
|
53
|
+
models: [
|
|
54
|
+
{ alias: 'glm-image', id: 'glm-image' },
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'xai-grok',
|
|
59
|
+
name: 'xAI(Grok)',
|
|
60
|
+
apiUrl: 'https://api.x.ai/v1',
|
|
61
|
+
hint: 'xAI 官方接口:Grok Imagine 系列',
|
|
62
|
+
models: [
|
|
63
|
+
{ alias: 'grok-imagine-image', id: 'grok-imagine-image' },
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
/** Look up one built-in provider by id. */
|
|
69
|
+
export function presetById(id: string): PresetProvider | undefined {
|
|
70
|
+
return IMAGE_PRESETS.find(preset => preset.id === id)
|
|
71
|
+
}
|
package/src/prompt-enhancer.ts
CHANGED
|
@@ -1,79 +1,137 @@
|
|
|
1
|
-
/** OpenAI-compatible chat helpers used by the optional prompt-enhancement UI. */
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
1
|
+
/** OpenAI-compatible chat helpers used by the optional prompt-enhancement UI. */
|
|
2
|
+
|
|
3
|
+
import { isLikelyImageModelId } from './model-catalog.ts'
|
|
4
|
+
|
|
5
|
+
export interface PromptModelConfig {
|
|
6
|
+
apiUrl: string
|
|
7
|
+
apiKey: string
|
|
8
|
+
model: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Credentials shared by OpenAI-compatible `/models` discovery. */
|
|
12
|
+
export interface ModelListConfig {
|
|
13
|
+
apiUrl: string
|
|
14
|
+
apiKey: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function endpoint(base: string, suffix: string): string {
|
|
18
|
+
return `${base.replace(/\/+$/, '')}${suffix}`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function headers(apiKey: string): HeadersInit {
|
|
22
|
+
return {
|
|
23
|
+
'content-type': 'application/json',
|
|
24
|
+
...apiKey.trim() === '' ? {} : { authorization: `Bearer ${apiKey.trim()}` },
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function responseJson(response: Response): Promise<Record<string, unknown>> {
|
|
29
|
+
const body: unknown = await response.json().catch(() => undefined)
|
|
30
|
+
if (!response.ok || body === undefined || body === null || typeof body !== 'object') {
|
|
31
|
+
const message = body !== null && typeof body === 'object' && typeof (body as { error?: { message?: unknown } }).error?.message === 'string'
|
|
32
|
+
? (body as { error: { message: string } }).error.message
|
|
33
|
+
: `HTTP ${response.status}`
|
|
34
|
+
throw new Error(message)
|
|
35
|
+
}
|
|
36
|
+
return body as Record<string, unknown>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type ModelRecord = Record<string, unknown> & { id: string }
|
|
40
|
+
|
|
41
|
+
async function listModelRecords(config: ModelListConfig): Promise<ModelRecord[]> {
|
|
42
|
+
if (config.apiUrl.trim() === '') throw new Error('API URL is required')
|
|
43
|
+
const response = await fetch(endpoint(config.apiUrl, '/models'), { headers: headers(config.apiKey) })
|
|
44
|
+
const body = await responseJson(response)
|
|
45
|
+
const data = Array.isArray(body.data) ? body.data : []
|
|
46
|
+
return data.flatMap(item => {
|
|
47
|
+
if (item === null || typeof item !== 'object' || typeof (item as { id?: unknown }).id !== 'string') return []
|
|
48
|
+
const id = (item as { id: string }).id.trim()
|
|
49
|
+
return id === '' ? [] : [{ ...(item as Record<string, unknown>), id }]
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function textOf(value: unknown): string[] {
|
|
54
|
+
if (typeof value === 'string') return [value]
|
|
55
|
+
if (!Array.isArray(value)) return []
|
|
56
|
+
return value.filter((item): item is string => typeof item === 'string')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function hasImageGenerationCapability(record: ModelRecord): boolean | undefined {
|
|
60
|
+
const capability = record.capabilities
|
|
61
|
+
if (capability !== null && typeof capability === 'object') {
|
|
62
|
+
const values = capability as Record<string, unknown>
|
|
63
|
+
for (const key of ['image_generation', 'imageGeneration', 'text_to_image', 'textToImage', 'image_gen']) {
|
|
64
|
+
if (typeof values[key] === 'boolean') return values[key]
|
|
65
|
+
}
|
|
66
|
+
const serialized = JSON.stringify(values).toLowerCase()
|
|
67
|
+
if (/image[ _-]?generation|text[ _-]?to[ _-]?image/.test(serialized)) return true
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const taskText = [
|
|
71
|
+
...textOf(record.task),
|
|
72
|
+
...textOf(record.task_type),
|
|
73
|
+
...textOf(record.taskType),
|
|
74
|
+
...textOf(record.type),
|
|
75
|
+
...textOf(record.model_type),
|
|
76
|
+
...textOf(record.modelType),
|
|
77
|
+
...textOf(record.tasks),
|
|
78
|
+
...textOf(record.description),
|
|
79
|
+
].join(' ').toLowerCase()
|
|
80
|
+
if (/image[ _-]?generation|text[ _-]?to[ _-]?image|image[ _-]?gen/.test(taskText)) return true
|
|
81
|
+
if (/^image(?:[ _-]?generation)?$/.test(taskText.trim())) return true
|
|
82
|
+
if (/embedding|rerank|moderation|transcri|speech|audio|video|chat[ _-]?completion/.test(taskText)) return false
|
|
83
|
+
|
|
84
|
+
for (const key of ['output_modalities', 'outputModalities', 'supported_output_modalities']) {
|
|
85
|
+
const modalities = textOf(record[key]).map(value => value.toLowerCase())
|
|
86
|
+
if (modalities.length > 0) return modalities.includes('image')
|
|
87
|
+
}
|
|
88
|
+
return undefined
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isImageModelRecord(record: ModelRecord): boolean {
|
|
92
|
+
return hasImageGenerationCapability(record) ?? isLikelyImageModelId(record.id)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** List candidates exposed by an OpenAI-compatible endpoint. */
|
|
96
|
+
export async function listOpenAIModels(config: ModelListConfig): Promise<string[]> {
|
|
97
|
+
return [...new Set((await listModelRecords(config)).map(record => record.id))]
|
|
98
|
+
.sort((a, b) => a.localeCompare(b))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** List only models that advertise or conventionally represent image generation. */
|
|
102
|
+
export async function listImageModels(config: ModelListConfig): Promise<string[]> {
|
|
103
|
+
return [...new Set((await listModelRecords(config)).filter(isImageModelRecord).map(record => record.id))]
|
|
104
|
+
.sort((a, b) => a.localeCompare(b))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** List chat models exposed by an OpenAI-compatible endpoint. */
|
|
108
|
+
export async function listPromptModels(config: PromptModelConfig): Promise<string[]> {
|
|
109
|
+
return listOpenAIModels(config)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Expand a concise image request into a production-ready image prompt. */
|
|
113
|
+
export async function enhancePrompt(config: PromptModelConfig, prompt: string): Promise<string> {
|
|
114
|
+
if (config.apiUrl.trim() === '' || config.model.trim() === '') throw new Error('prompt enhancement model is not configured')
|
|
115
|
+
const response = await fetch(endpoint(config.apiUrl, '/chat/completions'), {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
headers: headers(config.apiKey),
|
|
118
|
+
body: JSON.stringify({
|
|
119
|
+
model: config.model.trim(),
|
|
120
|
+
temperature: 0.7,
|
|
121
|
+
messages: [
|
|
122
|
+
{
|
|
123
|
+
role: 'system',
|
|
124
|
+
content: 'You are an expert image-prompt editor. Expand the user request into one vivid, specific image-generation prompt. Preserve intent and language. Add only useful visual detail: subject, composition, lighting, materials, color, camera/style and quality. Return only the finished prompt, with no preface or markdown.',
|
|
125
|
+
},
|
|
126
|
+
{ role: 'user', content: prompt },
|
|
127
|
+
],
|
|
128
|
+
}),
|
|
129
|
+
})
|
|
130
|
+
const body = await responseJson(response)
|
|
131
|
+
const choices = Array.isArray(body.choices) ? body.choices : []
|
|
132
|
+
const content = choices[0] !== null && typeof choices[0] === 'object'
|
|
133
|
+
? (choices[0] as { message?: { content?: unknown } }).message?.content
|
|
134
|
+
: undefined
|
|
135
|
+
if (typeof content !== 'string' || content.trim() === '') throw new Error('chat model returned an empty prompt')
|
|
136
|
+
return content.trim()
|
|
137
|
+
}
|