@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
@@ -9,6 +9,14 @@ import { DEFAULT_QUESTION_TIMEOUT_MS } from "../../../../shared/question-timeout
9
9
  import { waitForRelayQuestion } from "../../../../shared/question-wait.mjs";
10
10
  import { EMPTY_TURN_COMPLETION_NOTE } from "../../../../shared/empty-turn-completion.mjs";
11
11
  import { stripPromptContextPrefix } from "../skills/prompt-context.mjs";
12
+ import {
13
+ isOpenAIImageTerminalError,
14
+ runDirectOpenAIImageTurn,
15
+ shouldUseDirectOpenAIImageApi,
16
+ } from "../../../../shared/worker-runtime/direct-openai-image.mjs";
17
+
18
+ // Re-exported for the extension's tests, which import it from here.
19
+ export { shouldUseDirectOpenAIImageApi };
12
20
 
13
21
  function isImageAttachment(att) {
14
22
  const type = String(att?.type || "").toLowerCase();
@@ -257,28 +265,6 @@ export function resolveEmptyFinalTextHandling({ lastStreamedSent = "", lastActiv
257
265
  };
258
266
  }
259
267
 
260
- export function shouldUseDirectOpenAIImageApi(message = {}) {
261
- const providerType = String(message?.providerType || "").trim().toLowerCase();
262
- if (providerType !== "openai") return false;
263
- const model = String(message?.providerModel || message?.model || "").trim().toLowerCase();
264
- return model.startsWith("gpt-image-") || model.startsWith("dall-e-");
265
- }
266
-
267
- function isDirectOpenAIImageTerminalStatus(status) {
268
- const numeric = Number(status);
269
- if (!Number.isFinite(numeric)) return false;
270
- return (
271
- numeric === 400
272
- || numeric === 401
273
- || numeric === 403
274
- || numeric === 404
275
- || numeric === 409
276
- || numeric === 422
277
- || numeric === 429
278
- || numeric === 503
279
- );
280
- }
281
-
282
268
  export async function publishRelayStreamEvent({
283
269
  api,
284
270
  message,
@@ -681,71 +667,12 @@ export function createPollingLoop({
681
667
  let sendAndWaitStartedAtMs = 0;
682
668
  const processDirectOpenAIImageRequest = async () => {
683
669
  if (!shouldUseDirectOpenAIImageApi(message)) return false;
684
- if (message?.imageOperationId) {
685
- const result = await api(
686
- "POST",
687
- `/api/image-operations/${encodeURIComponent(message.imageOperationId)}/execute`,
688
- { messageId: message.id },
689
- ).catch((error) => {
690
- if (error?.status === 409 || error?.status === 503) {
691
- return error?.payload || { outcome: error?.status === 503 ? "retryable" : "terminal" };
692
- }
693
- throw error;
694
- });
695
- const outcome = String(result?.outcome || "").trim().toLowerCase();
696
- if (!["completed", "executing", "terminal", "uncertain", "retryable"].includes(outcome)) {
697
- throw new Error("Image operation returned an invalid outcome");
698
- }
699
- await session.log(`Image operation ${outcome}`, {
700
- ephemeral: true,
701
- });
702
- return true;
703
- }
704
- const directModel = String(message?.providerModel || requestedManualModelOrNull(message) || message?.model || "").trim();
705
- const prompt = String(message?.text || "").trim();
706
- if (!directModel) throw new Error("OpenAI image request is missing model");
707
- if (!prompt) throw new Error("OpenAI image request is missing prompt");
708
- await api("POST", "/api/activity", {
709
- messageId: message.id,
710
- conversationId: message.conversationId,
711
- mode: message.relayMode || "agent",
712
- text: `Calling OpenAI image API directly (${directModel})`,
713
- }).catch(() => {});
714
- let directResult;
715
- try {
716
- directResult = await api("POST", "/api/openai/images/generate", {
717
- messageId: message.id,
718
- conversationId: message.conversationId,
719
- model: directModel,
720
- prompt,
721
- n: message?.n,
722
- size: message?.size || message?.contextTier,
723
- quality: message?.quality || message?.reasoningEffort,
724
- attachments: Array.isArray(message?.attachments) ? message.attachments : [],
725
- });
726
- } catch (error) {
727
- if (isDirectOpenAIImageTerminalStatus(error?.status)) {
728
- const terminalError = new Error(
729
- `OpenAI image request failed: ${error?.detail || `HTTP ${error?.status || "error"}`}`,
730
- );
731
- terminalError.code = "RELAY_OPENAI_IMAGE_TERMINAL";
732
- throw terminalError;
733
- }
734
- throw error;
735
- }
736
- const generatedImages = Array.isArray(directResult?.generatedImages) ? directResult.generatedImages : [];
737
- if (!generatedImages.length) {
738
- throw new Error("OpenAI image API returned no images");
670
+ const result = await runDirectOpenAIImageTurn({ api, message });
671
+ if (result.via === "operation") {
672
+ await session.log(`Image operation ${result.outcome}`, { ephemeral: true });
673
+ } else {
674
+ await session.log(`✅ Sent generated image response (${result.imageCount} image${result.imageCount === 1 ? "" : "s"})`, { ephemeral: true });
739
675
  }
740
- await api("POST", "/api/response", {
741
- messageId: message.id,
742
- conversationId: message.conversationId,
743
- text: "",
744
- generatedImages,
745
- model: String(directResult?.model || directModel).trim() || directModel,
746
- modelOrigin: isAutoRequestedModel(message?.model) ? "auto" : "manual",
747
- });
748
- await session.log(`✅ Sent generated image response (${generatedImages.length} image${generatedImages.length === 1 ? "" : "s"})`, { ephemeral: true });
749
676
  return true;
750
677
  };
751
678
 
@@ -988,7 +915,7 @@ export function createPollingLoop({
988
915
  }).catch(async () => {
989
916
  await api("POST", "/api/requeue", { messageId: message.id }).catch(() => {});
990
917
  });
991
- } else if (String(e?.code || "").trim() === "RELAY_OPENAI_IMAGE_TERMINAL") {
918
+ } else if (isOpenAIImageTerminalError(e)) {
992
919
  await pushRelayStream(lastStreamedSent, true);
993
920
  await api("POST", "/api/response", {
994
921
  messageId: message.id,
@@ -3,7 +3,7 @@ import {
3
3
  formatToolResultActivity as formatSharedToolResultActivity,
4
4
  formatVoteMemoryActivity,
5
5
  } from "../../../../shared/tool-activity.mjs";
6
- import { isRemoteRelayToolName } from "../../../../shared/remote-relay-tool-core.mjs";
6
+ import { REMOTE_RELAY_TOOL_NAME, isRemoteRelayToolName } from "../../../../shared/remote-relay-tool-core.mjs";
7
7
  import { summarizeRemoteRelayCall } from "../../../../shared/remote-relay-contract.mjs";
8
8
 
9
9
  export function normalizeActivityText(value, maxToolDetailLength = 140) {
@@ -135,10 +135,11 @@ export function formatToolActivity(request, maxToolDetailLength = 140) {
135
135
  if (lower.includes("report_intent")) {
136
136
  return detail ? `● ${detail}` : "● Working…";
137
137
  }
138
- // The OAR MCP server's tool reaches the CLI as `oar-remote_relay`: show the
139
- // same line every other engine writes for it.
138
+ // The OAR MCP server's tool reaches the CLI as `oar-relay_tool` (up to
139
+ // 0.9.17 `oar-remote_relay`): show the same line every other engine writes
140
+ // for it.
140
141
  if (isRemoteRelayToolName(rawName)) {
141
- return `Tool (remote_relay): ${summarizeRemoteRelayCall(toolArgsSnapshot(request))}`;
142
+ return `Tool (${REMOTE_RELAY_TOOL_NAME}): ${summarizeRemoteRelayCall(toolArgsSnapshot(request))}`;
142
143
  }
143
144
  return detail ? `Tool (${rawName}): ${detail}` : `Tool (${rawName})`;
144
145
  }
package/CHANGELOG.md CHANGED
@@ -3,6 +3,418 @@
3
3
  All notable changes to OAR are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow semver.
5
5
 
6
+ ## [0.9.18] — 2026-10-26
7
+
8
+ Highlights: Claude chats can run on a Claude Console API key. Claude API is
9
+ a provider beside your subscription, with an optional monthly spend cap and
10
+ a card in Check Usage with the estimated spend. A Claude chat can switch
11
+ between the subscription and the key and keep its Claude Code session, also
12
+ when it is paused at the usage limit. The relay's agent tool is now called
13
+ `relay_tool`. DeepSeek and Claude API chats start with all hooks off, and a
14
+ subscription chat no longer runs on an API key that Claude Code's settings
15
+ hand it.
16
+
17
+ ### Added
18
+
19
+ - **Claude API: Claude chats on a Claude Console API key.** A provider
20
+ beside Claude (your subscription), billed to the Console organisation
21
+ the key belongs to.
22
+ - Settings → Providers → Claude API holds the key, the switch, the
23
+ default model and an optional monthly spend cap in US dollars. The key
24
+ is stored on the relay host and never shown again: the tab only says
25
+ that one is saved. Any `sk-ant-` Console key is taken; Claude login
26
+ and session tokens and Admin keys are refused when you save. A key
27
+ Anthropic itself refuses stays saved, and the tab's status line says
28
+ so until a save or Refresh models goes through. A cap the field cannot
29
+ take is named under the field.
30
+ - The relay lists the key's models from Anthropic when the key is saved,
31
+ when Claude API is switched on again, and on Refresh models. Until
32
+ then it offers the Claude tab's models.
33
+ - Commit attribution, the background command limit, the effort levels
34
+ and the compaction window stay on the Claude tab, shared by both; the
35
+ Claude API tab links there.
36
+ - New Chat offers Claude API, Select Models has a Claude API tab, and
37
+ its chats show a gold "Claude API" pill and "🔒 Session locked to
38
+ Claude API models."
39
+ - A turn that Claude API refuses or could not run ends with buttons:
40
+ Claude API settings (at the cap field when the monthly cap is
41
+ reached), Switch to subscription, Add funds, Console billing or
42
+ Console limits, as fits the reason.
43
+ - When the relay refuses to start a Claude API chat (the monthly cap, no
44
+ key, switched off), New Chat stays open with the reason, Claude API
45
+ settings, and Start on subscription, which starts the same chat on
46
+ the Claude provider; on a phone the dialog scrolls to it.
47
+ - **Switch a Claude chat between your subscription and the Claude API
48
+ key.** `POST /api/conversation/:id/billing` moves a chat between
49
+ `claude` (the subscription) and `claude-api` (the saved key). It keeps
50
+ its Claude Code session.
51
+ - In the browser, a Claude chat has a `Subscription | API key` toggle
52
+ under the lock note while Claude API is on with a saved key. It says
53
+ what a pending switch waits for, and why a side cannot be chosen; at
54
+ the monthly cap it says the chat stays on the subscription, and so
55
+ does the usage-limit banner.
56
+ - The switch applies from the next turn: a running turn finishes on the
57
+ billing it started on. Messages already queued run on the new one.
58
+ - The relay restarts the chat's Claude Code between turns. It waits
59
+ while a turn, a question, a compaction or a background task is still
60
+ running.
61
+ - The transcript notes each switch. The first turn on the key has no
62
+ prompt cache and costs more.
63
+ - The switch is refused, with the reason, when the key is missing or
64
+ Claude API is off, when the monthly cap is reached, or when the
65
+ chat's model is not on the target's model list.
66
+ - **Switch this chat to API key at the usage limit.** When a subscription
67
+ chat is paused at the Claude usage limit, the banner offers the switch
68
+ if the chat may move to the key. The paused turn then carries on on
69
+ the key at once. Nothing switches by itself.
70
+ - **Agents can switch a Claude session's billing.** `relay_tool` `send`
71
+ takes `provider` (`claude` or `claude-api`) for a Claude session. The
72
+ other billing switches the session together with the message:
73
+ - in one call, so a refused switch sends nothing and a refused message
74
+ switches nothing;
75
+ - the approval card says "Switches billing to API key";
76
+ - a relay from before this version refuses it and sends nothing.
77
+ - **A Claude API card in Check Usage.** It shows what this relay's turns
78
+ on the Claude API key are estimated to have cost today and this month,
79
+ per model, with the tokens, and the monthly cap as a bar when one is set.
80
+ - The figures are Claude Code's own estimate from its price table, so
81
+ sub-agents, compaction and web search are in them. The Console's
82
+ billing page (linked) has the real figures and the balance.
83
+ - Days and the month follow the relay's time zone, and the cap starts
84
+ over at local midnight on the 1st.
85
+ - Tokens Claude Code has no price for show as "no price known" or
86
+ "estimate incomplete", never as $0.
87
+ - Turns paid from a Max or Team plan's subscriber API credit are counted
88
+ too; OAR cannot see that credit.
89
+ - The card stays while a key is saved, also with Claude API switched
90
+ off, so the month's spend stays in view (DeepSeek's card goes with the
91
+ provider).
92
+ - **A monthly cap for Claude API.** Once this month's estimate reaches the
93
+ cap set in Settings → Providers → Claude API:
94
+ - a new turn on the key does not run: the chat gets a note with the code
95
+ `relay.claude-api-cap-reached`. A turn Claude Code starts by itself,
96
+ when a background task reports back, gets the note and that chat's
97
+ background tasks are stopped as soon as the relay hears of the turn,
98
+ since each report would start another such turn; the turn itself is
99
+ stopped when the worker takes the stop. Its first request may already
100
+ be billed, and a short turn can end before the stop arrives. The note
101
+ also mentions tasks no stop reaches, since they can keep starting such
102
+ turns and keep a switch to the subscription waiting. The note comes
103
+ once per chat until the cap clears, a Claude API setting is saved, or a
104
+ turn of that chat runs on the key again. The same stop applies when
105
+ Claude API has no key or is switched off;
106
+ - a switch to the key is refused;
107
+ - an agent's send to a session on the key and a new Claude API chat (New
108
+ Chat or an agent's `create_session`) get 409 `CLAUDE_API_CAP_REACHED`,
109
+ and nothing is queued or created.
110
+
111
+ A turn that is already running finishes. A turn's cost is booked when it
112
+ ends, so running turns can take the month past the cap. The cap counts
113
+ only the priced part of the estimate. If the spend cannot be read, the cap counts as
114
+ reached. Console spend limits still apply on top.
115
+
116
+ ### Changed
117
+
118
+ - **A send does not switch a Claude chat's billing.** A message that
119
+ names the other billing gets 409 `CLAUDE_BILLING_SWITCH_REQUIRED`
120
+ (before, a Claude API chat ignored `provider: "claude"`). A message
121
+ from a page that still showed the other billing gets 409
122
+ `CLAUDE_BILLING_CHANGED`. Its text goes into the chat's draft, so the
123
+ page, and a send replayed from the offline outbox, lose nothing. The
124
+ outbox keeps the text only, not attachments.
125
+
126
+ - **No hooks or status line run in DeepSeek chats.** In 0.9.17, Claude Code
127
+ in a DeepSeek chat ran the hooks and the `statusLine` command of
128
+ DeepSeek's settings folder, of the chat folder's `.claude/settings.json`
129
+ and `settings.local.json`, and of plugins, as the relay's user with the
130
+ DeepSeek key in their environment; a folder's `SessionStart` hook ran
131
+ before the relay had pinned the endpoint and key. Now those chats start
132
+ with all hooks off, and so do Claude API chats. The setting is on Claude
133
+ Code's command line, so it is in force from the start, and a folder's
134
+ settings can't turn it back on (`scripts/claude-cli-hooks-proof.mjs`
135
+ shows it on the bundled Claude Code). Hooks in managed settings still
136
+ run. Chats on your Claude login keep their hooks and status line. The
137
+ agent's own commands can still read the key; the relay redacts it in
138
+ what it shows.
139
+ - **The relay's agent tool is now called `relay_tool`.** It was
140
+ `remote_relay`, and it is listed under the new name on every provider
141
+ (Claude's `mcp__relay__relay_tool`), with the same actions and fields; the
142
+ docs and the agents' instructions use it too. OAR's own MCP server, which
143
+ Grok and the Copilot extension engine use, still runs a call to
144
+ `remote_relay` but no longer lists it. Tool lines written under the old
145
+ name still show as this tool.
146
+ - If your own instructions, permission rules or hooks name `remote_relay`
147
+ or `mcp__relay__remote_relay`, change them to `relay_tool` or
148
+ `mcp__relay__relay_tool`: outside OAR's MCP server the old name is
149
+ refused.
150
+ - **A subscription chat never runs on an API key.** Claude Code takes an
151
+ API key over your Claude login when one of its settings hands it one:
152
+ - `env.ANTHROPIC_API_KEY` in `~/.claude/settings.json` or the chat
153
+ folder's `.claude/settings*.json`;
154
+ - an `apiKeyHelper`;
155
+ - a Console login.
156
+
157
+ Until now such a chat ran on that key and billed the key's owner. Now
158
+ the turn stops at Claude Code's start, with a note naming the source
159
+ and the setting to remove. At most one model request, sent a moment
160
+ after that start, can go out first.
161
+
162
+ - **The docs cover Claude API.** The providers guide has a Claude API
163
+ section: the key, the models, the shared `~/.claude`, the billing switch
164
+ and when it applies, what fails closed, the monthly cap and the
165
+ subscriber API credit. The web UI, configuration, security,
166
+ agents-together and troubleshooting pages, the README, the server
167
+ README and DEVELOPING.md (the worker and its live check) cover it too,
168
+ and the feature tracker has a Claude API page. Troubleshooting has two
169
+ known gaps:
170
+ - a key that is not tied to a workspace gets HTTP 400 from Anthropic;
171
+ for now, create the key inside a workspace (naming the workspace in
172
+ OAR is planned);
173
+ - a model Claude Code has no price for (currently Claude Haiku 5.5)
174
+ shows "no price known" on the Claude API card, and its tokens are left
175
+ out of the estimate.
176
+ - **Corrections to the 0.9.17 notes:**
177
+ - DeepSeek's budget bar changes colour at 50 % and 75 % of the budget
178
+ and turns red at 90 %; the notes said amber near it and red over it.
179
+ - The composer's context chip shows DeepSeek's 1M window (1,048,576
180
+ tokens) as `1.05M`.
181
+ - The `preview` tool's hint to bind a dev server to 127.0.0.1 reaches
182
+ only sessions that have the tool (Claude, Claude API, DeepSeek, Cursor,
183
+ and Grok and Copilot's Extension engine through OAR's MCP server), not
184
+ every provider. Copilot's SDK engine, and those two when the relay runs
185
+ with `OAR_MCP_SERVER=0`, are taught the preview API without it.
186
+
187
+ ### Upgrade notes
188
+
189
+ - **Downgrading to 0.9.17:** first switch Claude chats on the Claude API
190
+ key back to Subscription. 0.9.17 does not know Claude API, so a chat
191
+ left on the key runs as a Copilot chat there. That now includes a chat
192
+ that started on the subscription and was switched to the key. 0.9.17
193
+ ignores the new database columns and tables. The two triggers on the
194
+ queue table keep stamping its new column there, which does no harm.
195
+ - **Downgrading to 0.9.17 turns hooks back on in DeepSeek chats:** they
196
+ run the hooks and status line of their settings folders again.
197
+
198
+ ## [0.9.17] — 2026-10-25
199
+
200
+ Highlights: DeepSeek is a provider of its own. Paste a DeepSeek API key and
201
+ New Chat offers DeepSeek's models, with all four modes, tools, question
202
+ cards, sub-agents and reasoning effort, the 1M context window, a usage card
203
+ with the balance and the estimated spend, and an import of earlier usage
204
+ from DeepSeek's usage export. A new session in a group now shows
205
+ at the top of the group. And the relay's own Anthropic credentials
206
+ (`ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`) no longer reach the agents it
207
+ starts.
208
+
209
+ ### Added
210
+
211
+ - **DeepSeek is a provider of its own.** Paste a DeepSeek API key in
212
+ Settings → Providers → DeepSeek, and New Chat offers DeepSeek with the
213
+ models DeepSeek lists (`deepseek-flash` by default, `deepseek-v4-pro`).
214
+ - A DeepSeek chat runs Claude Code on the relay host against DeepSeek's
215
+ Anthropic-compatible API, so it has all four modes, tools, question
216
+ cards, sub-agents, steering, Stop and background tasks.
217
+ - Reasoning effort is low, high or max (high by default). The model can
218
+ change between turns; a switch to a model without image input is
219
+ refused once the chat holds images.
220
+ - The context panel shows the 1M window, and the chat compacts at 400K
221
+ unless you set another window there.
222
+ - Commits carry `Open Agent Relay (DeepSeek Flash)` or no attribution, as
223
+ set on the DeepSeek tab.
224
+ - The key is stored on the relay host, is never sent to browsers, and
225
+ reaches only DeepSeek's workers. Their Claude Code keeps its state in a
226
+ directory of its own, without your Claude login. The tab's Advanced
227
+ section takes another base URL (https, or http on the same machine).
228
+ - The chat folder's settings can't route the key elsewhere. A
229
+ `.claude/settings.json` or `.claude/settings.local.json` there that sets
230
+ `ANTHROPIC_BASE_URL`, a key of its own or an `apiKeyHelper` took the
231
+ DeepSeek key to that endpoint, or replaced it; the relay now pins
232
+ DeepSeek's endpoint and key over the folder's settings, and with them
233
+ the proxy, TLS trust, provider and credential settings, without putting
234
+ the key on the command line, and sends nothing when Claude Code doesn't
235
+ take them within a minute (`scripts/deepseek-cli-proof.mjs` shows it on
236
+ the bundled Claude Code). This covers settings, not commands: hooks and
237
+ a status line in those files still run as the relay's user, as in any
238
+ Claude Code chat, so use DeepSeek chats only in folders you trust.
239
+ - The agent's own commands can read the key from their environment, as
240
+ with any key a CLI holds. The relay replaces it with `[redacted]` in
241
+ everything a worker posts (replies, thoughts, tool activity, sub-agent
242
+ text, questions, task output), including the first part of a key cut
243
+ short at a text's end or before an ellipsis, the last part of one a
244
+ text starts with, and keys changed since the relay started, before it
245
+ stores or shows it; the DeepSeek worker does the same in its log. Task
246
+ output the relay reads from a task's file is scrubbed by shape only
247
+ (`sk-` keys and other long tokens), not by the saved key's exact text.
248
+ This keeps the key out of what the relay stores and shows; it is no
249
+ barrier against the agent, which can change the text, call the relay's
250
+ local API without the worker header or read the relay's database.
251
+ - The usage view has a DeepSeek card: the account balance, read with the
252
+ saved key, and the estimated spend of DeepSeek turns on this relay, by
253
+ model, from DeepSeek's published prices with peak and off-peak hours
254
+ (the card names the peak hours in your browser's time zone). On Chinese
255
+ public holidays DeepSeek charges off-peak prices all day; the estimate
256
+ doesn't know those dates, so turns at peak hours on a holiday are
257
+ counted at double the real price. Only WebSearch runs on DeepSeek's
258
+ servers: its searches are counted at $0.75 / ¥5 per 1,000 requests from
259
+ 2026-10-31 16:00 UTC, on a "Search" row of their own, taken from Claude
260
+ Code's own totals, which count the request its WebSearch tool makes.
261
+ WebFetch reads the page on the relay host and costs only tokens, and
262
+ DeepSeek does not bill count_tokens requests.
263
+ - The card draws the money: a balance bar per currency, measured against
264
+ the balance after its last top-up (kept on the relay; migration 0029
265
+ notes when it rose), or against a starting balance set on the DeepSeek
266
+ tab, such as the first top-up, until the next top-up replaces it (typed
267
+ in again, the same amount too, it counts from then on); the
268
+ daily spend of the last 30 days as columns stacked by model, with the
269
+ day's total on hover or tap; where the money goes per model (cache hits, misses,
270
+ output, searches) with what the cache saved; and a monthly
271
+ budget bar when a budget is set on the DeepSeek tab, in the currency
272
+ chosen with it (USD or CNY, named on the bar; a budget saved without one
273
+ follows the balance's currency), amber near it and red over it, starting
274
+ over on the 1st (UTC). No chart library; every bar has a text equivalent
275
+ and every chart a table, in both themes. The card shows everything at
276
+ once, figures first (the balance and the spend as label-and-value rows,
277
+ the tables by model open, the note on the estimate last), and fits a
278
+ phone's width.
279
+ - A DeepSeek chat's cold start reads "Starting Claude Code for DeepSeek",
280
+ and the `preview` tool tells the agent to bind its dev server to
281
+ 127.0.0.1 (every provider).
282
+ - Sub-agents stay on the chat's model. Claude Code's model aliases are
283
+ pinned to it, so a sub-agent the agent asks for as "opus" or "sonnet",
284
+ or one without a model, runs on the chat's model, and one asked for as
285
+ "haiku" on DeepSeek Flash; before, an alias went to DeepSeek as a Claude
286
+ model id, which DeepSeek serves by its own mapping (a Flash chat got a
287
+ V4 Pro sub-agent). A model switch between turns now restarts the chat's
288
+ Claude Code, like an effort change. A sub-agent's badge names the
289
+ DeepSeek model, and usage a Claude Code still reports under a Claude
290
+ name is booked and priced under the DeepSeek model that served it;
291
+ rows booked under a Claude name earlier are folded in at start-up
292
+ (migration 0026). The folded rows are priced at one time each, the
293
+ row's last turn, so a row that spans peak and off-peak turns is
294
+ approximated (the token part by up to 2×).
295
+ - **Import usage export** on the DeepSeek tab takes the two CSV files
296
+ DeepSeek's usage page exports (amount and cost) and shows what was
297
+ imported. The usage card then shows DeepSeek's own figures for the saved
298
+ key on the days the export covers, by the export's days, and the
299
+ estimate on the other days; the card says through when. Only rows in
300
+ the card's currency are used, and the import says when they won't be.
301
+ A re-import replaces the key's days it covers, a day with a period that
302
+ has not ended is left out, and so are export days up to and including
303
+ the first day of the relay's daily history.
304
+ `npm run deepseek:import -- amount.csv cost.csv [db-path]` does the same
305
+ from the shell. The relay's own usage logging is unchanged (migration
306
+ 0030 adds a table of its own).
307
+
308
+ ### Changed
309
+
310
+ - **A new session in a group shows at the top of the group.** In a group
311
+ you ordered by hand, a session an agent creates there, one started with
312
+ the group chosen, or one moved in by its menu used to join at the end;
313
+ it now comes first, right under the group's first session. The order you
314
+ set stays, and the next drag numbers the members again from there.
315
+ - **A host's `ANTHROPIC_API_KEY` and `ANTHROPIC_AUTH_TOKEN` no longer reach
316
+ any worker.** A relay started with either in its environment handed it to
317
+ every worker it started (Copilot, OpenAI, Cursor, Grok, Claude, Claude
318
+ Cloud, DeepSeek), on every platform, and Claude Code prefers either to the
319
+ Claude login. Now no worker gets them, and under tmux the worker's shell
320
+ also unsets them, so they cannot come back from a tmux server that was
321
+ already running or from your login profile. Claude chats that ran on such
322
+ a key now run on the host's Claude login.
323
+
324
+ ### Fixed
325
+
326
+ - **The Inbox opens at the top, or where you left it.** It used to open at
327
+ the scroll height of the transcript it replaced, somewhere in the middle of
328
+ the list. It now opens at the top the first time and afterwards where it
329
+ was scrolled when you last left it.
330
+ - **The session header on a phone comes in two rows.** ☰, the title and the
331
+ buttons share the first row, so the title gets the width left after the
332
+ buttons. The "via agent" line, the watching, watched and scheduled chips
333
+ and the folder sit side by side on a second row across the header, and a
334
+ chip no longer breaks inside its text. The desktop header is unchanged.
335
+ - **A DeepSeek chat's failures carry DeepSeek's codes, and their buttons
336
+ show.** A refused DeepSeek request printed as `relay.claude-api-error`,
337
+ and a used-up balance or a refused key as `relay.claude-deepseek-balance`
338
+ or `relay.claude-deepseek-api-key`, which the **Top up DeepSeek** and
339
+ **DeepSeek settings** buttons never matched. They now print as
340
+ `relay.deepseek-balance`, `relay.deepseek-api-key`,
341
+ `relay.deepseek-api-error` and so on, without the Claude account advice.
342
+ - **A relay's Claude login token no longer reaches workers that are not
343
+ Claude's.** On a relay started with `CLAUDE_CODE_OAUTH_TOKEN` in its
344
+ environment, the Copilot, OpenAI, Cursor and Grok workers got the token too:
345
+ on Windows and without tmux through the environment they inherit, and under
346
+ tmux whenever the launch started the tmux server, which then kept it for
347
+ every later session. Now only Claude Cloud and Claude workers get it, under
348
+ tmux through the owner-only file. Local Claude chats on a host that runs on
349
+ the token alone now get it under tmux as well.
350
+ - **A relay started from inside a Claude Code session no longer hands that
351
+ session to its workers.** Its variables (`CLAUDECODE`, the session id, the
352
+ messaging socket and token, entrypoint, `CLAUDE_PID`, `CLAUDE_EFFORT`)
353
+ reached every worker, with and without tmux, and a DeepSeek chat's Claude
354
+ Code took itself for a child of the host's session. The launcher now drops
355
+ them for every provider, and the DeepSeek backend drops them again. The
356
+ values of one launch (worker kind, model, folder) go into that session's
357
+ command only, no longer into the environment a tmux server started by the
358
+ relay kept for every later session. Under tmux that was not enough: a new
359
+ tmux session starts from the tmux server's own environment, which a
360
+ server started from inside a Claude Code session, or by an older relay,
361
+ carried on to every worker. Every worker's command now begins by unsetting
362
+ the host session's variables and the launch variables it sets no value
363
+ for, so a stale model or worker kind from an earlier launch is gone too.
364
+ The host session's variables are matched by family as well as by name
365
+ (every `CLAUDE_CODE_SESSION_*`, `CLAUDE_CODE_*_SESSION_ID` and
366
+ `CLAUDE_CODE_MESSAGING_*`, with the SDK host's version and app markers
367
+ and the Bash cap a relay's Claude worker gives its CLI), so a name a
368
+ newer Claude Code adds is dropped too; your own `CLAUDE_CODE_*` and
369
+ `CLAUDE_AGENT_SDK_*` settings stay. A DeepSeek chat's Claude Code also no
370
+ longer gets the relay's settings for its worker (worker kind, the relay's
371
+ folders and tools file, `COPILOT_ALLOW_ALL`, `SESSION_ID`);
372
+ with them the worker log file a relay started from inside a worker
373
+ session inherited, and, by name, every secret the launch carries no value
374
+ for (other kinds' and its own alike) that a shell may have exported into
375
+ the tmux server's environment; the worker's own keys come from its secret
376
+ file, read after that.
377
+ - **The DeepSeek spend no longer undercounts a Claude Code that started
378
+ counting over, and no longer double-counts a report that fell behind.**
379
+ The relay books what a worker's report adds to the session's last totals;
380
+ totals below them were booked as nothing. Now the worker names its Claude
381
+ Code process in each report (`runId`; migration 0027 keeps it with the
382
+ checkpoint): totals below the checkpoint from the same process are a
383
+ stale report (a retried post, or one delivered after a later one) and
384
+ book nothing, while from another process they are a CLI counting from
385
+ zero again on the same session id, booked in full. The web searches and
386
+ fetches are reported as the process's running counts and booked the same
387
+ way, so a retried post no longer counts them twice.
388
+ - **The DeepSeek spend no longer books a session's history again after a
389
+ Claude Code died hard.** Claude Code writes its cost state at exit, so a
390
+ resumed CLI that was killed left an older snapshot, and the next resume
391
+ restored totals below what the relay had booked: read as a process
392
+ counting from zero, the whole restored history was booked once more. The
393
+ worker no longer reports what the CLI restored: it reports each process's
394
+ own totals, counted from zero (what the session totals grow by per
395
+ result; for the first result of a resumed process the turn's own usage,
396
+ which leaves out what sub-agents spent in that one turn), and the relay
397
+ books a new process in full and the increase within one. The search and
398
+ fetch counts keep a run id of their own in the checkpoint (migration
399
+ 0028), so a report with counts only no longer moves the token totals' run
400
+ id, and a `/clear` in the CLI, which starts a new session id, starts the
401
+ worker's counts over with it instead of booking the old session's under
402
+ the new one. A turn that finishes after its Claude Code died (a background
403
+ task's continuation whose row was still registering) reports under that
404
+ process, not under the one running by then or as a report from no
405
+ process, and a result that arrives behind a newer one never lowers the
406
+ worker's baseline. A late report of an earlier process after a later one
407
+ started is booked in full, and the later process's next report is booked
408
+ in full too: the relay cannot tell either from a new process.
409
+ - **OpenAI image models work on the SDK engine again.** A turn on
410
+ `gpt-image-*` or `dall-e-*` went to the Copilot runtime as a chat turn
411
+ and failed ("No available capacity was found for the model", or a request
412
+ rejected as invalid). It now goes to the OpenAI Images API, as on the
413
+ Extension engine, and an attached image is edited. A refused image request
414
+ now fails its turn with `relay.image-provider-rejected` instead of staying
415
+ in progress until the relay ran it again, and an OpenAI chat turn that
416
+ fails on the SDK engine says OpenAI rather than Copilot.
417
+
6
418
  ## [0.9.16] — 2026-10-24
7
419
 
8
420
  Highlights: agents can file the sessions they start into your session