@oar-sh/oar 0.9.23 → 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.
Files changed (110) hide show
  1. package/.github/extensions/web-relay/skills/reasoning-stream.mjs +3 -1
  2. package/CHANGELOG.md +307 -0
  3. package/README.md +12 -11
  4. package/bin/oar.js +54 -4
  5. package/package.json +11 -7
  6. package/server/agent-sdk-loader.mjs +116 -0
  7. package/server/agent-sdks/claude/entry.mjs +4 -0
  8. package/server/agent-sdks/claude/package-lock.json +1438 -0
  9. package/server/agent-sdks/claude/package.json +13 -0
  10. package/server/agent-sdks/cursor/entry-sqlite.mjs +2 -0
  11. package/server/agent-sdks/cursor/entry.mjs +3 -0
  12. package/server/agent-sdks/cursor/package-lock.json +171 -0
  13. package/server/agent-sdks/cursor/package.json +11 -0
  14. package/server/agent-sdks/koffi/entry.cjs +2 -0
  15. package/server/agent-sdks/koffi/package-lock.json +368 -0
  16. package/server/agent-sdks/koffi/package.json +10 -0
  17. package/server/claude-worker/claude-preview-tool.mjs +5 -2
  18. package/server/claude-worker/claude-remote-relay-tool.mjs +20 -2
  19. package/server/claude-worker/claude-sdk-adapter.mjs +4 -1
  20. package/server/claude-worker/sdk-message-normalizer.mjs +29 -1
  21. package/server/copilot-worker/copilot-sdk-session-process.mjs +13 -8
  22. package/server/cursor-worker/cursor-sdk-adapter.mjs +5 -3
  23. package/server/cursor-worker/sdk-message-normalizer.mjs +13 -0
  24. package/server/db-schema.mjs +19 -0
  25. package/server/grok-worker/sdk-message-normalizer.mjs +3 -0
  26. package/server/migrations/0039-media-index.mjs +167 -0
  27. package/server/migrations/0040-image-session-defaults.mjs +93 -0
  28. package/server/public/app/agent-sdk-ui.js +349 -0
  29. package/server/public/app/api-client.js +149 -8
  30. package/server/public/app/ask-user-view.js +2 -1
  31. package/server/public/app/attachments-view.js +66 -9
  32. package/server/public/app/bootstrap.js +289 -94
  33. package/server/public/app/chat-actions-menu-state.mjs +1 -0
  34. package/server/public/app/context-media-tab.js +143 -0
  35. package/server/public/app/conversation-view.js +90 -5
  36. package/server/public/app/image-composer-ui.js +518 -0
  37. package/server/public/app/image-turn-placeholder.mjs +233 -0
  38. package/server/public/app/journal-view.js +12 -2
  39. package/server/public/app/media-gallery-model.mjs +202 -0
  40. package/server/public/app/media-gallery-ui.js +524 -0
  41. package/server/public/app/media-view.js +79 -0
  42. package/server/public/app/model-refresh-ui.mjs +413 -0
  43. package/server/public/app/no-conversation-state.mjs +5 -2
  44. package/server/public/app/openai-image-composer.mjs +492 -0
  45. package/server/public/app/openai-image-cost-ui.js +426 -0
  46. package/server/public/app/openai-image-spend.mjs +443 -0
  47. package/server/public/app/plan-usage-view.mjs +38 -6
  48. package/server/public/app/provider-readiness.mjs +67 -4
  49. package/server/public/app/relay-error-ctas.mjs +5 -0
  50. package/server/public/app/session-history-ui.js +2 -0
  51. package/server/public/app/settings-modal.js +171 -0
  52. package/server/public/app/socket-handlers.js +47 -0
  53. package/server/public/app/tab-strip-fade.mjs +1 -1
  54. package/server/public/app/viewer-gallery.mjs +23 -1
  55. package/server/public/index.html +478 -9
  56. package/server/relay-tools.md +1 -0
  57. package/server/repositories/image-conversation-repository.mjs +69 -0
  58. package/server/repositories/media-repository.mjs +305 -0
  59. package/server/routes/agent-sdk-routes.mjs +73 -0
  60. package/server/routes/media-routes.mjs +361 -0
  61. package/server/routes/messages-routes.mjs +1467 -343
  62. package/server/routes/sessions-routes.mjs +596 -123
  63. package/server/server-runtime.mjs +414 -81
  64. package/server/services/about-service.mjs +26 -6
  65. package/server/services/agent-sdk-launch-gate.mjs +124 -0
  66. package/server/services/agent-sdk-paths.mjs +307 -0
  67. package/server/services/agent-sdk-store.mjs +1211 -0
  68. package/server/services/claude-api-settings-service.mjs +54 -17
  69. package/server/services/cli-install-service.mjs +3 -2
  70. package/server/services/copilot-model-discovery-service.mjs +7 -1
  71. package/server/services/database-file.mjs +23 -5
  72. package/server/services/deepseek-settings-service.mjs +58 -20
  73. package/server/services/earlier-database-service.mjs +4 -2
  74. package/server/services/generated-image-cleanup-service.mjs +2 -1
  75. package/server/services/generated-image-files.mjs +110 -0
  76. package/server/services/image-input-paths.mjs +389 -0
  77. package/server/services/image-operation-service.mjs +6 -2
  78. package/server/services/media-extract.mjs +653 -0
  79. package/server/services/media-index-service.mjs +221 -0
  80. package/server/services/model-refresh-scheduler.mjs +153 -0
  81. package/server/services/model-variant-catalog-service.mjs +141 -9
  82. package/server/services/oar-cli-helpers.mjs +72 -9
  83. package/server/services/oar-state-migration-service.mjs +2 -12
  84. package/server/services/oar-state-root.mjs +17 -0
  85. package/server/services/openai-image-preview-turns.mjs +330 -0
  86. package/server/services/openai-image-spend-service.mjs +516 -0
  87. package/server/services/openai-image-stream.mjs +170 -0
  88. package/server/services/plan-usage-openai-image.mjs +148 -0
  89. package/server/services/provider-model-refresh.mjs +326 -0
  90. package/server/services/provider-model-state.mjs +395 -0
  91. package/server/services/relay-singleton-guard.mjs +72 -10
  92. package/server/services/remote-relay-caller-context.mjs +6 -0
  93. package/server/services/remote-relay-dispatcher.mjs +386 -22
  94. package/server/services/remote-relay-registry-service.mjs +3 -1
  95. package/server/services/session-worker-launch-service.mjs +13 -0
  96. package/server/services/session-worker-supervisor-service.mjs +19 -1
  97. package/server/services/session-worker-websocket-service.mjs +15 -0
  98. package/server/services/token-usage-service.mjs +15 -0
  99. package/server/services/update-install-service.mjs +2 -2
  100. package/server/services/worker-launch-note-service.mjs +14 -2
  101. package/server/services/worker-recycle-service.mjs +145 -11
  102. package/shared/image-header.mjs +90 -0
  103. package/shared/model-refresh-schedule.mjs +120 -0
  104. package/shared/openai-image-options.mjs +299 -0
  105. package/shared/openai-image-presets.mjs +175 -0
  106. package/shared/openai-image-previews.mjs +40 -0
  107. package/shared/openai-image-prices.mjs +291 -0
  108. package/shared/openai-image-spend.mjs +90 -0
  109. package/shared/remote-relay-contract.mjs +126 -4
  110. package/shared/worker-runtime/direct-openai-image.mjs +98 -48
