@dickpy/dsh-imagegen 1.5.5 → 1.5.7

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.
@@ -1,137 +1,151 @@
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
- }
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
+ /** Remove reasoning-model artifacts from a chat model's visible content:
113
+ * complete `<think>…</think>` blocks first, then anything from a dangling
114
+ * unclosed `<think>` to the end of the text. Reasoning models served through
115
+ * OpenAI-compatible endpoints (MiniMax M3, DeepSeek R1, Qwen QVQ, …) inline
116
+ * these blocks in `message.content`; leaking them into the prompt box both
117
+ * pollutes the prompt and can push it past image models' length limits. */
118
+ function stripReasoning(text: string): string {
119
+ const withoutClosed = text.replace(/<think>[\s\S]*?<\/think>/gi, '')
120
+ const dangling = /<think>/i.exec(withoutClosed)
121
+ return (dangling === null ? withoutClosed : withoutClosed.slice(0, dangling.index)).trim()
122
+ }
123
+
124
+ /** Expand a concise image request into a production-ready image prompt. */
125
+ export async function enhancePrompt(config: PromptModelConfig, prompt: string): Promise<string> {
126
+ if (config.apiUrl.trim() === '' || config.model.trim() === '') throw new Error('prompt enhancement model is not configured')
127
+ const response = await fetch(endpoint(config.apiUrl, '/chat/completions'), {
128
+ method: 'POST',
129
+ headers: headers(config.apiKey),
130
+ body: JSON.stringify({
131
+ model: config.model.trim(),
132
+ temperature: 0.7,
133
+ messages: [
134
+ {
135
+ role: 'system',
136
+ 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.',
137
+ },
138
+ { role: 'user', content: prompt },
139
+ ],
140
+ }),
141
+ })
142
+ const body = await responseJson(response)
143
+ const choices = Array.isArray(body.choices) ? body.choices : []
144
+ const content = choices[0] !== null && typeof choices[0] === 'object'
145
+ ? (choices[0] as { message?: { content?: unknown } }).message?.content
146
+ : undefined
147
+ if (typeof content !== 'string' || content.trim() === '') throw new Error('chat model returned an empty prompt')
148
+ const enhanced = stripReasoning(content)
149
+ if (enhanced === '') throw new Error('chat model returned only reasoning content (empty <think> payload)')
150
+ return enhanced
151
+ }