@oar-sh/oar 0.9.24 → 0.9.25
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/.github/extensions/web-relay/skills/reasoning-stream.mjs +3 -1
- package/CHANGELOG.md +142 -0
- package/README.md +12 -11
- package/package.json +3 -1
- package/server/claude-worker/claude-remote-relay-tool.mjs +15 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +29 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +11 -9
- package/server/cursor-worker/sdk-message-normalizer.mjs +13 -0
- package/server/db-schema.mjs +19 -0
- package/server/grok-worker/sdk-message-normalizer.mjs +3 -0
- package/server/migrations/0039-media-index.mjs +167 -0
- package/server/migrations/0040-image-session-defaults.mjs +93 -0
- package/server/public/app/api-client.js +58 -0
- package/server/public/app/ask-user-view.js +2 -1
- package/server/public/app/attachments-view.js +66 -9
- package/server/public/app/bootstrap.js +85 -25
- package/server/public/app/chat-actions-menu-state.mjs +1 -0
- package/server/public/app/context-media-tab.js +143 -0
- package/server/public/app/conversation-view.js +90 -5
- package/server/public/app/image-composer-ui.js +518 -0
- package/server/public/app/image-turn-placeholder.mjs +233 -0
- package/server/public/app/journal-view.js +12 -2
- package/server/public/app/media-gallery-model.mjs +202 -0
- package/server/public/app/media-gallery-ui.js +524 -0
- package/server/public/app/media-view.js +79 -0
- package/server/public/app/openai-image-composer.mjs +492 -0
- package/server/public/app/openai-image-cost-ui.js +426 -0
- package/server/public/app/openai-image-spend.mjs +443 -0
- package/server/public/app/plan-usage-view.mjs +38 -6
- package/server/public/app/relay-error-ctas.mjs +5 -0
- package/server/public/app/session-history-ui.js +2 -0
- package/server/public/app/settings-modal.js +4 -0
- package/server/public/app/socket-handlers.js +26 -0
- package/server/public/app/tab-strip-fade.mjs +1 -1
- package/server/public/app/viewer-gallery.mjs +23 -1
- package/server/public/index.html +383 -7
- package/server/relay-tools.md +1 -0
- package/server/repositories/image-conversation-repository.mjs +69 -0
- package/server/repositories/media-repository.mjs +305 -0
- package/server/routes/media-routes.mjs +361 -0
- package/server/routes/messages-routes.mjs +1410 -360
- package/server/routes/sessions-routes.mjs +361 -14
- package/server/server-runtime.mjs +19 -0
- package/server/services/database-file.mjs +6 -4
- package/server/services/generated-image-cleanup-service.mjs +2 -1
- package/server/services/generated-image-files.mjs +110 -0
- package/server/services/image-input-paths.mjs +389 -0
- package/server/services/image-operation-service.mjs +6 -2
- package/server/services/media-extract.mjs +653 -0
- package/server/services/media-index-service.mjs +221 -0
- package/server/services/openai-image-preview-turns.mjs +330 -0
- package/server/services/openai-image-spend-service.mjs +516 -0
- package/server/services/openai-image-stream.mjs +170 -0
- package/server/services/plan-usage-openai-image.mjs +148 -0
- package/server/services/remote-relay-caller-context.mjs +6 -0
- package/server/services/remote-relay-dispatcher.mjs +386 -22
- package/server/services/remote-relay-registry-service.mjs +3 -1
- package/server/services/token-usage-service.mjs +15 -0
- package/shared/image-header.mjs +90 -0
- package/shared/openai-image-options.mjs +299 -0
- package/shared/openai-image-presets.mjs +175 -0
- package/shared/openai-image-previews.mjs +40 -0
- package/shared/openai-image-prices.mjs +291 -0
- package/shared/openai-image-spend.mjs +90 -0
- package/shared/remote-relay-contract.mjs +126 -4
- package/shared/worker-runtime/direct-openai-image.mjs +96 -50
|
@@ -6,16 +6,18 @@
|
|
|
6
6
|
//
|
|
7
7
|
// With image continuity on (the default) the relay created an image operation
|
|
8
8
|
// when the message was sent, and `POST /api/image-operations/:id/execute` does
|
|
9
|
-
// everything: the provider call, the generated-image reply,
|
|
10
|
-
//
|
|
11
|
-
// an operation the worker calls `POST /api/openai/images/generate`
|
|
12
|
-
// publishes the reply
|
|
9
|
+
// everything: the provider call with its one retry, the generated-image reply,
|
|
10
|
+
// and on a failure the failure note. The caller only reports the outcome.
|
|
11
|
+
// Without an operation the worker calls `POST /api/openai/images/generate`
|
|
12
|
+
// (the same request, the same retry) and publishes the reply or the note
|
|
13
|
+
// itself. Neither path requeues a row whose request may have reached OpenAI:
|
|
14
|
+
// a second run is billed again.
|
|
13
15
|
|
|
14
16
|
export const DIRECT_IMAGE_OUTCOMES = Object.freeze(['completed', 'executing', 'terminal', 'uncertain', 'retryable']);
|
|
15
17
|
|
|
16
|
-
// The code of the error thrown when the
|
|
17
|
-
//
|
|
18
|
-
// requeued.
|
|
18
|
+
// The code of the error thrown when the direct request failed: the relay
|
|
19
|
+
// already retried what could pass, so the turn fails with the relay's note
|
|
20
|
+
// instead of being requeued. `imageCode` carries the relay's own code.
|
|
19
21
|
export const OPENAI_IMAGE_TERMINAL_CODE = 'RELAY_OPENAI_IMAGE_TERMINAL';
|
|
20
22
|
|
|
21
23
|
export function shouldUseDirectOpenAIImageApi(message = {}) {
|
|
@@ -25,28 +27,51 @@ export function shouldUseDirectOpenAIImageApi(message = {}) {
|
|
|
25
27
|
return model.startsWith('gpt-image-') || model.startsWith('dall-e-');
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
export function isDirectOpenAIImageTerminalStatus(status) {
|
|
29
|
-
const numeric = Number(status);
|
|
30
|
-
if (!Number.isFinite(numeric)) return false;
|
|
31
|
-
return [400, 401, 403, 404, 409, 422, 429, 503].includes(numeric);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
30
|
export function isOpenAIImageTerminalError(error) {
|
|
35
31
|
return String(error?.code || '').trim() === OPENAI_IMAGE_TERMINAL_CODE;
|
|
36
32
|
}
|
|
37
33
|
|
|
38
34
|
const isAutoModel = (value) => String(value || '').trim().toLowerCase() === 'auto';
|
|
39
35
|
|
|
36
|
+
function imageTerminalError(message, { imageCode = '', status, detail = null, relayProcessing = false } = {}) {
|
|
37
|
+
const error = new Error(message);
|
|
38
|
+
error.code = OPENAI_IMAGE_TERMINAL_CODE;
|
|
39
|
+
error.imageCode = String(imageCode || '').trim().toLowerCase().replace(/_/g, '-');
|
|
40
|
+
error.relayProcessing = relayProcessing;
|
|
41
|
+
error.status = status;
|
|
42
|
+
error.detail = detail;
|
|
43
|
+
return error;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The image options of a direct request: the message's `imageOptions`
|
|
48
|
+
* ({ size, quality, count, output_format, background }) first, then the
|
|
49
|
+
* composer's quality and size fields.
|
|
50
|
+
*/
|
|
51
|
+
function directImageOptions(message = {}) {
|
|
52
|
+
const options = message?.imageOptions && typeof message.imageOptions === 'object' ? message.imageOptions : {};
|
|
53
|
+
const pick = (...values) => values.find((value) => value !== undefined && value !== null && String(value).trim() !== '');
|
|
54
|
+
return {
|
|
55
|
+
n: pick(options.count, message?.n),
|
|
56
|
+
size: pick(options.size, message?.size, message?.contextTier),
|
|
57
|
+
quality: pick(options.quality, message?.quality, message?.reasoningEffort),
|
|
58
|
+
output_format: pick(options.output_format),
|
|
59
|
+
background: pick(options.background),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
40
63
|
/**
|
|
41
64
|
* Run one image turn. `requestOptions` goes to every `api` call that may wait
|
|
42
|
-
* on the provider (a worker passes `{ longCall: true }`: an edit
|
|
43
|
-
* minutes); `responseFields` is merged into the reply the
|
|
44
|
-
* itself (the attempt fence).
|
|
65
|
+
* on the provider (a worker passes `{ longCall: true }`: an edit with its
|
|
66
|
+
* retry can take minutes); `responseFields` is merged into the reply the
|
|
67
|
+
* worker publishes itself (the attempt fence).
|
|
45
68
|
*
|
|
46
|
-
* Resolves `{ via: 'operation', outcome }` or `{ via: 'direct',
|
|
47
|
-
* 'completed', imageCount, model }`.
|
|
48
|
-
*
|
|
49
|
-
* and
|
|
69
|
+
* Resolves `{ via: 'operation', outcome, usage }` or `{ via: 'direct',
|
|
70
|
+
* outcome: 'completed', imageCount, model, usage }`. On the direct path every
|
|
71
|
+
* failure, once the request may have gone out, throws an error with `code`
|
|
72
|
+
* `OPENAI_IMAGE_TERMINAL_CODE` and the relay's note as its message: the
|
|
73
|
+
* caller publishes it and never requeues. A plain error comes only from the
|
|
74
|
+
* operation path (the relay's execute route guards a second run itself).
|
|
50
75
|
*/
|
|
51
76
|
export async function runDirectOpenAIImageTurn({
|
|
52
77
|
api,
|
|
@@ -61,8 +86,8 @@ export async function runDirectOpenAIImageTurn({
|
|
|
61
86
|
{ messageId: message.id },
|
|
62
87
|
requestOptions,
|
|
63
88
|
).catch((error) => {
|
|
64
|
-
// The route answers a failure it already
|
|
65
|
-
//
|
|
89
|
+
// The route answers a failure it already published with 409 (503 on
|
|
90
|
+
// an older relay that requeued it) and the outcome in the body.
|
|
66
91
|
if (error?.status === 409 || error?.status === 503) {
|
|
67
92
|
return error?.body || error?.payload || { outcome: error?.status === 503 ? 'retryable' : 'terminal' };
|
|
68
93
|
}
|
|
@@ -72,14 +97,15 @@ export async function runDirectOpenAIImageTurn({
|
|
|
72
97
|
if (!DIRECT_IMAGE_OUTCOMES.includes(outcome)) {
|
|
73
98
|
throw new Error('Image operation returned an invalid outcome');
|
|
74
99
|
}
|
|
75
|
-
return { via: 'operation', outcome };
|
|
100
|
+
return { via: 'operation', outcome, usage: result?.usage || null };
|
|
76
101
|
}
|
|
77
102
|
|
|
78
103
|
const requestedModel = isAutoModel(message?.model) ? '' : String(message?.model || '').trim();
|
|
79
104
|
const directModel = String(message?.providerModel || requestedModel || message?.model || '').trim();
|
|
80
105
|
const prompt = String(message?.text || '').trim();
|
|
81
|
-
|
|
82
|
-
if (!
|
|
106
|
+
// Nothing was sent: a note, not a requeue that fails the same way.
|
|
107
|
+
if (!directModel) throw imageTerminalError('OpenAI image request is missing model', { imageCode: 'image-request-invalid' });
|
|
108
|
+
if (!prompt) throw imageTerminalError('OpenAI image request is missing prompt', { imageCode: 'image-request-invalid' });
|
|
83
109
|
await api('POST', '/api/activity', {
|
|
84
110
|
messageId: message.id,
|
|
85
111
|
conversationId: message.conversationId,
|
|
@@ -93,40 +119,60 @@ export async function runDirectOpenAIImageTurn({
|
|
|
93
119
|
conversationId: message.conversationId,
|
|
94
120
|
model: directModel,
|
|
95
121
|
prompt,
|
|
96
|
-
|
|
97
|
-
size: message?.size || message?.contextTier,
|
|
98
|
-
quality: message?.quality || message?.reasoningEffort,
|
|
122
|
+
...directImageOptions(message),
|
|
99
123
|
attachments: Array.isArray(message?.attachments) ? message.attachments : [],
|
|
100
124
|
}, requestOptions);
|
|
101
125
|
} catch (error) {
|
|
126
|
+
// The relay answered with its note (it retried what could pass), or the
|
|
127
|
+
// call to the relay itself broke off while OpenAI may have been working.
|
|
102
128
|
// `relayProcessing`: the provider answered 2xx and the relay could not
|
|
103
|
-
// process the reply; the generation is billed
|
|
129
|
+
// process the reply; the generation is billed.
|
|
104
130
|
const relayProcessing = error?.body?.relayProcessing === true;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
131
|
+
const note = String(error?.body?.error || '').trim();
|
|
132
|
+
if (error?.status) {
|
|
133
|
+
throw imageTerminalError(note || `OpenAI image request failed: ${error?.detail || `HTTP ${error.status}`}`, {
|
|
134
|
+
imageCode: error?.body?.code || (relayProcessing ? 'image-reply-unprocessed' : 'image-provider-rejected'),
|
|
135
|
+
status: error.status,
|
|
136
|
+
// The relay's detail (a network error's code), not its note again.
|
|
137
|
+
detail: error?.body?.detail || null,
|
|
138
|
+
relayProcessing,
|
|
139
|
+
});
|
|
114
140
|
}
|
|
115
|
-
throw
|
|
141
|
+
throw imageTerminalError(
|
|
142
|
+
`The relay did not finish the image request (${error?.message || 'connection error'}); the request may have been billed; retry the message.`,
|
|
143
|
+
{ imageCode: 'image-provider-no-answer', detail: error?.message || null },
|
|
144
|
+
);
|
|
116
145
|
}
|
|
117
146
|
const generatedImages = Array.isArray(directResult?.generatedImages) ? directResult.generatedImages : [];
|
|
118
147
|
if (!generatedImages.length) {
|
|
119
|
-
throw
|
|
148
|
+
throw imageTerminalError('OpenAI image API returned no images. The generation may have been billed; the message was not retried.', {
|
|
149
|
+
imageCode: 'image-reply-unprocessed',
|
|
150
|
+
relayProcessing: true,
|
|
151
|
+
});
|
|
120
152
|
}
|
|
121
153
|
const model = String(directResult?.model || directModel).trim() || directModel;
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
154
|
+
const usage = directResult?.usage || null;
|
|
155
|
+
// The relay's note when the first try may have been billed as well.
|
|
156
|
+
const note = String(directResult?.note || '').trim();
|
|
157
|
+
try {
|
|
158
|
+
await api('POST', '/api/response', {
|
|
159
|
+
messageId: message.id,
|
|
160
|
+
conversationId: message.conversationId,
|
|
161
|
+
text: '',
|
|
162
|
+
generatedImages,
|
|
163
|
+
model,
|
|
164
|
+
modelOrigin: isAutoModel(message?.model) ? 'auto' : 'manual',
|
|
165
|
+
// What OpenAI counted for this request, for the cost booking.
|
|
166
|
+
...(usage ? { imageUsage: usage } : {}),
|
|
167
|
+
...(note ? { imageNote: note } : {}),
|
|
168
|
+
...responseFields,
|
|
169
|
+
});
|
|
170
|
+
} catch (error) {
|
|
171
|
+
// The images are billed; a requeue would pay for them again.
|
|
172
|
+
throw imageTerminalError(
|
|
173
|
+
`The images OpenAI returned could not be saved (${error?.detail || error?.message || 'unknown error'}). The generation was billed; the message was not retried.`,
|
|
174
|
+
{ imageCode: 'image-finalization-uncertain', status: error?.status, detail: error?.detail || null, relayProcessing: true },
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
return { via: 'direct', outcome: 'completed', imageCount: generatedImages.length, model, usage };
|
|
132
178
|
}
|