@@ -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, and on a failure
10
- // the failure note or the requeue. The caller only reports the outcome. Without
11
- // an operation the worker calls `POST /api/openai/images/generate` and
12
- // publishes the reply itself.
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 provider refused the request outright:
17
- // a retry would be refused the same way, so the turn fails instead of being
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 can take
43
- * minutes); `responseFields` is merged into the reply the worker publishes
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', outcome:
47
- * 'completed', imageCount, model }`. Throws an error with `code`
48
- * `OPENAI_IMAGE_TERMINAL_CODE` when the provider refused the direct request,
49
- * and a plain error for anything else (the caller requeues).
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 handled (published or
65
- // requeued) with 409 or 503 and the outcome in the body.
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
- if (!directModel) throw new Error('OpenAI image request is missing model');
82
- if (!prompt) throw new Error('OpenAI image request is missing prompt');
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,36 +119,60 @@ export async function runDirectOpenAIImageTurn({
93
119
  conversationId: message.conversationId,
94
120
  model: directModel,
95
121
  prompt,
96
- n: message?.n,
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) {
102
- if (isDirectOpenAIImageTerminalStatus(error?.status)) {
103
- const terminalError = new Error(
104
- `OpenAI image request failed: ${error?.detail || `HTTP ${error?.status || 'error'}`}`,
105
- );
106
- terminalError.code = OPENAI_IMAGE_TERMINAL_CODE;
107
- terminalError.status = error?.status;
108
- terminalError.detail = error?.detail || null;
109
- throw terminalError;
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.
128
+ // `relayProcessing`: the provider answered 2xx and the relay could not
129
+ // process the reply; the generation is billed.
130
+ const relayProcessing = error?.body?.relayProcessing === true;
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
+ });
110
140
  }
111
- throw error;
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
+ );
112
145
  }
113
146
  const generatedImages = Array.isArray(directResult?.generatedImages) ? directResult.generatedImages : [];
114
147
  if (!generatedImages.length) {
115
- throw new Error('OpenAI image API returned no images');
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
+ });
116
152
  }
117
153
  const model = String(directResult?.model || directModel).trim() || directModel;
118
- await api('POST', '/api/response', {
119
- messageId: message.id,
120
- conversationId: message.conversationId,
121
- text: '',
122
- generatedImages,
123
- model,
124
- modelOrigin: isAutoModel(message?.model) ? 'auto' : 'manual',
125
- ...responseFields,
126
- });
127
- return { via: 'direct', outcome: 'completed', imageCount: generatedImages.length, model };
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 };
128
178
  }