imagegen-smarto 0.1.1 → 0.1.4
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/README.md +42 -13
- package/bin/cli.js +17 -4
- package/bin/generate.js +391 -0
- package/imagegen-smarto/SKILL.md +17 -37
- package/imagegen-smarto/agents/openai.yaml +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
# imagegen-smarto
|
|
2
2
|
|
|
3
|
-
Codex skill for
|
|
4
|
-
|
|
3
|
+
Codex skill for natural-language image generation and editing through a custom
|
|
4
|
+
SmartO OpenAI-compatible VPS relay.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
non-image requests remain unchanged.
|
|
6
|
+
It implicitly handles requests to create, draw, generate, modify, perform
|
|
7
|
+
image-to-image, or perform text-to-image generation. Requests to view,
|
|
8
|
+
analyze, describe, or recognize an image remain ordinary requests.
|
|
10
9
|
|
|
11
10
|
## Install with npm
|
|
12
11
|
|
|
@@ -19,7 +18,13 @@ npm install --global imagegen-smarto
|
|
|
19
18
|
The npm postinstall step copies the skill to `CODEX_HOME/skills/imagegen-smarto`
|
|
20
19
|
or, when `CODEX_HOME` is not set, `~/.codex/skills/imagegen-smarto`.
|
|
21
20
|
|
|
22
|
-
To
|
|
21
|
+
To update to the latest published package:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install --global imagegen-smarto
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
To recopy the skill from the already installed npm package:
|
|
23
28
|
|
|
24
29
|
```bash
|
|
25
30
|
imagegen-smarto install
|
|
@@ -76,10 +81,35 @@ python3 ~/.codex/skills/.system/skill-installer/scripts/install-skill-from-githu
|
|
|
76
81
|
--method git
|
|
77
82
|
```
|
|
78
83
|
|
|
79
|
-
After installation, use
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
84
|
+
After installation, use the profile that points requests to SmartO. The skill
|
|
85
|
+
is configured for implicit invocation, so an image request can be written in
|
|
86
|
+
natural language without first typing `$imagegen-smarto`.
|
|
87
|
+
|
|
88
|
+
## How image generation works
|
|
89
|
+
|
|
90
|
+
When the skill is triggered, Codex runs the installed `imagegen-smarto`
|
|
91
|
+
command. The command reads the active provider URL, model, and Codex
|
|
92
|
+
credential, sends a streaming Responses request with the SmartO marker, parses
|
|
93
|
+
the returned `image_generation_call`, and saves the PNG locally. It prints an
|
|
94
|
+
absolute `IMAGE_MARKDOWN=...` line so Codex can display the result inline.
|
|
95
|
+
|
|
96
|
+
You can also test the execution path directly:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
imagegen-smarto generate --prompt "一只戴红色围巾的小猫,儿童绘本风格"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
For editing, add one or more reference images:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
imagegen-smarto generate \
|
|
106
|
+
--prompt "把背景改成海边日落,保留主体" \
|
|
107
|
+
--image /absolute/path/to/source.png
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The command always uses `stream=true` internally because the relay requires
|
|
111
|
+
streaming for image generation. The skill does not need to describe or modify
|
|
112
|
+
the relay request body.
|
|
83
113
|
|
|
84
114
|
## Local profile switch
|
|
85
115
|
|
|
@@ -90,5 +120,4 @@ codex --profile smarto
|
|
|
90
120
|
codex --profile official
|
|
91
121
|
```
|
|
92
122
|
|
|
93
|
-
`smarto` selects the custom relay and enables this skill.
|
|
94
|
-
the built-in OpenAI provider and enables Codex's original `imagegen` skill.
|
|
123
|
+
`smarto` selects the custom relay and enables this skill.
|
package/bin/cli.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
const fs = require('node:fs')
|
|
6
6
|
const os = require('node:os')
|
|
7
7
|
const path = require('node:path')
|
|
8
|
+
const { generateImage } = require('./generate')
|
|
8
9
|
|
|
9
10
|
const packageRoot = path.resolve(__dirname, '..')
|
|
10
11
|
const bundledSkillPath = path.join(packageRoot, 'imagegen-smarto')
|
|
@@ -16,9 +17,12 @@ Usage:
|
|
|
16
17
|
imagegen-smarto install [--codex-home <path>]
|
|
17
18
|
imagegen-smarto uninstall [--codex-home <path>]
|
|
18
19
|
imagegen-smarto path [--codex-home <path>]
|
|
20
|
+
imagegen-smarto generate --prompt "your prompt" [options]
|
|
19
21
|
imagegen-smarto --help
|
|
20
22
|
|
|
21
|
-
The default Codex home is CODEX_HOME or ~/.codex
|
|
23
|
+
The default Codex home is CODEX_HOME or ~/.codex.
|
|
24
|
+
|
|
25
|
+
Run imagegen-smarto generate --help for image generation options.`)
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
function parseArgs(argv) {
|
|
@@ -80,8 +84,14 @@ function uninstallSkill(codexHome) {
|
|
|
80
84
|
return target
|
|
81
85
|
}
|
|
82
86
|
|
|
83
|
-
function main() {
|
|
84
|
-
const
|
|
87
|
+
async function main() {
|
|
88
|
+
const argv = process.argv.slice(2)
|
|
89
|
+
if (argv[0] === 'generate') {
|
|
90
|
+
await generateImage(argv.slice(1))
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const { command, codexHome: explicitCodexHome, quiet } = parseArgs(argv)
|
|
85
95
|
|
|
86
96
|
if (command === 'help') {
|
|
87
97
|
printHelp()
|
|
@@ -113,7 +123,10 @@ function main() {
|
|
|
113
123
|
}
|
|
114
124
|
|
|
115
125
|
try {
|
|
116
|
-
main()
|
|
126
|
+
main().catch((error) => {
|
|
127
|
+
console.error(`imagegen-smarto: ${error.message}`)
|
|
128
|
+
process.exitCode = 1
|
|
129
|
+
})
|
|
117
130
|
} catch (error) {
|
|
118
131
|
console.error(`imagegen-smarto: ${error.message}`)
|
|
119
132
|
process.exitCode = 1
|
package/bin/generate.js
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
'use strict'
|
|
4
|
+
|
|
5
|
+
const fs = require('node:fs')
|
|
6
|
+
const os = require('node:os')
|
|
7
|
+
const path = require('node:path')
|
|
8
|
+
|
|
9
|
+
const IMAGEGEN_MARKER = '__CODEX_VPS_IMAGEGEN__'
|
|
10
|
+
const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000
|
|
11
|
+
|
|
12
|
+
function printGenerateHelp() {
|
|
13
|
+
console.log(`imagegen-smarto generate - generate or edit an image through the active relay
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
imagegen-smarto generate --prompt "your prompt" [options]
|
|
17
|
+
imagegen-smarto generate "your prompt" [options]
|
|
18
|
+
|
|
19
|
+
Options:
|
|
20
|
+
--prompt <text> Prompt for generation or editing
|
|
21
|
+
--image <path> Reference image; may be repeated for image editing
|
|
22
|
+
--output <path> Output PNG path (default: a temporary file)
|
|
23
|
+
--model <model> Override the model from Codex config
|
|
24
|
+
--base-url <url> Override the active provider base URL
|
|
25
|
+
--timeout <seconds> Request timeout (default: 600)
|
|
26
|
+
--help Show this help`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseGenerateArgs(argv) {
|
|
30
|
+
const options = {
|
|
31
|
+
images: [],
|
|
32
|
+
prompt: null,
|
|
33
|
+
output: null,
|
|
34
|
+
model: null,
|
|
35
|
+
baseUrl: null,
|
|
36
|
+
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
37
|
+
}
|
|
38
|
+
const positional = []
|
|
39
|
+
|
|
40
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
41
|
+
const arg = argv[index]
|
|
42
|
+
if (arg === '--help' || arg === '-h') {
|
|
43
|
+
options.help = true
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (arg === '--prompt') {
|
|
48
|
+
options.prompt = argv[++index]
|
|
49
|
+
if (!options.prompt) throw new Error('--prompt requires text')
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (arg === '--image' || arg === '--input-image') {
|
|
54
|
+
const image = argv[++index]
|
|
55
|
+
if (!image) throw new Error(`${arg} requires a path`)
|
|
56
|
+
options.images.push(image)
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (arg === '--output') {
|
|
61
|
+
options.output = argv[++index]
|
|
62
|
+
if (!options.output) throw new Error('--output requires a path')
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (arg === '--model') {
|
|
67
|
+
options.model = argv[++index]
|
|
68
|
+
if (!options.model) throw new Error('--model requires a model name')
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (arg === '--base-url') {
|
|
73
|
+
options.baseUrl = argv[++index]
|
|
74
|
+
if (!options.baseUrl) throw new Error('--base-url requires a URL')
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (arg === '--timeout') {
|
|
79
|
+
const seconds = Number(argv[++index])
|
|
80
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
81
|
+
throw new Error('--timeout must be a positive number of seconds')
|
|
82
|
+
}
|
|
83
|
+
options.timeoutMs = seconds * 1000
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (arg.startsWith('-')) {
|
|
88
|
+
throw new Error(`unknown option: ${arg}`)
|
|
89
|
+
}
|
|
90
|
+
positional.push(arg)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!options.prompt && positional.length > 0) {
|
|
94
|
+
options.prompt = positional.join(' ')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return options
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function resolveCodexHome() {
|
|
101
|
+
const configuredPath = process.env.CODEX_HOME
|
|
102
|
+
if (configuredPath && configuredPath.trim()) {
|
|
103
|
+
return path.resolve(configuredPath)
|
|
104
|
+
}
|
|
105
|
+
return path.join(os.homedir(), '.codex')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function readCodexConfig(codexHome) {
|
|
109
|
+
const configPath = path.join(codexHome, 'config.toml')
|
|
110
|
+
try {
|
|
111
|
+
return fs.readFileSync(configPath, 'utf8')
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (error.code === 'ENOENT') return ''
|
|
114
|
+
throw new Error(`cannot read ${configPath}: ${error.message}`)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function tomlStringValue(text, key, section) {
|
|
119
|
+
let sectionText = text
|
|
120
|
+
|
|
121
|
+
if (section) {
|
|
122
|
+
const sectionHeader = `[model_providers.${section}]`
|
|
123
|
+
const sectionStart = text.indexOf(sectionHeader)
|
|
124
|
+
if (sectionStart >= 0) {
|
|
125
|
+
const contentStart = text.indexOf('\n', sectionStart)
|
|
126
|
+
const nextSection = text.slice(contentStart + 1).search(/^\s*\[/m)
|
|
127
|
+
sectionText = nextSection >= 0
|
|
128
|
+
? text.slice(contentStart + 1, contentStart + 1 + nextSection)
|
|
129
|
+
: text.slice(contentStart + 1)
|
|
130
|
+
} else {
|
|
131
|
+
sectionText = ''
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const match = sectionText.match(new RegExp(`^\\s*${key}\\s*=\\s*["']([^"']+)["']`, 'm'))
|
|
136
|
+
return match ? match[1] : null
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function readActiveProviderConfig(codexHome) {
|
|
140
|
+
const config = readCodexConfig(codexHome)
|
|
141
|
+
const provider = tomlStringValue(config, 'model_provider') || 'openai'
|
|
142
|
+
const baseUrl = process.env.IMAGEGEN_SMARTO_BASE_URL
|
|
143
|
+
|| process.env.OPENAI_BASE_URL
|
|
144
|
+
|| tomlStringValue(config, 'base_url', provider)
|
|
145
|
+
|| tomlStringValue(config, 'base_url')
|
|
146
|
+
const model = process.env.IMAGEGEN_SMARTO_MODEL
|
|
147
|
+
|| tomlStringValue(config, 'model')
|
|
148
|
+
|
|
149
|
+
if (!baseUrl) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`no active provider base_url found in ${path.join(codexHome, 'config.toml')}; `
|
|
152
|
+
+ 'set IMAGEGEN_SMARTO_BASE_URL or use a Codex profile with a base_url',
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
if (!model) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`no active model found in ${path.join(codexHome, 'config.toml')}; `
|
|
158
|
+
+ 'set IMAGEGEN_SMARTO_MODEL or pass --model',
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return { baseUrl, model }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function readApiKey(codexHome) {
|
|
166
|
+
if (process.env.IMAGEGEN_SMARTO_API_KEY) return process.env.IMAGEGEN_SMARTO_API_KEY
|
|
167
|
+
if (process.env.OPENAI_API_KEY) return process.env.OPENAI_API_KEY
|
|
168
|
+
|
|
169
|
+
const authPath = path.join(codexHome, 'auth.json')
|
|
170
|
+
try {
|
|
171
|
+
const auth = JSON.parse(fs.readFileSync(authPath, 'utf8'))
|
|
172
|
+
const candidates = [
|
|
173
|
+
auth.OPENAI_API_KEY,
|
|
174
|
+
auth.openai_api_key,
|
|
175
|
+
auth.api_key,
|
|
176
|
+
auth.access_token,
|
|
177
|
+
auth.tokens?.access_token,
|
|
178
|
+
]
|
|
179
|
+
const key = candidates.find((value) => typeof value === 'string' && value.trim())
|
|
180
|
+
if (key) return key
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (error.code !== 'ENOENT') {
|
|
183
|
+
throw new Error(`cannot read ${authPath}: ${error.message}`)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
throw new Error(
|
|
188
|
+
`no API credential found; set OPENAI_API_KEY or log in so ${authPath} contains OPENAI_API_KEY`,
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function normalizeResponsesUrl(baseUrl) {
|
|
193
|
+
const normalized = baseUrl.replace(/\/+$/, '')
|
|
194
|
+
return normalized.endsWith('/responses') ? normalized : `${normalized}/responses`
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function imageMimeType(filePath) {
|
|
198
|
+
const extension = path.extname(filePath).toLowerCase()
|
|
199
|
+
if (extension === '.jpg' || extension === '.jpeg') return 'image/jpeg'
|
|
200
|
+
if (extension === '.webp') return 'image/webp'
|
|
201
|
+
if (extension === '.gif') return 'image/gif'
|
|
202
|
+
return 'image/png'
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function imageDataUrl(filePath) {
|
|
206
|
+
const absolutePath = path.resolve(filePath)
|
|
207
|
+
if (!fs.existsSync(absolutePath)) {
|
|
208
|
+
throw new Error(`reference image does not exist: ${absolutePath}`)
|
|
209
|
+
}
|
|
210
|
+
const data = fs.readFileSync(absolutePath).toString('base64')
|
|
211
|
+
return `data:${imageMimeType(absolutePath)};base64,${data}`
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function decodeImageResult(result) {
|
|
215
|
+
if (typeof result !== 'string' || !result) {
|
|
216
|
+
throw new Error('the relay returned an empty image result')
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (result.startsWith('data:')) {
|
|
220
|
+
const comma = result.indexOf(',')
|
|
221
|
+
if (comma < 0) throw new Error('the relay returned an invalid data URL')
|
|
222
|
+
return Buffer.from(result.slice(comma + 1), 'base64')
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return Buffer.from(result, 'base64')
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function parseSseBlock(block) {
|
|
229
|
+
const data = block
|
|
230
|
+
.split(/\r?\n/)
|
|
231
|
+
.filter((line) => line.startsWith('data:'))
|
|
232
|
+
.map((line) => line.slice(5).trimStart())
|
|
233
|
+
.join('\n')
|
|
234
|
+
if (!data || data === '[DONE]') return null
|
|
235
|
+
try {
|
|
236
|
+
return JSON.parse(data)
|
|
237
|
+
} catch {
|
|
238
|
+
return null
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function consumeSseText(buffer, onEvent, flush = false) {
|
|
243
|
+
const blocks = buffer.split(/\r?\n\r?\n/)
|
|
244
|
+
const remainder = flush ? '' : blocks.pop()
|
|
245
|
+
for (const block of blocks) {
|
|
246
|
+
const event = parseSseBlock(block)
|
|
247
|
+
if (event) onEvent(event)
|
|
248
|
+
}
|
|
249
|
+
if (flush && remainder) {
|
|
250
|
+
const event = parseSseBlock(remainder)
|
|
251
|
+
if (event) onEvent(event)
|
|
252
|
+
}
|
|
253
|
+
return remainder || ''
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function extractError(event) {
|
|
257
|
+
if (!event || typeof event !== 'object') return null
|
|
258
|
+
if (event.error?.message) return event.error.message
|
|
259
|
+
if (event.response?.error?.message) return event.response.error.message
|
|
260
|
+
if (event.type?.endsWith('.failed')) {
|
|
261
|
+
return event.message || `relay event ${event.type} reported failure`
|
|
262
|
+
}
|
|
263
|
+
return null
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function outputPathForIndex(requestedPath, index) {
|
|
267
|
+
if (index === 0 && requestedPath) return path.resolve(requestedPath)
|
|
268
|
+
const base = requestedPath
|
|
269
|
+
? path.resolve(requestedPath)
|
|
270
|
+
: path.join(os.tmpdir(), `imagegen-smarto-${Date.now()}.png`)
|
|
271
|
+
if (index === 0) return base
|
|
272
|
+
const extension = path.extname(base) || '.png'
|
|
273
|
+
return `${base.slice(0, -extension.length)}-${index + 1}${extension}`
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function requestImage({ prompt, images, output, model, baseUrl, timeoutMs }) {
|
|
277
|
+
const codexHome = resolveCodexHome()
|
|
278
|
+
const active = readActiveProviderConfig(codexHome)
|
|
279
|
+
const apiKey = readApiKey(codexHome)
|
|
280
|
+
const content = [{ type: 'input_text', text: prompt }]
|
|
281
|
+
for (const image of images) {
|
|
282
|
+
content.push({ type: 'input_image', image_url: imageDataUrl(image) })
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const body = {
|
|
286
|
+
model: model || active.model,
|
|
287
|
+
instructions: IMAGEGEN_MARKER,
|
|
288
|
+
input: [{ role: 'user', content }],
|
|
289
|
+
stream: true,
|
|
290
|
+
}
|
|
291
|
+
const controller = new AbortController()
|
|
292
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs)
|
|
293
|
+
let response
|
|
294
|
+
try {
|
|
295
|
+
response = await fetch(normalizeResponsesUrl(baseUrl || active.baseUrl), {
|
|
296
|
+
method: 'POST',
|
|
297
|
+
headers: {
|
|
298
|
+
Authorization: `Bearer ${apiKey}`,
|
|
299
|
+
'Content-Type': 'application/json',
|
|
300
|
+
},
|
|
301
|
+
body: JSON.stringify(body),
|
|
302
|
+
signal: controller.signal,
|
|
303
|
+
})
|
|
304
|
+
} catch (error) {
|
|
305
|
+
clearTimeout(timeout)
|
|
306
|
+
if (error.name === 'AbortError') throw new Error(`image request timed out after ${timeoutMs / 1000}s`)
|
|
307
|
+
throw new Error(`cannot reach the image relay: ${error.message}`)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (!response.ok) {
|
|
311
|
+
clearTimeout(timeout)
|
|
312
|
+
const errorText = (await response.text()).slice(0, 2000)
|
|
313
|
+
throw new Error(`image relay returned HTTP ${response.status}: ${errorText}`)
|
|
314
|
+
}
|
|
315
|
+
if (!response.body) {
|
|
316
|
+
clearTimeout(timeout)
|
|
317
|
+
throw new Error('image relay returned no response body')
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const results = []
|
|
321
|
+
let relayError = null
|
|
322
|
+
let buffer = ''
|
|
323
|
+
const decoder = new TextDecoder()
|
|
324
|
+
const onEvent = (event) => {
|
|
325
|
+
relayError ||= extractError(event)
|
|
326
|
+
const item = event.type === 'response.output_item.done' ? event.item : null
|
|
327
|
+
if (item?.type === 'image_generation_call' && item.result) {
|
|
328
|
+
results.push(item.result)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
const reader = response.body.getReader()
|
|
334
|
+
while (true) {
|
|
335
|
+
const { value, done } = await reader.read()
|
|
336
|
+
if (done) break
|
|
337
|
+
buffer += decoder.decode(value, { stream: true })
|
|
338
|
+
buffer = consumeSseText(buffer, onEvent)
|
|
339
|
+
}
|
|
340
|
+
buffer += decoder.decode()
|
|
341
|
+
consumeSseText(buffer, onEvent, true)
|
|
342
|
+
} catch (error) {
|
|
343
|
+
if (error.name === 'AbortError') {
|
|
344
|
+
throw new Error(`image request timed out after ${timeoutMs / 1000}s`)
|
|
345
|
+
}
|
|
346
|
+
throw new Error(`failed while reading the image relay response: ${error.message}`)
|
|
347
|
+
} finally {
|
|
348
|
+
clearTimeout(timeout)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (relayError) throw new Error(relayError)
|
|
352
|
+
if (results.length === 0) {
|
|
353
|
+
throw new Error('the relay completed without an image result')
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const paths = []
|
|
357
|
+
for (let index = 0; index < results.length; index += 1) {
|
|
358
|
+
const filePath = outputPathForIndex(output, index)
|
|
359
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
360
|
+
fs.writeFileSync(filePath, decodeImageResult(results[index]), { mode: 0o600 })
|
|
361
|
+
paths.push(filePath)
|
|
362
|
+
}
|
|
363
|
+
return paths
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function generateImage(argv) {
|
|
367
|
+
const options = parseGenerateArgs(argv)
|
|
368
|
+
if (options.help) {
|
|
369
|
+
printGenerateHelp()
|
|
370
|
+
return
|
|
371
|
+
}
|
|
372
|
+
if (!options.prompt) {
|
|
373
|
+
printGenerateHelp()
|
|
374
|
+
throw new Error('a prompt is required')
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const paths = await requestImage({
|
|
378
|
+
prompt: options.prompt,
|
|
379
|
+
images: options.images,
|
|
380
|
+
output: options.output,
|
|
381
|
+
model: options.model,
|
|
382
|
+
baseUrl: options.baseUrl,
|
|
383
|
+
timeoutMs: options.timeoutMs,
|
|
384
|
+
})
|
|
385
|
+
for (const filePath of paths) {
|
|
386
|
+
console.log(`IMAGE_PATH=${filePath}`)
|
|
387
|
+
console.log(`IMAGE_MARKDOWN=`)
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
module.exports = { generateImage, printGenerateHelp }
|
package/imagegen-smarto/SKILL.md
CHANGED
|
@@ -1,47 +1,27 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: imagegen-smarto
|
|
3
|
-
description:
|
|
3
|
+
description: Use the active SmartO relay to actually create or edit images. Trigger immediately for any request to create, draw, generate, modify, transform, or redesign a picture, including text-to-image (文生图), image-to-image (图生图), and image generation inside another workflow. Do not trigger for viewing, analyzing, describing, or recognizing an existing image, or for ordinary non-image tasks.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# SmartO
|
|
6
|
+
# SmartO image generation
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
When this skill triggers, execute the installed `imagegen-smarto` command. The
|
|
9
|
+
command is the image-generation entry point: it sends the request to the active
|
|
10
|
+
Codex provider, adds the SmartO marker internally, keeps streaming enabled, and
|
|
11
|
+
saves the returned image locally. Do not stop after reading this file and do
|
|
12
|
+
not try to change the outer Codex request yourself.
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
For a text-to-image request, run:
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
- Keep the user's natural-language image prompt, reference images, and edit
|
|
18
|
-
constraints in the normal Responses request.
|
|
19
|
-
- Do not add a tool to ordinary non-image requests.
|
|
20
|
-
- Do not expose the routing marker in the user-visible answer.
|
|
21
|
-
|
|
22
|
-
The relay recognizes the marker below in the active skill instructions. Keep
|
|
23
|
-
this marker stable because it is the relay-side routing contract. Only image
|
|
24
|
-
requests that load this skill should cause the relay to add the Responses
|
|
25
|
-
built-in image tool:
|
|
26
|
-
|
|
27
|
-
`__CODEX_VPS_IMAGEGEN__`
|
|
28
|
-
|
|
29
|
-
The relay-side rule should apply only to `/v1/responses` requests whose
|
|
30
|
-
`instructions` contain that marker. It should add this tool when the request
|
|
31
|
-
does not already declare tools:
|
|
32
|
-
|
|
33
|
-
```json
|
|
34
|
-
{"type":"image_generation","action":"auto"}
|
|
16
|
+
```sh
|
|
17
|
+
imagegen-smarto generate --prompt "<the user's complete image prompt>"
|
|
35
18
|
```
|
|
36
19
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
in context.
|
|
41
|
-
|
|
42
|
-
The relay must forward `image_generation_call` output items unchanged. A
|
|
43
|
-
completed item contains a base64 image in `result`; do not turn it into a text
|
|
44
|
-
description or replace it with the built-in tool protocol.
|
|
20
|
+
Preserve the user's complete prompt and edit instructions. For image-to-image
|
|
21
|
+
requests, pass each available reference image as `--image <absolute-path>`.
|
|
22
|
+
The command can be repeated for multiple requested outputs.
|
|
45
23
|
|
|
46
|
-
|
|
47
|
-
|
|
24
|
+
After the command succeeds, use every printed `IMAGE_MARKDOWN=...` line in the
|
|
25
|
+
response so the generated file is displayed as an image. Do not replace the
|
|
26
|
+
image with a textual description. If the command fails, report its actual error
|
|
27
|
+
instead of claiming that an image was generated.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
interface:
|
|
2
2
|
display_name: "SmartO VPS Image Generation"
|
|
3
|
-
short_description: "Generate images through
|
|
4
|
-
default_prompt: "
|
|
3
|
+
short_description: "Generate or edit images through SmartO"
|
|
4
|
+
default_prompt: "Generate or edit the requested image through SmartO."
|
|
5
5
|
|
|
6
6
|
policy:
|
|
7
7
|
allow_implicit_invocation: true
|