@oar-sh/oar 0.9.16 → 0.9.18

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 (136) hide show
  1. package/.github/extensions/web-relay/polling/polling-loop.mjs +14 -87
  2. package/.github/extensions/web-relay/skills/tool-activity.mjs +5 -4
  3. package/CHANGELOG.md +412 -0
  4. package/README.md +15 -13
  5. package/package.json +2 -1
  6. package/server/claude-api-worker/claude-api-backend.mjs +345 -0
  7. package/server/claude-api-worker/claude-api-session-worker.mjs +29 -0
  8. package/server/claude-worker/claude-attachments.mjs +7 -4
  9. package/server/claude-worker/claude-cli-pinned-env.mjs +150 -0
  10. package/server/claude-worker/claude-remote-relay-tool.mjs +3 -3
  11. package/server/claude-worker/claude-sdk-adapter.mjs +199 -15
  12. package/server/claude-worker/claude-session-process.mjs +276 -33
  13. package/server/claude-worker/claude-session-worker-main.mjs +269 -0
  14. package/server/claude-worker/claude-session-worker.mjs +2 -253
  15. package/server/claude-worker/claude-subscription-key-check.mjs +64 -0
  16. package/server/claude-worker/claude-turn-failure.mjs +8 -0
  17. package/server/claude-worker/claude-turn-publisher.mjs +394 -13
  18. package/server/claude-worker/claude-usage-totals.mjs +161 -0
  19. package/server/claude-worker/sdk-message-normalizer.mjs +56 -1
  20. package/server/copilot-worker/copilot-remote-relay-tool.mjs +2 -2
  21. package/server/copilot-worker/copilot-sdk-adapter.mjs +18 -4
  22. package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +1 -1
  23. package/server/copilot-worker/copilot-sdk-session-process.mjs +80 -14
  24. package/server/cursor-worker/cursor-remote-relay-tool.mjs +3 -3
  25. package/server/cursor-worker/cursor-turn-runner.mjs +4 -4
  26. package/server/cursor-worker/sdk-message-normalizer.mjs +2 -2
  27. package/server/db-schema.mjs +96 -1
  28. package/server/deepseek-worker/deepseek-claude-backend.mjs +317 -0
  29. package/server/deepseek-worker/deepseek-session-worker.mjs +37 -0
  30. package/server/grok-worker/acp-client.mjs +1 -1
  31. package/server/grok-worker/grok-turn-runner.mjs +4 -4
  32. package/server/grok-worker/sdk-message-normalizer.mjs +1 -1
  33. package/server/mcp/oar-mcp-server.mjs +11 -5
  34. package/server/migrations/0024-deepseek-usage-totals.mjs +151 -0
  35. package/server/migrations/0025-deepseek-usage-requests-daily.mjs +179 -0
  36. package/server/migrations/0026-deepseek-usage-served-models.mjs +222 -0
  37. package/server/migrations/0027-deepseek-usage-checkpoint-runs.mjs +101 -0
  38. package/server/migrations/0028-deepseek-usage-checkpoint-request-runs.mjs +99 -0
  39. package/server/migrations/0029-deepseek-balance-rises.mjs +99 -0
  40. package/server/migrations/0030-deepseek-usage-actuals.mjs +123 -0
  41. package/server/migrations/0031-queue-provider.mjs +143 -0
  42. package/server/migrations/0032-runtime-session-billing-switch.mjs +94 -0
  43. package/server/migrations/0033-claude-api-usage-checkpoints.mjs +138 -0
  44. package/server/migrations/0034-claude-api-usage-totals.mjs +112 -0
  45. package/server/public/app/activity-replay-state.mjs +4 -3
  46. package/server/public/app/agent-sessions-settings-ui.js +1 -1
  47. package/server/public/app/agent-sessions-settings.mjs +1 -1
  48. package/server/public/app/api-client.js +105 -0
  49. package/server/public/app/bootstrap.js +243 -29
  50. package/server/public/app/claude-api-settings-ui.js +362 -0
  51. package/server/public/app/claude-billing-toggle-ui.js +224 -0
  52. package/server/public/app/claude-billing-toggle.mjs +112 -0
  53. package/server/public/app/context-tier-options.mjs +10 -2
  54. package/server/public/app/context-usage-view.mjs +1 -1
  55. package/server/public/app/conversation-provider-indicator.mjs +18 -0
  56. package/server/public/app/conversation-view.js +31 -3
  57. package/server/public/app/deepseek-settings-ui.js +342 -0
  58. package/server/public/app/inbox-ui.js +14 -3
  59. package/server/public/app/inbox-view.mjs +1 -1
  60. package/server/public/app/journal-view.js +156 -11
  61. package/server/public/app/model-selector-options.mjs +28 -1
  62. package/server/public/app/model-vision-support.mjs +2 -0
  63. package/server/public/app/new-conversation-model-choice.mjs +21 -0
  64. package/server/public/app/no-conversation-state.mjs +1 -1
  65. package/server/public/app/plan-usage-view.mjs +165 -13
  66. package/server/public/app/provider-readiness.mjs +8 -2
  67. package/server/public/app/relay-error-ctas.mjs +58 -0
  68. package/server/public/app/relay-schedules-ui.js +1 -1
  69. package/server/public/app/settings-modal.js +22 -0
  70. package/server/public/app/settings-tabs.js +1 -1
  71. package/server/public/app/socket-handlers.js +28 -1
  72. package/server/public/app/tool-activity-emoji.mjs +4 -3
  73. package/server/public/app/usage-limit-ui.js +136 -5
  74. package/server/public/index.html +596 -35
  75. package/server/relay-tools.md +4 -4
  76. package/server/repositories/conversation-group-repository.mjs +14 -7
  77. package/server/repositories/message-repository.mjs +7 -0
  78. package/server/repositories/session-repository.mjs +8 -1
  79. package/server/routes/ask-user-routes.mjs +1 -1
  80. package/server/routes/conversation-group-routes.mjs +3 -2
  81. package/server/routes/deepseek-usage-import-routes.mjs +72 -0
  82. package/server/routes/messages-routes.mjs +831 -36
  83. package/server/routes/relay-ask-routes.mjs +3 -3
  84. package/server/routes/relay-event-routes.mjs +2 -2
  85. package/server/routes/relay-schedule-routes.mjs +1 -1
  86. package/server/routes/remote-relay-routes.mjs +3 -3
  87. package/server/routes/sessions-routes.mjs +670 -63
  88. package/server/server-runtime.mjs +476 -26
  89. package/server/services/about-service.mjs +2 -0
  90. package/server/services/background-work-view.mjs +2 -1
  91. package/server/services/claude-api-settings-service.mjs +575 -0
  92. package/server/services/claude-api-usage-service.mjs +361 -0
  93. package/server/services/claude-billing-swap-service.mjs +274 -0
  94. package/server/services/claude-billing-switch-service.mjs +343 -0
  95. package/server/services/claude-session-leftovers.mjs +226 -0
  96. package/server/services/deepseek-balance-service.mjs +102 -0
  97. package/server/services/deepseek-settings-service.mjs +380 -0
  98. package/server/services/deepseek-usage-import.mjs +471 -0
  99. package/server/services/model-variant-catalog-service.mjs +1 -0
  100. package/server/services/plan-usage-claude-api.mjs +445 -0
  101. package/server/services/plan-usage-contract.mjs +100 -5
  102. package/server/services/plan-usage-deepseek.mjs +931 -0
  103. package/server/services/plan-usage-service.mjs +436 -0
  104. package/server/services/provider-model-selection.mjs +4 -2
  105. package/server/services/provider-reasoning-effort.mjs +24 -2
  106. package/server/services/relay-ask-service.mjs +1 -1
  107. package/server/services/relay-event-service.mjs +3 -2
  108. package/server/services/relay-schedule-service.mjs +1 -1
  109. package/server/services/remote-relay-caller-context.mjs +7 -0
  110. package/server/services/remote-relay-dispatcher.mjs +84 -14
  111. package/server/services/remote-relay-registry-service.mjs +1 -1
  112. package/server/services/session-worker-launch-service.mjs +294 -84
  113. package/server/services/session-worker-registry-service.mjs +28 -1
  114. package/server/services/session-worker-websocket-service.mjs +17 -0
  115. package/server/services/usage-limit-pause-service.mjs +55 -4
  116. package/server/services/worker-secret-redaction.mjs +320 -0
  117. package/shared/anthropic-api.mjs +202 -0
  118. package/shared/context-window-fallbacks.mjs +3 -0
  119. package/shared/deepseek-api.mjs +137 -0
  120. package/shared/deepseek-models.mjs +114 -0
  121. package/shared/deepseek-pricing.mjs +146 -0
  122. package/shared/host-claude-session-env.mjs +101 -0
  123. package/shared/model-id.mjs +7 -0
  124. package/shared/preview-tool-core.mjs +1 -1
  125. package/shared/provider-routing.mjs +5 -3
  126. package/shared/relay-asks.mjs +11 -5
  127. package/shared/relay-events.mjs +2 -2
  128. package/shared/relay-schedules.mjs +2 -2
  129. package/shared/remote-relay-contract.mjs +29 -9
  130. package/shared/remote-relay-tool-core.mjs +23 -6
  131. package/shared/steer-settle-failure.mjs +2 -0
  132. package/shared/worker-runtime/api-client.mjs +2 -2
  133. package/shared/worker-runtime/direct-openai-image.mjs +128 -0
  134. package/shared/worker-runtime/heartbeat.mjs +5 -0
  135. package/shared/worker-runtime/worker-log-file.mjs +60 -0
  136. package/shared/worker-runtime/worker-websocket-link.mjs +6 -0
