johns-harness 2026.9.13 → 2026.9.14
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 +9 -6
- package/dist/auth-profiles-5CHn7vq1.js +12 -0
- package/dist/commands-registry-C78-tPS3.js +14 -0
- package/dist/commands-registry-D0Jkz-Sn.js +14 -0
- package/dist/commands-registry-DJWLO-6B.js +14 -0
- package/dist/commands-registry-l51VuG0m.js +14 -0
- package/dist/compact-B247y5Qt.js +20 -2
- package/dist/config-koj5M_EO.js +12 -0
- package/dist/daemon-cli.js +12 -0
- package/dist/image-generate.js +269 -0
- package/dist/model-selection-BGlGpPgM.js +12 -0
- package/dist/model-selection-BU6wl1le.js +12 -0
- package/dist/model-selection-L7RMwsG-.js +12 -0
- package/dist/pi-embedded-C6ITuRXf.js +20 -2
- package/dist/pi-embedded-DoQsYfIY.js +20 -2
- package/dist/pi-embedded-helpers-CkWXaNFn.js +9 -0
- package/dist/pi-embedded-helpers-DLLQ3MsR.js +9 -0
- package/dist/plugin-sdk/bluebubbles.js +12 -0
- package/dist/plugin-sdk/commands-registry-B4ZP4Y1B.js +14 -0
- package/dist/plugin-sdk/commands-registry-B8Og-fWU.js +14 -0
- package/dist/plugin-sdk/commands-registry-BoAXxFpH.js +14 -0
- package/dist/plugin-sdk/commands-registry-Ca1h6XvV.js +14 -0
- package/dist/plugin-sdk/commands-registry-Do-Y2EhN.js +14 -0
- package/dist/plugin-sdk/commands-registry-DpRs-E_M.js +14 -0
- package/dist/plugin-sdk/config-4Fgm-yEH.js +12 -0
- package/dist/plugin-sdk/config-Di-lyCdh.js +12 -0
- package/dist/plugin-sdk/discord.js +12 -0
- package/dist/plugin-sdk/dispatch-BP0viZiL.js +20 -2
- package/dist/plugin-sdk/dispatch-CQsjmw7g.js +20 -2
- package/dist/plugin-sdk/dispatch-Cerq29sy.js +20 -2
- package/dist/plugin-sdk/dispatch-Cndjtt0g.js +20 -2
- package/dist/plugin-sdk/dispatch-UogiJYul.js +20 -2
- package/dist/plugin-sdk/feishu.js +12 -0
- package/dist/plugin-sdk/googlechat.js +12 -0
- package/dist/plugin-sdk/imessage.js +12 -0
- package/dist/plugin-sdk/mattermost.js +14 -0
- package/dist/plugin-sdk/model-auth-CX9cPHdC.js +12 -0
- package/dist/plugin-sdk/model-auth-Ciehz0x5.js +12 -0
- package/dist/plugin-sdk/model-auth-DVyo6JSX.js +12 -0
- package/dist/plugin-sdk/model-auth-kiHYHGrs.js +12 -0
- package/dist/plugin-sdk/model-selection-Dbj4jDYM.js +12 -0
- package/dist/plugin-sdk/pi-embedded-helpers-B89Z8pGU.js +9 -0
- package/dist/plugin-sdk/pi-embedded-helpers-BDFUaaiY.js +9 -0
- package/dist/plugin-sdk/pi-embedded-helpers-BnFhV3or.js +9 -0
- package/dist/plugin-sdk/pi-embedded-helpers-CRUVFL1T.js +9 -0
- package/dist/plugin-sdk/pi-embedded-helpers-CY7PsNpl.js +9 -0
- package/dist/plugin-sdk/pi-embedded-helpers-Dira0ZRW.js +9 -0
- package/dist/plugin-sdk/reply-DbZnH8-h.js +20 -2
- package/dist/plugin-sdk/signal.js +12 -0
- package/dist/plugin-sdk/slack.js +12 -0
- package/dist/plugin-sdk/telegram.js +12 -0
- package/dist/plugin-sdk/tlon.js +12 -0
- package/dist/plugin-sdk/twitch.js +12 -0
- package/dist/plugin-sdk/zalo.js +12 -0
- package/dist/plugin-sdk/zalouser.js +12 -0
- package/dist/redact-snapshot-CjeFvdcl.js +5 -0
- package/dist/redact-snapshot-DuqfRMA9.js +5 -0
- package/dist/reply-C5LKjXcC.js +20 -2
- package/dist/runtime-path.js +58 -0
- package/dist/tool-catalog-CFg6jrp9.js +9 -0
- package/dist/tool-catalog-DE9Q8xiB.js +9 -0
- package/dist/update-cli-Cm_l3W29.js +1 -1
- package/dist/update-command.js +276 -0
- package/docs/PATCHES.md +14 -0
- package/package.json +1 -1
- package/skills/openai-image-gen/SKILL.md +3 -3
- package/skills/openai-image-gen/scripts/gen.py +1 -1
- package/vendor/patched-deps/@mariozechner/pi-ai/dist/models.generated.js +37 -0
- package/skills/nano-banana-pro/SKILL.md +0 -65
- package/skills/nano-banana-pro/scripts/generate_image.py +0 -235
- package/skills/nano-banana-pro/scripts/test_generate_image.py +0 -36
package/README.md
CHANGED
|
@@ -55,7 +55,10 @@ Expired ChatGPT credentials trigger an owner notice with the `/login` recovery c
|
|
|
55
55
|
|
|
56
56
|
<!-- JOHNNESS_PATCH_DEFAULTS_61 -->
|
|
57
57
|
### Batteries included
|
|
58
|
-
The harness ships the current model catalog and the short aliases `opus`, `sonnet`, `fable`, `astra`, `sol`, `luna`, `terra`, and `gpt`, so `/model opus` works on a fresh install with no `models.providers` block. Channels you configure load without a `plugins.allow` list; only an explicit `enabled: false` or `plugins.deny` turns one off. Updating the harness updates the catalog and aliases for every agent.
|
|
58
|
+
The harness ships the current model catalog and the short aliases `opus`, `sonnet`, `fable`, `astra`, `sol`, `luna`, `terra`, and `gpt`, so `/model opus` works on a fresh install with no `models.providers` block. Channels you configure load without a `plugins.allow` list; only an explicit `enabled: false` or `plugins.deny` turns one off. Updating the harness updates the catalog and aliases for every agent. Image generation and editing use GPT Image 2 through the `image_generate` tool: `openai-codex/gpt-image-2` on the ChatGPT subscription first, `openai/gpt-image-2` on the API key second (`agents.defaults.imageGenerationModel`).
|
|
59
|
+
|
|
60
|
+
### Updating from chat
|
|
61
|
+
Send `/update` to install the newest `johns-harness` on the machine the gateway runs on and restart into it; `/update 2026.9.14` pins an exact version. Owner only. The reply names the current and target versions, a second `/update` while one is running is refused, and the first message after the restart confirms the running version. On a writable host the package is reinstalled into the existing global prefix. On an immutable image with a persistent volume (Fly), it is installed into `$JOHNNESS_STATE_DIR/runtime/johnness` and the entrypoint prefers that runtime when it is newer than the image: `exec node "$(node /opt/johnness/lib/node_modules/johns-harness/dist/runtime-path.js)" gateway run`. `JOHNNESS_UPDATE_PREFIX` overrides the target. On failure the reply carries the last lines of npm output and the one command that fixes it.
|
|
59
62
|
|
|
60
63
|
### From source
|
|
61
64
|
|
|
@@ -68,8 +71,8 @@ node johnness.mjs --help
|
|
|
68
71
|
### Verify
|
|
69
72
|
|
|
70
73
|
```bash
|
|
71
|
-
npm test #
|
|
72
|
-
bash scripts/verify-patches.sh #
|
|
74
|
+
npm test # 767 tests against the installed dependency tree
|
|
75
|
+
bash scripts/verify-patches.sh # 360 checks across every patch family
|
|
73
76
|
```
|
|
74
77
|
|
|
75
78
|
## Launch a swarm with one command
|
|
@@ -115,7 +118,7 @@ Coding agents today run as isolated processes. Two agents on the same repository
|
|
|
115
118
|
- **Memory built for production.** Tiered recall that traces from consolidated reflections down to the verbatim record, surviving compaction and shared across the swarm. Installed and enabled by default.
|
|
116
119
|
- **Production authority per commit.** Promotion is granted to an exact SHA, never to a branch name, so one worker's authorization never covers another worker's commits.
|
|
117
120
|
- **An append-only action log.** Agents record every consequential action to `ACTION_LOG.md` and read it before acting.
|
|
118
|
-
- **A fixed runtime surface.** The core does not grow on a schedule. New mechanisms enter only to solve a demonstrated production failure, one narrow, tested, reversible patch at a time:
|
|
121
|
+
- **A fixed runtime surface.** The core does not grow on a schedule. New mechanisms enter only to solve a demonstrated production failure, one narrow, tested, reversible patch at a time: 62 patch families, each with a regression test and a rollback path.
|
|
119
122
|
- **Reliability with a definition.** Bounded retries, explicit failure states, exact-once continuation, context-pressure recovery, durable delivery, and supervised gateway recovery.
|
|
120
123
|
- **Coding first, phone optional.** Direct the work at a high level from a chat channel while agents plan, implement, test, coordinate, and stage releases unattended.
|
|
121
124
|
- **Local voice.** Optional native Whisper transcription of inbound audio, entirely on your machine. Off by default. [Setup](docs/NATIVE-WHISPER-MUSE.md).
|
|
@@ -182,7 +185,7 @@ The log is a [`RULES.md`](RULES.md) workspace convention enforced through the ac
|
|
|
182
185
|
|
|
183
186
|
## Patch families
|
|
184
187
|
|
|
185
|
-
The runtime carries
|
|
188
|
+
The runtime carries 62 patch families. Each is a production fix applied at the source with a regression test, a durable patch marker, and a rollback path. A verifier runs 360 checks against the package tree and installed dependencies, and 767 tests run against the installed dependency tree.
|
|
186
189
|
|
|
187
190
|
Every family follows the same six steps: reproduce the failure, trace the exact runtime path, make the smallest source-level change that restores the invariant, add a regression test and a patch marker, run the verifier, and retain rollback artifacts. The full index: [`docs/PATCHES.md`](docs/PATCHES.md).
|
|
188
191
|
|
|
@@ -199,7 +202,7 @@ Every family follows the same six steps: reproduce the failure, trace the exact
|
|
|
199
202
|
## Documentation
|
|
200
203
|
|
|
201
204
|
- [`docs/index.md`](docs/index.md): runtime docs, from gateway and channels to plugins, nodes, and CLI reference.
|
|
202
|
-
- [`docs/PATCHES.md`](docs/PATCHES.md): the
|
|
205
|
+
- [`docs/PATCHES.md`](docs/PATCHES.md): the 62 patch families, one record each.
|
|
203
206
|
- [`docs/NATIVE-WHISPER-MUSE.md`](docs/NATIVE-WHISPER-MUSE.md): local Whisper transcription setup.
|
|
204
207
|
- [`RULES.md`](RULES.md): the default operating rules, including Rule Zero on production authorization.
|
|
205
208
|
|
|
@@ -6443,6 +6443,17 @@ function applyLegacyAudioTranscriptionModel(params) {
|
|
|
6443
6443
|
params.changes.push(params.alreadySetMessage);
|
|
6444
6444
|
}
|
|
6445
6445
|
const LEGACY_CONFIG_MIGRATIONS_PART_2 = [
|
|
6446
|
+
{
|
|
6447
|
+
id: "agent.image-generation-model-v1",
|
|
6448
|
+
describe: "Migrate agents.defaults.imageGenerationModel string to primary/fallbacks",
|
|
6449
|
+
apply: (raw, changes) => {
|
|
6450
|
+
const defaults = getRecord(getRecord(raw.agents)?.defaults);
|
|
6451
|
+
if (!defaults || typeof defaults.imageGenerationModel !== "string") return;
|
|
6452
|
+
const primary = defaults.imageGenerationModel.trim();
|
|
6453
|
+
defaults.imageGenerationModel = primary ? { primary, fallbacks: [] } : void 0;
|
|
6454
|
+
changes.push("Migrated agents.defaults.imageGenerationModel string to primary/fallbacks");
|
|
6455
|
+
}
|
|
6456
|
+
}, // JOHNNESS_PATCH_GPT_IMAGE_62
|
|
6446
6457
|
{
|
|
6447
6458
|
id: "agent.model-config-v2",
|
|
6448
6459
|
describe: "Migrate legacy agent.model/allowedModels/modelAliases/modelFallbacks/imageModelFallbacks to agent.models + model lists",
|
|
@@ -10876,6 +10887,7 @@ function isValidNonNegativeByteSizeString(value) {
|
|
|
10876
10887
|
const AgentDefaultsSchema = z.object({
|
|
10877
10888
|
model: AgentModelSchema.optional(),
|
|
10878
10889
|
imageModel: AgentModelSchema.optional(),
|
|
10890
|
+
imageGenerationModel: AgentModelSchema.optional(), // JOHNNESS_PATCH_GPT_IMAGE_62
|
|
10879
10891
|
pdfModel: AgentModelSchema.optional(),
|
|
10880
10892
|
pdfMaxBytesMb: z.number().positive().optional(),
|
|
10881
10893
|
pdfMaxPages: z.number().int().positive().optional(),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// JOHNNESS_PATCH_UPDATE_COMMAND_62
|
|
1
2
|
// JOHNNESS_PATCH_DEVICE_LOGIN_60
|
|
2
3
|
// JOHNNESS_PATCH_DEVICE_LOGIN_60B
|
|
3
4
|
import { l as escapeRegExp } from "./utils-DC4zYvW0.js";
|
|
@@ -181,6 +182,19 @@ function buildChatCommands() {
|
|
|
181
182
|
captureRemaining: true
|
|
182
183
|
}]
|
|
183
184
|
}),
|
|
185
|
+
defineChatCommand({
|
|
186
|
+
key: "update",
|
|
187
|
+
nativeName: "update",
|
|
188
|
+
description: "Install the newest johns-harness and restart (owner only).",
|
|
189
|
+
textAlias: "/update",
|
|
190
|
+
category: "management",
|
|
191
|
+
args: [{
|
|
192
|
+
name: "version",
|
|
193
|
+
description: "Exact version to install (default latest)",
|
|
194
|
+
type: "string",
|
|
195
|
+
required: false
|
|
196
|
+
}]
|
|
197
|
+
}),
|
|
184
198
|
defineChatCommand({
|
|
185
199
|
key: "login",
|
|
186
200
|
nativeName: "login",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// JOHNNESS_PATCH_UPDATE_COMMAND_62
|
|
1
2
|
// JOHNNESS_PATCH_DEVICE_LOGIN_60
|
|
2
3
|
// JOHNNESS_PATCH_DEVICE_LOGIN_60B
|
|
3
4
|
import { D as isPlainObject, u as escapeRegExp } from "./logger-Bj0Xl6pn.js";
|
|
@@ -218,6 +219,19 @@ function buildChatCommands() {
|
|
|
218
219
|
captureRemaining: true
|
|
219
220
|
}]
|
|
220
221
|
}),
|
|
222
|
+
defineChatCommand({
|
|
223
|
+
key: "update",
|
|
224
|
+
nativeName: "update",
|
|
225
|
+
description: "Install the newest johns-harness and restart (owner only).",
|
|
226
|
+
textAlias: "/update",
|
|
227
|
+
category: "management",
|
|
228
|
+
args: [{
|
|
229
|
+
name: "version",
|
|
230
|
+
description: "Exact version to install (default latest)",
|
|
231
|
+
type: "string",
|
|
232
|
+
required: false
|
|
233
|
+
}]
|
|
234
|
+
}),
|
|
221
235
|
defineChatCommand({
|
|
222
236
|
key: "login",
|
|
223
237
|
nativeName: "login",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// JOHNNESS_PATCH_UPDATE_COMMAND_62
|
|
1
2
|
// JOHNNESS_PATCH_DEVICE_LOGIN_60
|
|
2
3
|
// JOHNNESS_PATCH_DEVICE_LOGIN_60B
|
|
3
4
|
import { D as isPlainObject, u as escapeRegExp } from "./logger-BW8uLq6f.js";
|
|
@@ -218,6 +219,19 @@ function buildChatCommands() {
|
|
|
218
219
|
captureRemaining: true
|
|
219
220
|
}]
|
|
220
221
|
}),
|
|
222
|
+
defineChatCommand({
|
|
223
|
+
key: "update",
|
|
224
|
+
nativeName: "update",
|
|
225
|
+
description: "Install the newest johns-harness and restart (owner only).",
|
|
226
|
+
textAlias: "/update",
|
|
227
|
+
category: "management",
|
|
228
|
+
args: [{
|
|
229
|
+
name: "version",
|
|
230
|
+
description: "Exact version to install (default latest)",
|
|
231
|
+
type: "string",
|
|
232
|
+
required: false
|
|
233
|
+
}]
|
|
234
|
+
}),
|
|
221
235
|
defineChatCommand({
|
|
222
236
|
key: "login",
|
|
223
237
|
nativeName: "login",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// JOHNNESS_PATCH_UPDATE_COMMAND_62
|
|
1
2
|
// JOHNNESS_PATCH_DEVICE_LOGIN_60
|
|
2
3
|
// JOHNNESS_PATCH_DEVICE_LOGIN_60B
|
|
3
4
|
import { Bi as DEFAULT_PROVIDER, Or as resolveConfiguredModelRef, zi as DEFAULT_MODEL } from "./auth-profiles-5CHn7vq1.js";
|
|
@@ -181,6 +182,19 @@ function buildChatCommands() {
|
|
|
181
182
|
captureRemaining: true
|
|
182
183
|
}]
|
|
183
184
|
}),
|
|
185
|
+
defineChatCommand({
|
|
186
|
+
key: "update",
|
|
187
|
+
nativeName: "update",
|
|
188
|
+
description: "Install the newest johns-harness and restart (owner only).",
|
|
189
|
+
textAlias: "/update",
|
|
190
|
+
category: "management",
|
|
191
|
+
args: [{
|
|
192
|
+
name: "version",
|
|
193
|
+
description: "Exact version to install (default latest)",
|
|
194
|
+
type: "string",
|
|
195
|
+
required: false
|
|
196
|
+
}]
|
|
197
|
+
}),
|
|
184
198
|
defineChatCommand({
|
|
185
199
|
key: "login",
|
|
186
200
|
nativeName: "login",
|
package/dist/compact-B247y5Qt.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createImageGenerateTool } from "./image-generate.js";
|
|
2
|
+
import { handleUpdateCommand } from "./update-command.js";
|
|
1
3
|
// JOHNNESS_PATCH_DEVICE_LOGIN_60
|
|
2
4
|
// JOHNNESS_PATCH_DEVICE_LOGIN_60B
|
|
3
5
|
import { handleDeviceLoginCommand } from "./codex-oauth-device.js";
|
|
@@ -11283,6 +11285,7 @@ const TRUSTED_TOOL_RESULT_MEDIA = new Set([
|
|
|
11283
11285
|
"exec",
|
|
11284
11286
|
"gateway",
|
|
11285
11287
|
"image",
|
|
11288
|
+
"image_generate",
|
|
11286
11289
|
"memory_get",
|
|
11287
11290
|
"memory_search",
|
|
11288
11291
|
"message",
|
|
@@ -15583,7 +15586,8 @@ function buildAgentSystemPrompt(params) {
|
|
|
15583
15586
|
sessions_spawn: acpSpawnRuntimeEnabled ? "Spawn an isolated sub-agent or ACP coding session (runtime=\"acp\" requires `agentId` unless `acp.defaultAgent` is configured; ACP harness ids follow acp.allowedAgents, not agents_list)" : "Spawn an isolated sub-agent session",
|
|
15584
15587
|
subagents: "List, steer, or kill sub-agent runs for this requester session",
|
|
15585
15588
|
session_status: "Show a /status-equivalent status card (usage + time + Reasoning/Verbose/Elevated); use for model-use questions (📊 session_status); optional per-session model override",
|
|
15586
|
-
image: "Analyze an image with the configured image model"
|
|
15589
|
+
image: "Analyze an image with the configured image model",
|
|
15590
|
+
image_generate: "Generate or edit images with GPT Image 2 (subscription first, API key second)"
|
|
15587
15591
|
};
|
|
15588
15592
|
const toolOrder = [
|
|
15589
15593
|
"read",
|
|
@@ -15610,7 +15614,8 @@ function buildAgentSystemPrompt(params) {
|
|
|
15610
15614
|
"sessions_send",
|
|
15611
15615
|
"subagents",
|
|
15612
15616
|
"session_status",
|
|
15613
|
-
"image"
|
|
15617
|
+
"image",
|
|
15618
|
+
"image_generate"
|
|
15614
15619
|
];
|
|
15615
15620
|
const canonicalToolNames = (params.toolNames ?? []).map((tool) => tool.trim()).filter(Boolean);
|
|
15616
15621
|
const canonicalByNormalized = /* @__PURE__ */ new Map();
|
|
@@ -40979,6 +40984,8 @@ function resolveSessionEntryForHookSessionKey(sessionStore, sessionKey) {
|
|
|
40979
40984
|
async function handleCommands(params) {
|
|
40980
40985
|
const deviceLoginReply = await handleDeviceLoginCommand(params, shouldHandleTextCommands({ cfg: params.cfg, surface: params.command.surface, commandSource: params.ctx.CommandSource }), { routeReply, callGateway });
|
|
40981
40986
|
if (deviceLoginReply) return deviceLoginReply;
|
|
40987
|
+
const updateReply = await handleUpdateCommand(params, shouldHandleTextCommands({ cfg: params.cfg, surface: params.command.surface, commandSource: params.ctx.CommandSource }), { routeReply, callGateway, writeRestartSentinel, extractDeliveryInfo, restart: () => scheduleGatewaySigusr1Restart({ reason: "/update" }) }); // JOHNNESS_PATCH_UPDATE_COMMAND_62
|
|
40988
|
+
if (updateReply) return updateReply;
|
|
40982
40989
|
if (HANDLERS === null) HANDLERS = [
|
|
40983
40990
|
handlePluginCommand,
|
|
40984
40991
|
handleBashCommand,
|
|
@@ -55646,6 +55653,16 @@ function createJohnnessTools(options) {
|
|
|
55646
55653
|
fsPolicy: options?.fsPolicy,
|
|
55647
55654
|
modelHasVision: options?.modelHasVision
|
|
55648
55655
|
}) : null;
|
|
55656
|
+
const imageGenerateTool = options?.agentDir?.trim() ? createImageGenerateTool({
|
|
55657
|
+
config: options?.config,
|
|
55658
|
+
agentDir: options.agentDir,
|
|
55659
|
+
workspaceDir,
|
|
55660
|
+
sandbox: options?.sandboxRoot && options?.sandboxFsBridge ? { root: options.sandboxRoot, bridge: options.sandboxFsBridge } : void 0
|
|
55661
|
+
}, {
|
|
55662
|
+
resolveApiKeyForProvider,
|
|
55663
|
+
loadWebMedia,
|
|
55664
|
+
localRoots: resolveMediaToolLocalRoots(workspaceDir, { workspaceOnly: options?.fsPolicy?.workspaceOnly === true })
|
|
55665
|
+
}) : null; // JOHNNESS_PATCH_GPT_IMAGE_62
|
|
55649
55666
|
const pdfTool = options?.agentDir?.trim() ? createPdfTool({
|
|
55650
55667
|
config: options?.config,
|
|
55651
55668
|
agentDir: options.agentDir,
|
|
@@ -55765,6 +55782,7 @@ function createJohnnessTools(options) {
|
|
|
55765
55782
|
...webSearchTool ? [webSearchTool] : [],
|
|
55766
55783
|
...webFetchTool ? [webFetchTool] : [],
|
|
55767
55784
|
...imageTool ? [imageTool] : [],
|
|
55785
|
+
...imageGenerateTool ? [imageGenerateTool] : [],
|
|
55768
55786
|
...pdfTool ? [pdfTool] : []
|
|
55769
55787
|
];
|
|
55770
55788
|
const pluginTools = resolvePluginTools({
|
package/dist/config-koj5M_EO.js
CHANGED
|
@@ -3132,6 +3132,17 @@ function applyLegacyAudioTranscriptionModel(params) {
|
|
|
3132
3132
|
params.changes.push(params.alreadySetMessage);
|
|
3133
3133
|
}
|
|
3134
3134
|
const LEGACY_CONFIG_MIGRATIONS_PART_2 = [
|
|
3135
|
+
{
|
|
3136
|
+
id: "agent.image-generation-model-v1",
|
|
3137
|
+
describe: "Migrate agents.defaults.imageGenerationModel string to primary/fallbacks",
|
|
3138
|
+
apply: (raw, changes) => {
|
|
3139
|
+
const defaults = getRecord(getRecord(raw.agents)?.defaults);
|
|
3140
|
+
if (!defaults || typeof defaults.imageGenerationModel !== "string") return;
|
|
3141
|
+
const primary = defaults.imageGenerationModel.trim();
|
|
3142
|
+
defaults.imageGenerationModel = primary ? { primary, fallbacks: [] } : void 0;
|
|
3143
|
+
changes.push("Migrated agents.defaults.imageGenerationModel string to primary/fallbacks");
|
|
3144
|
+
}
|
|
3145
|
+
}, // JOHNNESS_PATCH_GPT_IMAGE_62
|
|
3135
3146
|
{
|
|
3136
3147
|
id: "agent.model-config-v2",
|
|
3137
3148
|
describe: "Migrate legacy agent.model/allowedModels/modelAliases/modelFallbacks/imageModelFallbacks to agent.models + model lists",
|
|
@@ -6762,6 +6773,7 @@ function isValidNonNegativeByteSizeString(value) {
|
|
|
6762
6773
|
const AgentDefaultsSchema = z.object({
|
|
6763
6774
|
model: AgentModelSchema.optional(),
|
|
6764
6775
|
imageModel: AgentModelSchema.optional(),
|
|
6776
|
+
imageGenerationModel: AgentModelSchema.optional(), // JOHNNESS_PATCH_GPT_IMAGE_62
|
|
6765
6777
|
pdfModel: AgentModelSchema.optional(),
|
|
6766
6778
|
pdfMaxBytesMb: z.number().positive().optional(),
|
|
6767
6779
|
pdfMaxPages: z.number().int().positive().optional(),
|
package/dist/daemon-cli.js
CHANGED
|
@@ -6112,6 +6112,17 @@ function applyLegacyAudioTranscriptionModel(params) {
|
|
|
6112
6112
|
params.changes.push(params.alreadySetMessage);
|
|
6113
6113
|
}
|
|
6114
6114
|
const LEGACY_CONFIG_MIGRATIONS_PART_2 = [
|
|
6115
|
+
{
|
|
6116
|
+
id: "agent.image-generation-model-v1",
|
|
6117
|
+
describe: "Migrate agents.defaults.imageGenerationModel string to primary/fallbacks",
|
|
6118
|
+
apply: (raw, changes) => {
|
|
6119
|
+
const defaults = getRecord(getRecord(raw.agents)?.defaults);
|
|
6120
|
+
if (!defaults || typeof defaults.imageGenerationModel !== "string") return;
|
|
6121
|
+
const primary = defaults.imageGenerationModel.trim();
|
|
6122
|
+
defaults.imageGenerationModel = primary ? { primary, fallbacks: [] } : void 0;
|
|
6123
|
+
changes.push("Migrated agents.defaults.imageGenerationModel string to primary/fallbacks");
|
|
6124
|
+
}
|
|
6125
|
+
}, // JOHNNESS_PATCH_GPT_IMAGE_62
|
|
6115
6126
|
{
|
|
6116
6127
|
id: "agent.model-config-v2",
|
|
6117
6128
|
describe: "Migrate legacy agent.model/allowedModels/modelAliases/modelFallbacks/imageModelFallbacks to agent.models + model lists",
|
|
@@ -9694,6 +9705,7 @@ function isValidNonNegativeByteSizeString(value) {
|
|
|
9694
9705
|
const AgentDefaultsSchema = z.object({
|
|
9695
9706
|
model: AgentModelSchema.optional(),
|
|
9696
9707
|
imageModel: AgentModelSchema.optional(),
|
|
9708
|
+
imageGenerationModel: AgentModelSchema.optional(), // JOHNNESS_PATCH_GPT_IMAGE_62
|
|
9697
9709
|
pdfModel: AgentModelSchema.optional(),
|
|
9698
9710
|
pdfMaxBytesMb: z.number().positive().optional(),
|
|
9699
9711
|
pdfMaxPages: z.number().int().positive().optional(),
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// JOHNNESS_PATCH_GPT_IMAGE_62
|
|
2
|
+
// First-class `image_generate` agent tool: OpenAI Images API (generations when no input
|
|
3
|
+
// image, edits when one is given) with the model from `agents.defaults.imageGenerationModel`.
|
|
4
|
+
// Default order: `openai-codex/gpt-image-2` (ChatGPT subscription, Family 58 OAuth token)
|
|
5
|
+
// then `openai/gpt-image-2` (OPENAI_API_KEY or the configured openai provider key).
|
|
6
|
+
// A provider that rejects the request as unsupported (404/405/501, 429 usage limit, or no
|
|
7
|
+
// credentials) yields to the next entry; the result names the provider that served it.
|
|
8
|
+
// Output is written as PNG under <workspace>/media/generated and the path is returned so the
|
|
9
|
+
// agent can send it with the `message` tool. Path safety mirrors createImageTool: inputs load
|
|
10
|
+
// through loadWebMedia with the same local roots, outputs never escape the workspace.
|
|
11
|
+
import fs from "node:fs/promises";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { Type } from "@sinclair/typebox";
|
|
14
|
+
|
|
15
|
+
export const DEFAULT_IMAGE_GENERATION_MODEL = Object.freeze({
|
|
16
|
+
primary: "openai-codex/gpt-image-2",
|
|
17
|
+
fallbacks: Object.freeze(["openai/gpt-image-2"]),
|
|
18
|
+
});
|
|
19
|
+
export const IMAGE_GENERATION_MODEL_ID = "gpt-image-2";
|
|
20
|
+
const OPENAI_BASE = "https://api.openai.com/v1";
|
|
21
|
+
const CODEX_BASE = "https://chatgpt.com/backend-api/codex";
|
|
22
|
+
const SIZES = new Set(["1024x1024", "1536x1024", "1024x1536", "auto"]);
|
|
23
|
+
const DEFAULT_MAX_INPUT_BYTES = 20 * 1024 * 1024;
|
|
24
|
+
const JWT_CLAIM_PATH = "https://api.openai.com/auth";
|
|
25
|
+
|
|
26
|
+
export function coerceImageGenerationModelConfig(cfg) {
|
|
27
|
+
const raw = cfg?.agents?.defaults?.imageGenerationModel;
|
|
28
|
+
if (typeof raw === "string" && raw.trim()) return { primary: raw.trim(), fallbacks: [] };
|
|
29
|
+
if (raw && typeof raw === "object") {
|
|
30
|
+
const primary = typeof raw.primary === "string" && raw.primary.trim() ? raw.primary.trim() : void 0;
|
|
31
|
+
const fallbacks = Array.isArray(raw.fallbacks) ? raw.fallbacks.filter((v) => typeof v === "string" && v.trim()).map((v) => v.trim()) : [];
|
|
32
|
+
if (primary || fallbacks.length > 0) return { primary: primary ?? fallbacks.shift(), fallbacks };
|
|
33
|
+
}
|
|
34
|
+
return { primary: DEFAULT_IMAGE_GENERATION_MODEL.primary, fallbacks: [...DEFAULT_IMAGE_GENERATION_MODEL.fallbacks] };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function listImageGenerationCandidates(cfg) {
|
|
38
|
+
const { primary, fallbacks } = coerceImageGenerationModelConfig(cfg);
|
|
39
|
+
const out = [];
|
|
40
|
+
for (const ref of [primary, ...fallbacks]) {
|
|
41
|
+
if (!ref || out.some((c) => c.ref === ref)) continue;
|
|
42
|
+
const slash = ref.indexOf("/");
|
|
43
|
+
if (slash <= 0) continue;
|
|
44
|
+
out.push({ ref, provider: ref.slice(0, slash), model: ref.slice(slash + 1) });
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function codexAccountIdFromToken(token) {
|
|
50
|
+
try {
|
|
51
|
+
const payload = JSON.parse(Buffer.from(String(token).split(".")[1], "base64url").toString("utf8"));
|
|
52
|
+
const id = payload?.[JWT_CLAIM_PATH]?.chatgpt_account_id;
|
|
53
|
+
if (typeof id === "string" && id) return id;
|
|
54
|
+
} catch {}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Endpoint + headers for one provider. Same OAuth transport shape as openai-codex-responses. */
|
|
59
|
+
export function buildProviderRequest({ provider, apiKey, edit, userAgent = "johnness" }) {
|
|
60
|
+
if (provider === "openai-codex") {
|
|
61
|
+
const accountId = codexAccountIdFromToken(apiKey);
|
|
62
|
+
if (!accountId) throw new ProviderUnsupported("codex token has no chatgpt_account_id");
|
|
63
|
+
return {
|
|
64
|
+
url: `${CODEX_BASE}/images/${edit ? "edits" : "generations"}`,
|
|
65
|
+
headers: { Authorization: `Bearer ${apiKey}`, "chatgpt-account-id": accountId, originator: "pi", "User-Agent": userAgent },
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return { url: `${OPENAI_BASE}/images/${edit ? "edits" : "generations"}`, headers: { Authorization: `Bearer ${apiKey}` } };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class ProviderUnsupported extends Error {
|
|
72
|
+
constructor(message, status) {
|
|
73
|
+
super(message);
|
|
74
|
+
this.name = "ProviderUnsupported";
|
|
75
|
+
this.status = status;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function isUnsupportedStatus(status, bodyText = "") {
|
|
80
|
+
if (status === 404 || status === 405 || status === 501) return true;
|
|
81
|
+
if (status === 429 && /usage_limit|usage limit/i.test(bodyText)) return true;
|
|
82
|
+
if (status === 401 || status === 403) return true;
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function redact(text) {
|
|
87
|
+
return String(text ?? "").replace(/[A-Za-z0-9_-]{40,}/g, "<redacted>").slice(0, 400);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function sanitizeName(input, fallback) {
|
|
91
|
+
const base = String(input ?? "").trim().replace(/\.[a-z0-9]+$/i, "").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
92
|
+
return base || fallback;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function resolveOutputPath({ workspaceDir, output, index = 0, count = 1 }) {
|
|
96
|
+
const root = path.resolve(workspaceDir);
|
|
97
|
+
const mediaDir = path.join(root, "media", "generated");
|
|
98
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
99
|
+
let target;
|
|
100
|
+
if (output && String(output).trim()) {
|
|
101
|
+
const raw = String(output).trim();
|
|
102
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(raw) && !/^[a-zA-Z]:[\\/]/.test(raw)) throw new Error(`output must be a workspace path, got ${raw}`);
|
|
103
|
+
const resolved = path.isAbsolute(raw) ? path.resolve(raw) : path.resolve(root, raw);
|
|
104
|
+
const rel = path.relative(root, resolved);
|
|
105
|
+
if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) throw new Error(`output must stay inside the workspace: ${raw}`);
|
|
106
|
+
target = count > 1 ? path.join(path.dirname(resolved), `${sanitizeName(path.basename(resolved), "image")}-${index + 1}.png`) : resolved.replace(/\.[a-z0-9]+$/i, "") + ".png";
|
|
107
|
+
} else {
|
|
108
|
+
target = path.join(mediaDir, `gpt-image-${stamp}${count > 1 ? `-${index + 1}` : ""}.png`);
|
|
109
|
+
}
|
|
110
|
+
return target;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function validateParams(args) {
|
|
114
|
+
const record = args && typeof args === "object" ? args : {};
|
|
115
|
+
const prompt = typeof record.prompt === "string" ? record.prompt.trim() : "";
|
|
116
|
+
if (!prompt) throw new Error("prompt required");
|
|
117
|
+
const inputs = [];
|
|
118
|
+
if (typeof record.image === "string" && record.image.trim()) inputs.push(record.image.trim());
|
|
119
|
+
if (Array.isArray(record.images)) for (const v of record.images) if (typeof v === "string" && v.trim()) inputs.push(v.trim());
|
|
120
|
+
if (inputs.length > 4) throw new Error("at most 4 input images");
|
|
121
|
+
const mask = typeof record.mask === "string" && record.mask.trim() ? record.mask.trim() : void 0;
|
|
122
|
+
if (mask && inputs.length === 0) throw new Error("mask requires an input image");
|
|
123
|
+
const size = typeof record.size === "string" && record.size.trim() ? record.size.trim() : "auto";
|
|
124
|
+
if (!SIZES.has(size)) throw new Error(`size must be one of ${[...SIZES].join(", ")}`);
|
|
125
|
+
const countRaw = typeof record.count === "number" ? Math.floor(record.count) : 1;
|
|
126
|
+
if (!Number.isFinite(countRaw) || countRaw < 1 || countRaw > 4) throw new Error("count must be 1..4");
|
|
127
|
+
const quality = typeof record.quality === "string" && ["low", "medium", "high", "auto"].includes(record.quality) ? record.quality : void 0;
|
|
128
|
+
return { prompt, inputs, mask, size, count: countRaw, quality, output: typeof record.output === "string" ? record.output : void 0, model: typeof record.model === "string" && record.model.trim() ? record.model.trim() : void 0 };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function callProvider({ candidate, apiKey, params, images, mask, fetchImpl, userAgent }) {
|
|
132
|
+
const edit = images.length > 0;
|
|
133
|
+
const { url, headers } = buildProviderRequest({ provider: candidate.provider, apiKey, edit, userAgent });
|
|
134
|
+
let body;
|
|
135
|
+
if (edit) {
|
|
136
|
+
body = new FormData();
|
|
137
|
+
body.set("model", candidate.model);
|
|
138
|
+
body.set("prompt", params.prompt);
|
|
139
|
+
body.set("n", String(params.count));
|
|
140
|
+
if (params.size !== "auto") body.set("size", params.size);
|
|
141
|
+
if (params.quality) body.set("quality", params.quality);
|
|
142
|
+
images.forEach((img, i) => body.append("image[]", new Blob([img.buffer], { type: img.mimeType }), `input-${i + 1}.${img.mimeType.includes("jpeg") ? "jpg" : "png"}`));
|
|
143
|
+
if (mask) body.set("mask", new Blob([mask.buffer], { type: mask.mimeType }), "mask.png");
|
|
144
|
+
} else {
|
|
145
|
+
headers["content-type"] = "application/json";
|
|
146
|
+
body = JSON.stringify({ model: candidate.model, prompt: params.prompt, n: params.count, ...(params.size !== "auto" ? { size: params.size } : {}), ...(params.quality ? { quality: params.quality } : {}) });
|
|
147
|
+
}
|
|
148
|
+
const res = await fetchImpl(url, { method: "POST", headers, body });
|
|
149
|
+
const text = await res.text();
|
|
150
|
+
if (!res.ok) {
|
|
151
|
+
if (isUnsupportedStatus(res.status, text)) throw new ProviderUnsupported(`${candidate.provider} ${res.status}: ${redact(text)}`, res.status);
|
|
152
|
+
throw new Error(`${candidate.provider} ${res.status}: ${redact(text)}`);
|
|
153
|
+
}
|
|
154
|
+
let json;
|
|
155
|
+
try {
|
|
156
|
+
json = JSON.parse(text);
|
|
157
|
+
} catch {
|
|
158
|
+
throw new Error(`${candidate.provider}: non-JSON response`);
|
|
159
|
+
}
|
|
160
|
+
const data = Array.isArray(json?.data) ? json.data : [];
|
|
161
|
+
const buffers = [];
|
|
162
|
+
for (const item of data) {
|
|
163
|
+
if (typeof item?.b64_json === "string") buffers.push(Buffer.from(item.b64_json, "base64"));
|
|
164
|
+
else if (typeof item?.url === "string") {
|
|
165
|
+
const r = await fetchImpl(item.url);
|
|
166
|
+
if (!r.ok) throw new Error(`${candidate.provider}: image download HTTP ${r.status}`);
|
|
167
|
+
buffers.push(Buffer.from(await r.arrayBuffer()));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (buffers.length === 0) throw new Error(`${candidate.provider}: response carried no image data`);
|
|
171
|
+
return { buffers, usage: json?.usage ?? null, mode: edit ? "edit" : "generate" };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Try each candidate in order. Returns { provider, model, buffers, usage, mode, attempts }.
|
|
176
|
+
* `resolveAuth({ provider })` -> { apiKey } or throws; a throw counts as unsupported for that provider.
|
|
177
|
+
*/
|
|
178
|
+
export async function generateWithFallback({ cfg, params, images, mask, resolveAuth, fetchImpl = fetch, userAgent }) {
|
|
179
|
+
const candidates = params.model ? [{ ref: params.model, provider: params.model.split("/")[0], model: params.model.split("/").slice(1).join("/") || IMAGE_GENERATION_MODEL_ID }] : listImageGenerationCandidates(cfg);
|
|
180
|
+
if (candidates.length === 0) throw new Error("No image generation model configured. Set agents.defaults.imageGenerationModel.primary.");
|
|
181
|
+
const attempts = [];
|
|
182
|
+
for (const candidate of candidates) {
|
|
183
|
+
let apiKey;
|
|
184
|
+
try {
|
|
185
|
+
apiKey = (await resolveAuth({ provider: candidate.provider }))?.apiKey;
|
|
186
|
+
if (!apiKey) throw new Error("no credentials");
|
|
187
|
+
} catch (error) {
|
|
188
|
+
attempts.push({ provider: candidate.provider, skipped: `auth: ${redact(error?.message ?? error)}` });
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
const result = await callProvider({ candidate, apiKey, params, images, mask, fetchImpl, userAgent });
|
|
193
|
+
return { provider: candidate.provider, model: candidate.model, ...result, attempts };
|
|
194
|
+
} catch (error) {
|
|
195
|
+
if (error instanceof ProviderUnsupported) {
|
|
196
|
+
attempts.push({ provider: candidate.provider, skipped: error.message });
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
throw new Error(`No image provider could serve the request. ${attempts.map((a) => `${a.provider}: ${a.skipped}`).join(" | ")}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Tool factory. `deps` are supplied by the bundle so this module owns no second auth path:
|
|
207
|
+
* resolveApiKeyForProvider({ provider, cfg, agentDir }) from auth-profiles (Family 58 owner refresh)
|
|
208
|
+
* loadWebMedia(pathOrUrl, { maxBytes, localRoots }) with the same roots createImageTool uses
|
|
209
|
+
*/
|
|
210
|
+
export function createImageGenerateTool(options, deps) {
|
|
211
|
+
const agentDir = options?.agentDir?.trim();
|
|
212
|
+
const workspaceDir = options?.workspaceDir?.trim();
|
|
213
|
+
if (!agentDir || !workspaceDir) return null;
|
|
214
|
+
if (options?.sandbox?.root?.trim()) return null;
|
|
215
|
+
const localRoots = deps.localRoots ?? [workspaceDir];
|
|
216
|
+
const fetchImpl = deps.fetch ?? fetch;
|
|
217
|
+
return {
|
|
218
|
+
label: "Image Generate",
|
|
219
|
+
name: "image_generate",
|
|
220
|
+
description: "Generate or edit images with GPT Image 2 (default openai-codex/gpt-image-2, then openai/gpt-image-2). Give a prompt; add image (or images, up to 4) to edit, and an optional mask PNG. Writes PNG files under the workspace media/generated folder and returns their paths; send them with the message tool.",
|
|
221
|
+
parameters: Type.Object({
|
|
222
|
+
prompt: Type.String({ description: "What to draw or how to change the input image." }),
|
|
223
|
+
image: Type.Optional(Type.String({ description: "Input image path to edit (workspace or allowed local root)." })),
|
|
224
|
+
images: Type.Optional(Type.Array(Type.String(), { description: "Multiple input images to edit together (max 4)." })),
|
|
225
|
+
mask: Type.Optional(Type.String({ description: "PNG mask path; transparent areas are edited." })),
|
|
226
|
+
size: Type.Optional(Type.String({ description: "1024x1024, 1536x1024, 1024x1536, or auto (default)." })),
|
|
227
|
+
count: Type.Optional(Type.Number({ description: "Number of images, 1..4 (default 1)." })),
|
|
228
|
+
quality: Type.Optional(Type.String({ description: "low, medium, high, or auto." })),
|
|
229
|
+
output: Type.Optional(Type.String({ description: "Output path inside the workspace (default media/generated/gpt-image-<timestamp>.png)." })),
|
|
230
|
+
model: Type.Optional(Type.String({ description: "Override provider/model, for example openai/gpt-image-2." })),
|
|
231
|
+
}),
|
|
232
|
+
execute: async (_toolCallId, args) => {
|
|
233
|
+
const params = validateParams(args);
|
|
234
|
+
const maxBytes = deps.maxInputBytes ?? DEFAULT_MAX_INPUT_BYTES;
|
|
235
|
+
const load = async (ref) => {
|
|
236
|
+
const raw = ref.startsWith("@") ? ref.slice(1).trim() : ref;
|
|
237
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(raw) && !/^[a-zA-Z]:[\\/]/.test(raw) && !/^file:/i.test(raw)) throw new Error(`input must be a local file path: ${ref}`);
|
|
238
|
+
const media = await deps.loadWebMedia(raw.startsWith("file://") ? raw.slice(7) : raw, { maxBytes, localRoots });
|
|
239
|
+
if (media.kind !== "image") throw new Error(`Unsupported media type: ${media.kind}`);
|
|
240
|
+
return { buffer: media.buffer, mimeType: ("contentType" in media && media.contentType) || ("mimeType" in media && media.mimeType) || "image/png" };
|
|
241
|
+
};
|
|
242
|
+
const images = [];
|
|
243
|
+
for (const ref of params.inputs) images.push(await load(ref));
|
|
244
|
+
const mask = params.mask ? await load(params.mask) : void 0;
|
|
245
|
+
const outputs = Array.from({ length: params.count }, (_, i) => resolveOutputPath({ workspaceDir, output: params.output, index: i, count: params.count }));
|
|
246
|
+
const result = await generateWithFallback({
|
|
247
|
+
cfg: options?.config,
|
|
248
|
+
params,
|
|
249
|
+
images,
|
|
250
|
+
mask,
|
|
251
|
+
fetchImpl,
|
|
252
|
+
userAgent: deps.userAgent,
|
|
253
|
+
resolveAuth: ({ provider }) => deps.resolveApiKeyForProvider({ provider, cfg: options?.config, agentDir }),
|
|
254
|
+
});
|
|
255
|
+
const written = [];
|
|
256
|
+
for (let i = 0; i < result.buffers.length && i < outputs.length; i++) {
|
|
257
|
+
await fs.mkdir(path.dirname(outputs[i]), { recursive: true });
|
|
258
|
+
await fs.writeFile(outputs[i], result.buffers[i]);
|
|
259
|
+
written.push(outputs[i]);
|
|
260
|
+
}
|
|
261
|
+
const served = `${result.provider}/${result.model}`;
|
|
262
|
+
const text = [`${result.mode === "edit" ? "Edited" : "Generated"} ${written.length} image${written.length === 1 ? "" : "s"} with ${served}.`, ...written.map((p) => `MEDIA:${p}`)].join("\n");
|
|
263
|
+
return {
|
|
264
|
+
content: [{ type: "text", text }],
|
|
265
|
+
details: { provider: result.provider, model: result.model, mode: result.mode, paths: written, usage: result.usage, attempts: result.attempts },
|
|
266
|
+
};
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
@@ -3027,6 +3027,17 @@ function applyLegacyAudioTranscriptionModel(params) {
|
|
|
3027
3027
|
params.changes.push(params.alreadySetMessage);
|
|
3028
3028
|
}
|
|
3029
3029
|
const LEGACY_CONFIG_MIGRATIONS_PART_2 = [
|
|
3030
|
+
{
|
|
3031
|
+
id: "agent.image-generation-model-v1",
|
|
3032
|
+
describe: "Migrate agents.defaults.imageGenerationModel string to primary/fallbacks",
|
|
3033
|
+
apply: (raw, changes) => {
|
|
3034
|
+
const defaults = getRecord(getRecord(raw.agents)?.defaults);
|
|
3035
|
+
if (!defaults || typeof defaults.imageGenerationModel !== "string") return;
|
|
3036
|
+
const primary = defaults.imageGenerationModel.trim();
|
|
3037
|
+
defaults.imageGenerationModel = primary ? { primary, fallbacks: [] } : void 0;
|
|
3038
|
+
changes.push("Migrated agents.defaults.imageGenerationModel string to primary/fallbacks");
|
|
3039
|
+
}
|
|
3040
|
+
}, // JOHNNESS_PATCH_GPT_IMAGE_62
|
|
3030
3041
|
{
|
|
3031
3042
|
id: "agent.model-config-v2",
|
|
3032
3043
|
describe: "Migrate legacy agent.model/allowedModels/modelAliases/modelFallbacks/imageModelFallbacks to agent.models + model lists",
|
|
@@ -9298,6 +9309,7 @@ function isValidNonNegativeByteSizeString(value) {
|
|
|
9298
9309
|
const AgentDefaultsSchema = z.object({
|
|
9299
9310
|
model: AgentModelSchema.optional(),
|
|
9300
9311
|
imageModel: AgentModelSchema.optional(),
|
|
9312
|
+
imageGenerationModel: AgentModelSchema.optional(), // JOHNNESS_PATCH_GPT_IMAGE_62
|
|
9301
9313
|
pdfModel: AgentModelSchema.optional(),
|
|
9302
9314
|
pdfMaxBytesMb: z.number().positive().optional(),
|
|
9303
9315
|
pdfMaxPages: z.number().int().positive().optional(),
|
|
@@ -3002,6 +3002,17 @@ function applyLegacyAudioTranscriptionModel(params) {
|
|
|
3002
3002
|
params.changes.push(params.alreadySetMessage);
|
|
3003
3003
|
}
|
|
3004
3004
|
const LEGACY_CONFIG_MIGRATIONS_PART_2 = [
|
|
3005
|
+
{
|
|
3006
|
+
id: "agent.image-generation-model-v1",
|
|
3007
|
+
describe: "Migrate agents.defaults.imageGenerationModel string to primary/fallbacks",
|
|
3008
|
+
apply: (raw, changes) => {
|
|
3009
|
+
const defaults = getRecord(getRecord(raw.agents)?.defaults);
|
|
3010
|
+
if (!defaults || typeof defaults.imageGenerationModel !== "string") return;
|
|
3011
|
+
const primary = defaults.imageGenerationModel.trim();
|
|
3012
|
+
defaults.imageGenerationModel = primary ? { primary, fallbacks: [] } : void 0;
|
|
3013
|
+
changes.push("Migrated agents.defaults.imageGenerationModel string to primary/fallbacks");
|
|
3014
|
+
}
|
|
3015
|
+
}, // JOHNNESS_PATCH_GPT_IMAGE_62
|
|
3005
3016
|
{
|
|
3006
3017
|
id: "agent.model-config-v2",
|
|
3007
3018
|
describe: "Migrate legacy agent.model/allowedModels/modelAliases/modelFallbacks/imageModelFallbacks to agent.models + model lists",
|
|
@@ -9187,6 +9198,7 @@ function isValidNonNegativeByteSizeString(value) {
|
|
|
9187
9198
|
const AgentDefaultsSchema = z.object({
|
|
9188
9199
|
model: AgentModelSchema.optional(),
|
|
9189
9200
|
imageModel: AgentModelSchema.optional(),
|
|
9201
|
+
imageGenerationModel: AgentModelSchema.optional(), // JOHNNESS_PATCH_GPT_IMAGE_62
|
|
9190
9202
|
pdfModel: AgentModelSchema.optional(),
|
|
9191
9203
|
pdfMaxBytesMb: z.number().positive().optional(),
|
|
9192
9204
|
pdfMaxPages: z.number().int().positive().optional(),
|
|
@@ -2914,6 +2914,17 @@ function applyLegacyAudioTranscriptionModel(params) {
|
|
|
2914
2914
|
params.changes.push(params.alreadySetMessage);
|
|
2915
2915
|
}
|
|
2916
2916
|
const LEGACY_CONFIG_MIGRATIONS_PART_2 = [
|
|
2917
|
+
{
|
|
2918
|
+
id: "agent.image-generation-model-v1",
|
|
2919
|
+
describe: "Migrate agents.defaults.imageGenerationModel string to primary/fallbacks",
|
|
2920
|
+
apply: (raw, changes) => {
|
|
2921
|
+
const defaults = getRecord(getRecord(raw.agents)?.defaults);
|
|
2922
|
+
if (!defaults || typeof defaults.imageGenerationModel !== "string") return;
|
|
2923
|
+
const primary = defaults.imageGenerationModel.trim();
|
|
2924
|
+
defaults.imageGenerationModel = primary ? { primary, fallbacks: [] } : void 0;
|
|
2925
|
+
changes.push("Migrated agents.defaults.imageGenerationModel string to primary/fallbacks");
|
|
2926
|
+
}
|
|
2927
|
+
}, // JOHNNESS_PATCH_GPT_IMAGE_62
|
|
2917
2928
|
{
|
|
2918
2929
|
id: "agent.model-config-v2",
|
|
2919
2930
|
describe: "Migrate legacy agent.model/allowedModels/modelAliases/modelFallbacks/imageModelFallbacks to agent.models + model lists",
|
|
@@ -7347,6 +7358,7 @@ function isValidNonNegativeByteSizeString(value) {
|
|
|
7347
7358
|
const AgentDefaultsSchema = z.object({
|
|
7348
7359
|
model: AgentModelSchema.optional(),
|
|
7349
7360
|
imageModel: AgentModelSchema.optional(),
|
|
7361
|
+
imageGenerationModel: AgentModelSchema.optional(), // JOHNNESS_PATCH_GPT_IMAGE_62
|
|
7350
7362
|
pdfModel: AgentModelSchema.optional(),
|
|
7351
7363
|
pdfMaxBytesMb: z.number().positive().optional(),
|
|
7352
7364
|
pdfMaxPages: z.number().int().positive().optional(),
|