@@ -14,6 +14,7 @@ import {
14
14
  REMOTE_RELAY_LOCAL_ALIAS,
15
15
  REMOTE_RELAY_LOCAL_ID,
16
16
  REMOTE_RELAY_PROVIDERS,
17
+ REMOTE_RELAY_SEND_PROVIDERS,
17
18
  REMOTE_RELAY_SESSION_SETTINGS,
18
19
  formatSessionGroupList,
19
20
  isRemoteRelayApprovalQuestion,
@@ -45,7 +46,7 @@ import {
45
46
  import { RELAY_ASK_KIND, relayQuestionKind } from '../../shared/relay-asks.mjs';
46
47
  import { RELAY_ASK_REMOTE_PATHS, isMissingRelayAskRoute, relayWithoutAsksText } from './relay-ask-link-service.mjs';
47
48
 
48
- // The one place a `remote_relay` tool call is decided and carried out (plan
49
+ // The one place a `relay_tool` tool call is decided and carried out (plan
49
50
  // §5.6). Every provider adapter ends up at POST /api/remote-relays/tool, whose
50
51
  // handler calls dispatch(). In order:
51
52
  //
@@ -114,7 +115,7 @@ const TITLE_MAX = 80;
114
115
 
115
116
  // The provider settings routes a relay serves; github (Copilot) has none with
116
117
  // models — its catalogue is /api/models plus /api/status defaultModel.
117
- const SETTINGS_PROVIDERS = Object.freeze(['claude', 'cursor', 'grok', 'openai', 'claude-cloud']);
118
+ const SETTINGS_PROVIDERS = Object.freeze(['claude', 'cursor', 'grok', 'openai', 'claude-cloud', 'deepseek', 'claude-api']);
118
119
  // A Claude Cloud session clones a GitHub repository instead of opening a
119
120
  // folder: create_session takes `repo` and `branch` for it.
120
121
  const CLOUD_PROVIDER = REMOTE_RELAY_CLOUD_PROVIDER;
@@ -140,6 +141,8 @@ const CATALOG_PROVIDER_LABELS = Object.freeze({
140
141
  cursor: 'cursor',
141
142
  grok: 'grok',
142
143
  'claude-cloud': 'claude-cloud',
144
+ deepseek: 'deepseek',
145
+ 'claude-api': 'claude-api',
143
146
  });
144
147
  // Worker statuses that mean "working on something right now" (scope active).
145
148
  const WORKING_WORKER_STATUSES = new Set(['starting', 'processing']);
@@ -1925,7 +1928,7 @@ export function createRemoteRelayDispatcher({
1925
1928
  * When the replay fails too, nobody knows; the error carries the session
1926
1929
  * and message id so the agent checks with wait instead of sending again.
1927
1930
  */
1928
- async function postPrompt(ctx, { session, text, origin, model, mode, effort }) {
1931
+ async function postPrompt(ctx, { session, text, origin, model, mode, effort, billing = '' }) {
1929
1932
  const messageId = String(randomUUID());
1930
1933
  const body = {
1931
1934
  conversationId: session,
@@ -1936,13 +1939,18 @@ export function createRemoteRelayDispatcher({
1936
1939
  ...(mode ? { relayMode: mode } : {}),
1937
1940
  ...(effort ? { reasoningEffort: effort } : {}),
1938
1941
  };
1942
+ // A Claude session moved to another billing with this message: one call,
1943
+ // which switches and enqueues together on the target, or does neither.
1944
+ const post = () => (billing
1945
+ ? postBillingSwitch(ctx, session, { to: billing, by: 'agent', message: body })
1946
+ : call(ctx, 'POST', '/api/message', { body }));
1939
1947
  let sent;
1940
1948
  try {
1941
- sent = await call(ctx, 'POST', '/api/message', { body });
1949
+ sent = await post();
1942
1950
  } catch (error) {
1943
1951
  if (!isTransientRemoteError(error)) throw error;
1944
1952
  try {
1945
- sent = await call(ctx, 'POST', '/api/message', { body });
1953
+ sent = await post();
1946
1954
  } catch (retryError) {
1947
1955
  if (isOwnDuplicateMessage(retryError, messageId)) return { duplicate: false, messageId };
1948
1956
  if (!isTransientRemoteError(retryError)) throw retryError;
@@ -1968,6 +1976,36 @@ export function createRemoteRelayDispatcher({
1968
1976
  };
1969
1977
  }
1970
1978
 
1979
+ /**
1980
+ * POST /api/conversation/:id/billing with a message. A relay older than the
1981
+ * switch answers 404 without a code: nothing was switched or sent. Its
1982
+ * refusals (the billing not set up there, the model, the cap, the
1983
+ * history) keep the remote's words.
1984
+ */
1985
+ async function postBillingSwitch(ctx, session, body) {
1986
+ try {
1987
+ return await call(ctx, 'POST', sessionPath(session, '/billing'), { body });
1988
+ } catch (error) {
1989
+ if (isTransientRemoteError(error)) throw error;
1990
+ const remoteCode = toText(error?.remoteBody?.code);
1991
+ const label = body.to === 'claude-api' ? 'the API key' : 'the subscription';
1992
+ if (Number(error?.status) === 404 && !remoteCode) {
1993
+ throw new DispatchFailure(409, REMOTE_RELAY_ERROR_CODES.unsupported,
1994
+ `Relay "${ctx.relay.name}" cannot switch a session's billing (it runs an OAR from before the switch); nothing was sent. Send without provider to keep the session's billing.`,
1995
+ { session });
1996
+ }
1997
+ const remoteText = toText(error?.remoteBody?.error) || toText(error?.message);
1998
+ const code = remoteCode === 'CLAUDE_BILLING_MODEL_UNAVAILABLE'
1999
+ ? REMOTE_RELAY_ERROR_CODES.unknownModel
2000
+ : (['CLAUDE_API_NOT_CONFIGURED', 'CLAUDE_NOT_CONFIGURED', 'CLAUDE_API_CAP_REACHED'].includes(remoteCode)
2001
+ ? REMOTE_RELAY_ERROR_CODES.providerUnavailable
2002
+ : (Number(error?.status) === 404 ? REMOTE_RELAY_ERROR_CODES.notFound : REMOTE_RELAY_ERROR_CODES.invalidInput));
2003
+ throw new DispatchFailure(Number(error?.status) || 409, code,
2004
+ `Relay "${ctx.relay.name}" did not switch session ${session} to ${label}: ${remoteText || 'refused'}. Nothing was sent.`,
2005
+ { session, ...(remoteCode ? { remoteCode } : {}) });
2006
+ }
2007
+ }
2008
+
1971
2009
  /**
1972
2010
  * What the remote made of the effort: `reported` is the one it bound (the
1973
2011
  * bootstrap's preferredReasoningEffort, the message's
@@ -2089,10 +2127,19 @@ export function createRemoteRelayDispatcher({
2089
2127
  }
2090
2128
  const runtime = isPlainObject(conversation?.runtimeSession) ? conversation.runtimeSession : {};
2091
2129
  const provider = toText(runtime.providerType).toLowerCase() || 'github';
2130
+ // `provider` on a send is a Claude session's billing: the subscription
2131
+ // or the API key. The other one switches the session with this message.
2132
+ const billing = toText(args.provider).toLowerCase();
2133
+ if (billing && !REMOTE_RELAY_SEND_PROVIDERS.includes(provider)) {
2134
+ throw new DispatchFailure(400, REMOTE_RELAY_ERROR_CODES.invalidInput,
2135
+ `Session ${session} on relay "${ctx.relay.name}" runs on ${provider}: provider on send switches a Claude session's billing only. Nothing was sent.`,
2136
+ { session, provider });
2137
+ }
2138
+ const switchTo = billing && billing !== provider ? billing : '';
2092
2139
  // Checked before any approval card (runRelayAction) when the session
2093
- // could be read there; here otherwise.
2140
+ // could be read there; here otherwise. Against the billing it moves to.
2094
2141
  const settled = args.model
2095
- ? (ctx.settledModel || await settleSendModel(ctx, conversation, args.model))
2142
+ ? (ctx.settledModel || await settleSendModel(ctx, conversation, args.model, { provider: billing }))
2096
2143
  : null;
2097
2144
  const requestedModel = settled?.model || '';
2098
2145
  const modelNote = settled?.note || null;
@@ -2109,16 +2156,25 @@ export function createRemoteRelayDispatcher({
2109
2156
  const requested = normalizeEffort(args.effort) || normalizeEffort(settled?.effort);
2110
2157
  const own = requested ? '' : normalizeEffort(conversation?.preferredReasoningEffort);
2111
2158
  const origin = await buildOrigin(ctx);
2112
- const { sent, choice } = await postPromptWithEffort(ctx, { session, text: args.text, origin, model, mode }, {
2159
+ const { sent, choice } = await postPromptWithEffort(ctx, { session, text: args.text, origin, model, mode, billing: switchTo }, {
2113
2160
  effort: requested || own,
2114
2161
  source: requested ? EFFORT_SOURCES.requested : (own ? EFFORT_SOURCES.remote : ''),
2115
2162
  note: null,
2116
2163
  });
2164
+ const billingNote = switchTo
2165
+ ? `Session ${session} now runs on ${switchTo === 'claude-api' ? 'the API key' : 'the subscription'}, from this message on (a running turn finishes on the old billing).`
2166
+ : '';
2117
2167
  return deliverAndWait('send', ctx, {
2118
2168
  session,
2119
2169
  sent,
2120
2170
  waitSeconds: args.wait_seconds,
2121
- extra: { ...(modelNote ? { modelNote } : {}), ...effortResult(choice) },
2171
+ extra: {
2172
+ ...(modelNote ? { modelNote } : {}),
2173
+ ...(billing && REMOTE_RELAY_SEND_PROVIDERS.includes(provider) ? { provider: switchTo || provider } : {}),
2174
+ ...(switchTo ? { billingSwitched: true } : {}),
2175
+ ...effortResult(choice),
2176
+ ...(billingNote ? { note: [effortResult(choice).note, billingNote].filter(Boolean).join(' ') } : {}),
2177
+ },
2122
2178
  });
2123
2179
  }
2124
2180
 
@@ -2177,9 +2233,11 @@ export function createRemoteRelayDispatcher({
2177
2233
  }
2178
2234
 
2179
2235
  /** settleRequestedModel for a send to `conversation` (its detail payload); an unread catalogue leaves it unchecked. */
2180
- async function settleSendModel(ctx, conversation, requested) {
2236
+ async function settleSendModel(ctx, conversation, requested, { provider: billing = '' } = {}) {
2181
2237
  const runtime = isPlainObject(conversation?.runtimeSession) ? conversation.runtimeSession : {};
2182
- const provider = toText(runtime.providerType).toLowerCase() || 'github';
2238
+ const own = toText(runtime.providerType).toLowerCase() || 'github';
2239
+ // A Claude session moving to its other billing takes that billing's list.
2240
+ const provider = billing && REMOTE_RELAY_SEND_PROVIDERS.includes(own) ? billing : own;
2183
2241
  let catalog = null;
2184
2242
  try {
2185
2243
  catalog = await loadCatalog(ctx);
@@ -3710,7 +3768,7 @@ export function createRemoteRelayDispatcher({
3710
3768
  }
3711
3769
  } else {
3712
3770
  const conversation = await call(early, 'GET', sessionPath(args.session), { query: { limit: 1 } });
3713
- settledModel = await settleSendModel(early, conversation, args.model);
3771
+ settledModel = await settleSendModel(early, conversation, args.model, { provider: args.provider });
3714
3772
  }
3715
3773
  } catch (error) {
3716
3774
  if (error instanceof DispatchFailure) {
@@ -3721,6 +3779,18 @@ export function createRemoteRelayDispatcher({
3721
3779
  settledModel = null;
3722
3780
  }
3723
3781
  }
3782
+ // The card names a billing switch only when the send makes one: a
3783
+ // provider that is already the session's billing changes nothing. Read
3784
+ // off the target before anyone is asked; a target that cannot be read
3785
+ // keeps the line (the send itself then answers).
3786
+ let cardArgs = args;
3787
+ if (action === 'send' && args.provider) {
3788
+ try {
3789
+ const conversation = await call({ relay, caller, hops, conversationId, signal }, 'GET', sessionPath(args.session), { query: { limit: 1 } });
3790
+ const runtime = isPlainObject(conversation?.runtimeSession) ? conversation.runtimeSession : {};
3791
+ if (toText(runtime.providerType).toLowerCase() === args.provider) cardArgs = { ...args, provider: undefined };
3792
+ } catch {}
3793
+ }
3724
3794
  // What an update_session would change, settled on the target before
3725
3795
  // anyone is asked: an unknown model or group, a busy session or a change
3726
3796
  // to nothing never raises a card.
@@ -3787,7 +3857,7 @@ export function createRemoteRelayDispatcher({
3787
3857
  callerContext: caller,
3788
3858
  relay: relayTarget(relay),
3789
3859
  action,
3790
- args,
3860
+ args: cardArgs,
3791
3861
  summary: summarizeRemoteRelayCall({ action, ...args, relay: relay.name }),
3792
3862
  signal,
3793
3863
  agentPrompts: agentPrompts(conversationId).count,
@@ -3809,7 +3879,7 @@ export function createRemoteRelayDispatcher({
3809
3879
  callerContext: caller,
3810
3880
  relay: relayTarget(relay),
3811
3881
  action,
3812
- args,
3882
+ args: cardArgs,
3813
3883
  summary: summarizeRemoteRelayCall({ action, ...args, relay: relay.name }),
3814
3884
  signal,
3815
3885
  ...(group ? { group } : {}),
@@ -27,7 +27,7 @@ import { remoteRelayAliases } from '../../shared/remote-relay-mentions.mjs';
27
27
  // remote_relay_inbound_enabled accept prompts from other relays' agents
28
28
  // agent_sessions_enabled this relay's own agents may start and use
29
29
  // sessions on it (the local target; off by default)
30
- // remote_relay_max_wait_seconds the longest wait of one remote_relay call
30
+ // remote_relay_max_wait_seconds the longest wait of one relay_tool call
31
31
  //
32
32
  // Tokens never leave through listPublic(), the socket event or any route; only
33
33
  // the outbound client reads them (list()/get()/resolve() are server-internal).