@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.
- package/.github/extensions/web-relay/polling/polling-loop.mjs +14 -87
- package/.github/extensions/web-relay/skills/tool-activity.mjs +5 -4
- package/CHANGELOG.md +412 -0
- package/README.md +15 -13
- package/package.json +2 -1
- package/server/claude-api-worker/claude-api-backend.mjs +345 -0
- package/server/claude-api-worker/claude-api-session-worker.mjs +29 -0
- package/server/claude-worker/claude-attachments.mjs +7 -4
- package/server/claude-worker/claude-cli-pinned-env.mjs +150 -0
- package/server/claude-worker/claude-remote-relay-tool.mjs +3 -3
- package/server/claude-worker/claude-sdk-adapter.mjs +199 -15
- package/server/claude-worker/claude-session-process.mjs +276 -33
- package/server/claude-worker/claude-session-worker-main.mjs +269 -0
- package/server/claude-worker/claude-session-worker.mjs +2 -253
- package/server/claude-worker/claude-subscription-key-check.mjs +64 -0
- package/server/claude-worker/claude-turn-failure.mjs +8 -0
- package/server/claude-worker/claude-turn-publisher.mjs +394 -13
- package/server/claude-worker/claude-usage-totals.mjs +161 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +56 -1
- package/server/copilot-worker/copilot-remote-relay-tool.mjs +2 -2
- package/server/copilot-worker/copilot-sdk-adapter.mjs +18 -4
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +1 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +80 -14
- package/server/cursor-worker/cursor-remote-relay-tool.mjs +3 -3
- package/server/cursor-worker/cursor-turn-runner.mjs +4 -4
- package/server/cursor-worker/sdk-message-normalizer.mjs +2 -2
- package/server/db-schema.mjs +96 -1
- package/server/deepseek-worker/deepseek-claude-backend.mjs +317 -0
- package/server/deepseek-worker/deepseek-session-worker.mjs +37 -0
- package/server/grok-worker/acp-client.mjs +1 -1
- package/server/grok-worker/grok-turn-runner.mjs +4 -4
- package/server/grok-worker/sdk-message-normalizer.mjs +1 -1
- package/server/mcp/oar-mcp-server.mjs +11 -5
- package/server/migrations/0024-deepseek-usage-totals.mjs +151 -0
- package/server/migrations/0025-deepseek-usage-requests-daily.mjs +179 -0
- package/server/migrations/0026-deepseek-usage-served-models.mjs +222 -0
- package/server/migrations/0027-deepseek-usage-checkpoint-runs.mjs +101 -0
- package/server/migrations/0028-deepseek-usage-checkpoint-request-runs.mjs +99 -0
- package/server/migrations/0029-deepseek-balance-rises.mjs +99 -0
- package/server/migrations/0030-deepseek-usage-actuals.mjs +123 -0
- package/server/migrations/0031-queue-provider.mjs +143 -0
- package/server/migrations/0032-runtime-session-billing-switch.mjs +94 -0
- package/server/migrations/0033-claude-api-usage-checkpoints.mjs +138 -0
- package/server/migrations/0034-claude-api-usage-totals.mjs +112 -0
- package/server/public/app/activity-replay-state.mjs +4 -3
- package/server/public/app/agent-sessions-settings-ui.js +1 -1
- package/server/public/app/agent-sessions-settings.mjs +1 -1
- package/server/public/app/api-client.js +105 -0
- package/server/public/app/bootstrap.js +243 -29
- package/server/public/app/claude-api-settings-ui.js +362 -0
- package/server/public/app/claude-billing-toggle-ui.js +224 -0
- package/server/public/app/claude-billing-toggle.mjs +112 -0
- package/server/public/app/context-tier-options.mjs +10 -2
- package/server/public/app/context-usage-view.mjs +1 -1
- package/server/public/app/conversation-provider-indicator.mjs +18 -0
- package/server/public/app/conversation-view.js +31 -3
- package/server/public/app/deepseek-settings-ui.js +342 -0
- package/server/public/app/inbox-ui.js +14 -3
- package/server/public/app/inbox-view.mjs +1 -1
- package/server/public/app/journal-view.js +156 -11
- package/server/public/app/model-selector-options.mjs +28 -1
- package/server/public/app/model-vision-support.mjs +2 -0
- package/server/public/app/new-conversation-model-choice.mjs +21 -0
- package/server/public/app/no-conversation-state.mjs +1 -1
- package/server/public/app/plan-usage-view.mjs +165 -13
- package/server/public/app/provider-readiness.mjs +8 -2
- package/server/public/app/relay-error-ctas.mjs +58 -0
- package/server/public/app/relay-schedules-ui.js +1 -1
- package/server/public/app/settings-modal.js +22 -0
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +28 -1
- package/server/public/app/tool-activity-emoji.mjs +4 -3
- package/server/public/app/usage-limit-ui.js +136 -5
- package/server/public/index.html +596 -35
- package/server/relay-tools.md +4 -4
- package/server/repositories/conversation-group-repository.mjs +14 -7
- package/server/repositories/message-repository.mjs +7 -0
- package/server/repositories/session-repository.mjs +8 -1
- package/server/routes/ask-user-routes.mjs +1 -1
- package/server/routes/conversation-group-routes.mjs +3 -2
- package/server/routes/deepseek-usage-import-routes.mjs +72 -0
- package/server/routes/messages-routes.mjs +831 -36
- package/server/routes/relay-ask-routes.mjs +3 -3
- package/server/routes/relay-event-routes.mjs +2 -2
- package/server/routes/relay-schedule-routes.mjs +1 -1
- package/server/routes/remote-relay-routes.mjs +3 -3
- package/server/routes/sessions-routes.mjs +670 -63
- package/server/server-runtime.mjs +476 -26
- package/server/services/about-service.mjs +2 -0
- package/server/services/background-work-view.mjs +2 -1
- package/server/services/claude-api-settings-service.mjs +575 -0
- package/server/services/claude-api-usage-service.mjs +361 -0
- package/server/services/claude-billing-swap-service.mjs +274 -0
- package/server/services/claude-billing-switch-service.mjs +343 -0
- package/server/services/claude-session-leftovers.mjs +226 -0
- package/server/services/deepseek-balance-service.mjs +102 -0
- package/server/services/deepseek-settings-service.mjs +380 -0
- package/server/services/deepseek-usage-import.mjs +471 -0
- package/server/services/model-variant-catalog-service.mjs +1 -0
- package/server/services/plan-usage-claude-api.mjs +445 -0
- package/server/services/plan-usage-contract.mjs +100 -5
- package/server/services/plan-usage-deepseek.mjs +931 -0
- package/server/services/plan-usage-service.mjs +436 -0
- package/server/services/provider-model-selection.mjs +4 -2
- package/server/services/provider-reasoning-effort.mjs +24 -2
- package/server/services/relay-ask-service.mjs +1 -1
- package/server/services/relay-event-service.mjs +3 -2
- package/server/services/relay-schedule-service.mjs +1 -1
- package/server/services/remote-relay-caller-context.mjs +7 -0
- package/server/services/remote-relay-dispatcher.mjs +84 -14
- package/server/services/remote-relay-registry-service.mjs +1 -1
- package/server/services/session-worker-launch-service.mjs +294 -84
- package/server/services/session-worker-registry-service.mjs +28 -1
- package/server/services/session-worker-websocket-service.mjs +17 -0
- package/server/services/usage-limit-pause-service.mjs +55 -4
- package/server/services/worker-secret-redaction.mjs +320 -0
- package/shared/anthropic-api.mjs +202 -0
- package/shared/context-window-fallbacks.mjs +3 -0
- package/shared/deepseek-api.mjs +137 -0
- package/shared/deepseek-models.mjs +114 -0
- package/shared/deepseek-pricing.mjs +146 -0
- package/shared/host-claude-session-env.mjs +101 -0
- package/shared/model-id.mjs +7 -0
- package/shared/preview-tool-core.mjs +1 -1
- package/shared/provider-routing.mjs +5 -3
- package/shared/relay-asks.mjs +11 -5
- package/shared/relay-events.mjs +2 -2
- package/shared/relay-schedules.mjs +2 -2
- package/shared/remote-relay-contract.mjs +29 -9
- package/shared/remote-relay-tool-core.mjs +23 -6
- package/shared/steer-settle-failure.mjs +2 -0
- package/shared/worker-runtime/api-client.mjs +2 -2
- package/shared/worker-runtime/direct-openai-image.mjs +128 -0
- package/shared/worker-runtime/heartbeat.mjs +5 -0
- package/shared/worker-runtime/worker-log-file.mjs +60 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +6 -0
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ On Windows, run `irm oar.sh/install.ps1 | iex` in PowerShell. Other ways to inst
|
|
|
17
17
|
|
|
18
18
|
## What OAR does
|
|
19
19
|
|
|
20
|
-
Coding agents such as GitHub Copilot, Claude Code, Cursor and
|
|
20
|
+
Coding agents such as GitHub Copilot, Claude Code, Cursor, Grok and DeepSeek usually live in a terminal. OAR puts them into a chat you can open anywhere: start a task at your desk, follow it from the sofa, answer the agent's question from your phone. The agents keep running on your own computer, with your files and your logins; OAR passes the conversation back and forth.
|
|
21
21
|
|
|
22
22
|
```mermaid
|
|
23
23
|
flowchart LR
|
|
@@ -27,7 +27,7 @@ flowchart LR
|
|
|
27
27
|
end
|
|
28
28
|
subgraph host["Your computer"]
|
|
29
29
|
relay["OAR relay<br/>chats, queue, history, files"]
|
|
30
|
-
agents["Copilot · OpenAI · Claude<br/>Cursor · Grok"]
|
|
30
|
+
agents["Copilot · OpenAI · Claude · Claude API<br/>Cursor · Grok · DeepSeek"]
|
|
31
31
|
end
|
|
32
32
|
cloud["☁️ Claude Cloud<br/>sandbox at Anthropic"]
|
|
33
33
|
phone <--> relay
|
|
@@ -36,7 +36,7 @@ flowchart LR
|
|
|
36
36
|
relay <--> cloud
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
- **
|
|
39
|
+
- **Eight runtimes, one chat**: you pick the agent per conversation, and all of them share the same chat, queue, history, file browser, and question cards ([Runtimes](#runtimes)).
|
|
40
40
|
- **Yours**: the relay runs on your hardware, answers only on this machine until you change that, and collects no telemetry ([Security notes](#security-notes)).
|
|
41
41
|
- **Under active development**: expect occasional rough edges, and some provider SDK features that are missing or incomplete.
|
|
42
42
|
|
|
@@ -45,29 +45,29 @@ flowchart LR
|
|
|
45
45
|
<table>
|
|
46
46
|
<tr>
|
|
47
47
|
<td width="42%" rowspan="4" valign="top">
|
|
48
|
-
<a href="https://oar.sh/assets/shots/cfmail-mobile.png?v=0.9.
|
|
48
|
+
<a href="https://oar.sh/assets/shots/cfmail-mobile.png?v=0.9.18"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-mobile.light.png?v=0.9.18"><img src="https://oar.sh/assets/shots/cfmail-mobile.png?v=0.9.18" alt="OAR on a phone: a finished agent turn in a conversation, with the composer below"></picture></a>
|
|
49
49
|
<br><sub>On a phone: the same conversations, installable as an app.</sub>
|
|
50
50
|
</td>
|
|
51
51
|
<td width="58%" valign="top">
|
|
52
|
-
<a href="https://oar.sh/assets/shots/cfmail-agents.png?v=0.9.
|
|
52
|
+
<a href="https://oar.sh/assets/shots/cfmail-agents.png?v=0.9.18"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-agents.light.png?v=0.9.18"><img src="https://oar.sh/assets/shots/cfmail-agents.png?v=0.9.18" alt="Background subagents running in parallel in the task panel, each with live token counts, elapsed time, and its own Stop button"></picture></a>
|
|
53
53
|
<br><sub>Agents working in parallel, each with live token counts and its own Stop.</sub>
|
|
54
54
|
</td>
|
|
55
55
|
</tr>
|
|
56
56
|
<tr>
|
|
57
57
|
<td valign="top">
|
|
58
|
-
<a href="https://oar.sh/assets/shots/cfmail-question.png?v=0.9.
|
|
58
|
+
<a href="https://oar.sh/assets/shots/cfmail-question.png?v=0.9.18"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-question.light.png?v=0.9.18"><img src="https://oar.sh/assets/shots/cfmail-question.png?v=0.9.18" alt="A question card in the middle of a turn: the agent offers several choices and a free-text reply"></picture></a>
|
|
59
59
|
<br><sub>When the agent needs a decision, it asks, and waits for your answer.</sub>
|
|
60
60
|
</td>
|
|
61
61
|
</tr>
|
|
62
62
|
<tr>
|
|
63
63
|
<td valign="top">
|
|
64
|
-
<a href="https://oar.sh/assets/shots/pingcf-thoughts.png?v=0.9.
|
|
64
|
+
<a href="https://oar.sh/assets/shots/pingcf-thoughts.png?v=0.9.18"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pingcf-thoughts.light.png?v=0.9.18"><img src="https://oar.sh/assets/shots/pingcf-thoughts.png?v=0.9.18" alt="An expanded Thoughts block showing the agent's reasoning above its reply"></picture></a>
|
|
65
65
|
<br><sub>Read the reasoning, not just the answer.</sub>
|
|
66
66
|
</td>
|
|
67
67
|
</tr>
|
|
68
68
|
<tr>
|
|
69
69
|
<td valign="top">
|
|
70
|
-
<a href="https://oar.sh/assets/shots/pagewright-watching.png?v=0.9.
|
|
70
|
+
<a href="https://oar.sh/assets/shots/pagewright-watching.png?v=0.9.18"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pagewright-watching.light.png?v=0.9.18"><img src="https://oar.sh/assets/shots/pagewright-watching.png?v=0.9.18" alt="A session watching two other sessions: a finished reply arrives as a compact relay-event row, the watching 2 sessions list is open with Unsubscribe, and a request to also watch a session on a paired relay waits on an Allow / Deny card"></picture></a>
|
|
71
71
|
<br><sub>Agents that watch agents: woken by events, approving nothing.</sub>
|
|
72
72
|
</td>
|
|
73
73
|
</tr>
|
|
@@ -102,9 +102,11 @@ The runtime (the *provider* in the UI) is chosen in **New Chat** and stays fixed
|
|
|
102
102
|
| [**OpenAI (BYOK)**](docs/providers.md#openai-byok-and-openai-image) | Copilot's agent against OpenAI or a compatible endpoint | Your own API key |
|
|
103
103
|
| [**OpenAI Image**](docs/providers.md#openai-byok-and-openai-image) | A chat whose replies are images | The same key |
|
|
104
104
|
| [**Claude**](docs/providers.md#claude-agent-sdk) | Claude Code on your machine | A Claude login, made from **⚙️ Settings** |
|
|
105
|
+
| [**Claude API**](docs/providers.md#claude-api) | Claude Code on your machine, billed to a Claude Console API key; a Claude chat can switch between the two | A Claude Console API key |
|
|
105
106
|
| [**Claude Cloud**](docs/providers.md#claude-cloud) (experimental) | Claude Code in a sandbox at Anthropic, on a GitHub repository | A claude.ai account with GitHub connected |
|
|
106
107
|
| [**Cursor**](docs/providers.md#cursor-agent-sdk) | Cursor's agent | A Cursor API key |
|
|
107
108
|
| [**Grok**](docs/providers.md#grok-cli-acp) | xAI's Grok CLI | A Grok sign-in, made from **⚙️ Settings** |
|
|
109
|
+
| [**DeepSeek**](docs/providers.md#deepseek) | Claude Code on your machine, thinking with DeepSeek's models | A DeepSeek API key |
|
|
108
110
|
|
|
109
111
|
API keys are entered in **⚙️ Settings** and stay on the relay host. The Claude and Grok CLIs can be [installed and signed in to from the browser](docs/providers.md#installing-a-provider-cli-from-the-relay), so no shell on the host is needed. Each runtime discovers its own [models](docs/providers.md#models). The full picture of each runtime is in [Providers](docs/providers.md).
|
|
110
112
|
|
|
@@ -116,11 +118,11 @@ API keys are entered in **⚙️ Settings** and stay on the relay host. The Clau
|
|
|
116
118
|
<table>
|
|
117
119
|
<tr>
|
|
118
120
|
<td width="62%" valign="top">
|
|
119
|
-
<a href="https://oar.sh/assets/shots/cfmail-steering.png?v=0.9.
|
|
121
|
+
<a href="https://oar.sh/assets/shots/cfmail-steering.png?v=0.9.18"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-steering.light.png?v=0.9.18"><img src="https://oar.sh/assets/shots/cfmail-steering.png?v=0.9.18" alt="A running Claude turn with a message steered into it mid-turn, the reply streaming with its tool calls, and the Steer button in the composer"></picture></a>
|
|
120
122
|
<br><sub>Send a message while the agent works, and it steers the running turn.</sub>
|
|
121
123
|
</td>
|
|
122
124
|
<td width="38%" valign="top">
|
|
123
|
-
<a href="https://oar.sh/assets/shots/usage-copilot-compact.png?v=0.9.
|
|
125
|
+
<a href="https://oar.sh/assets/shots/usage-copilot-compact.png?v=0.9.18"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/usage-copilot-compact.light.png?v=0.9.18"><img src="https://oar.sh/assets/shots/usage-copilot-compact.png?v=0.9.18" alt="The GitHub Copilot card in Check Usage: premium requests and plan quota with reset dates, and the cost of the last turn"></picture></a>
|
|
124
126
|
<br><sub>What each plan has left, in one place.</sub>
|
|
125
127
|
</td>
|
|
126
128
|
</tr>
|
|
@@ -132,12 +134,12 @@ API keys are entered in **⚙️ Settings** and stay on the relay host. The Clau
|
|
|
132
134
|
- **Background work**: a task panel lists the commands and sub-agents that outlive a reply, each with its own **Stop** and its full record; a command's console shows its output live, says when it has gone quiet and counts a test run's passes and failures; the panel and its consoles come back after a relay restart, and a queued restart shows what it waits for and asks after a maximum wait ([Background tasks](docs/web-ui.md#background-tasks), [After a relay restart](docs/web-ui.md#after-a-relay-restart)).
|
|
133
135
|
- **Choose how the agent works**: `ask`, `plan`, `agent` or `autopilot`, plus model and reasoning effort, per message ([Relay modes](docs/web-ui.md#relay-modes)).
|
|
134
136
|
- **Files and git**: browse the workspace, preview files, review git changes in a diff viewer, and publish a dev server as a preview link ([Files, git, and previews](docs/web-ui.md#files-git-and-previews), [preview servers](docs/preview-servers.md)).
|
|
135
|
-
- **Usage and context**: plan limits and credits of every provider, and what fills the model's context window ([Usage and context](docs/web-ui.md#usage-and-context)).
|
|
137
|
+
- **Usage and context**: plan limits and credits of every provider, the estimated spend of DeepSeek and Claude API keys, and what fills the model's context window ([Usage and context](docs/web-ui.md#usage-and-context)).
|
|
136
138
|
- **Your conversations everywhere**: the history lives on the relay, drafts follow you from device to device, and conversations can be searched, archived, grouped and pinned ([The conversation list](docs/web-ui.md#the-conversation-list), [drafts](docs/web-ui.md#drafts-across-devices), [session groups](docs/web-ui.md#session-groups), [pins](docs/web-ui.md#pinned-messages)).
|
|
137
139
|
- **Share** a conversation as a read-only link, hide single messages and the host's paths from it, and see when it was opened ([Sharing](docs/web-ui.md#sharing)).
|
|
138
140
|
- **On your phone**: install OAR as an app and get push notifications ([Notifications and the app](docs/web-ui.md#notifications-and-the-app)).
|
|
139
141
|
|
|
140
|
-
The full tour is in [Using the web UI](docs/web-ui.md); every setting and config key is in [Settings and configuration](docs/configuration.md). What changed in each release is in the [changelog](CHANGELOG.md), with [0.9.
|
|
142
|
+
The full tour is in [Using the web UI](docs/web-ui.md); every setting and config key is in [Settings and configuration](docs/configuration.md). What changed in each release is in the [changelog](CHANGELOG.md), with [0.9.18](CHANGELOG.md#0918--2026-10-26) as the latest.
|
|
141
143
|
|
|
142
144
|
## Remote access
|
|
143
145
|
|
|
@@ -188,7 +190,7 @@ flowchart LR
|
|
|
188
190
|
|
|
189
191
|
You stay in charge: per paired relay you decide whether agents may only read, also prompt, or do everything, and in the `ask` and `plan` modes every write asks you first. A subscription only notifies; it approves nothing.
|
|
190
192
|
|
|
191
|
-
<p><a href="https://oar.sh/assets/shots/pagewright-cross-relay.png?v=0.9.
|
|
193
|
+
<p><a href="https://oar.sh/assets/shots/pagewright-cross-relay.png?v=0.9.18"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pagewright-cross-relay.light.png?v=0.9.18"><img src="https://oar.sh/assets/shots/pagewright-cross-relay.png?v=0.9.18" alt="A session watching two sessions on a paired relay: a reply arrives as a relay-event row, and after a short outage the events the other relay kept arrive as one replayed row" width="720"></picture></a></p>
|
|
192
194
|
|
|
193
195
|
How pairing, permissions and limits work is in [Agents working together](docs/agents-together.md).
|
|
194
196
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oar-sh/oar",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.18",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OAR — Open Agent Relay: drive your local coding agents from any browser",
|
|
6
6
|
"license": "MIT",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"setup:git": "node scripts/setup-git.mjs",
|
|
50
50
|
"land": "node scripts/land.mjs",
|
|
51
51
|
"prune:local": "node scripts/prune-local.mjs",
|
|
52
|
+
"deepseek:import": "node server/services/deepseek-usage-import.mjs",
|
|
52
53
|
"test:e2e:raw": "node ./node_modules/@playwright/test/cli.js test --config tests/playwright.config.mjs"
|
|
53
54
|
},
|
|
54
55
|
"dependencies": {
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
// The Claude API backend of the Claude session runner: Claude Code on the
|
|
2
|
+
// Anthropic API, billed to the API key saved in Settings → Providers → Claude
|
|
3
|
+
// API instead of the host's Claude login.
|
|
4
|
+
//
|
|
5
|
+
// - The CLI's environment: the key as ANTHROPIC_API_KEY, the Anthropic API as
|
|
6
|
+
// ANTHROPIC_BASE_URL, and nothing of the host's Claude login, another
|
|
7
|
+
// Anthropic configuration or another provider's switch. The host's Claude
|
|
8
|
+
// Code state directory stays (CLAUDE_CONFIG_DIR is left as the relay set
|
|
9
|
+
// it): the chats are Claude chats, with the Claude provider's sessions,
|
|
10
|
+
// transcripts, resume and task output.
|
|
11
|
+
// - The flag settings layer: the user's settings and the chat folder's
|
|
12
|
+
// (.claude/settings.json, .claude/settings.local.json) can set `env`
|
|
13
|
+
// (another ANTHROPIC_BASE_URL, an ANTHROPIC_API_KEY that replaces the key,
|
|
14
|
+
// an ANTHROPIC_AUTH_TOKEN that beats it, a proxy, another provider, custom
|
|
15
|
+
// headers, a CA) and `apiKeyHelper` (a script whose output beats it). The
|
|
16
|
+
// flag layer outranks them all, so it pins the base URL and the key, blanks
|
|
17
|
+
// the bearer token and the helper, and holds every other variable that
|
|
18
|
+
// routes the requests or decides whom the CLI trusts at the CLI's own value
|
|
19
|
+
// (CLAUDE_CLI_PINNED_ENV, shared with the DeepSeek backend). The spawn's
|
|
20
|
+
// `--settings` argument, which other users of the host can read, carries
|
|
21
|
+
// the endpoint and the blanks (claudeApiFlagSettings); the key and the
|
|
22
|
+
// worker's own proxy and TLS values go into the same layer over the CLI's
|
|
23
|
+
// stdin before the first message (liveFlagSettings, claude-sdk-adapter.mjs;
|
|
24
|
+
// scripts/claude-api-cli-proof.mjs shows it).
|
|
25
|
+
// - The check: the CLI's `system/init` names where it took its API key from.
|
|
26
|
+
// Anything but ANTHROPIC_API_KEY stops the turn as soon as the init is read
|
|
27
|
+
// (checkInit). The CLI does not wait for that verdict: it sends its first
|
|
28
|
+
// request some 50 to 90 ms after the init (scripts/claude-api-cli-proof.mjs),
|
|
29
|
+
// so one request may already be out on the other credential when the stop
|
|
30
|
+
// lands. The check limits a wrong credential to that one request; it does
|
|
31
|
+
// not prevent it.
|
|
32
|
+
// - The model, effort, thinking and images: Claude's, exactly as the Claude
|
|
33
|
+
// worker has them (`claudeModels`).
|
|
34
|
+
// - What it reports: no Claude plan usage (there is no plan) and no Claude
|
|
35
|
+
// usage limit (the API answers with its own errors, which describeFailure
|
|
36
|
+
// reads in plain words).
|
|
37
|
+
//
|
|
38
|
+
// - Hooks: none run, neither the user's nor the chat folder's, and no
|
|
39
|
+
// statusLine command (CLAUDE_CLI_KEY_CHAT_SETTINGS, on the command line).
|
|
40
|
+
//
|
|
41
|
+
// The agent's shell inherits the CLI's environment, the key included; the
|
|
42
|
+
// relay takes the saved key out of what the worker posts and logs.
|
|
43
|
+
|
|
44
|
+
import { resolveAnthropicApiBaseUrl } from '../../shared/anthropic-api.mjs';
|
|
45
|
+
import { isHostClaudeSessionVariable } from '../../shared/host-claude-session-env.mjs';
|
|
46
|
+
import { CLAUDE_CLI_KEY_CHAT_SETTINGS, claudeCliPinnedEnv } from '../claude-worker/claude-cli-pinned-env.mjs';
|
|
47
|
+
|
|
48
|
+
export const CLAUDE_API_BACKEND_ID = 'claude-api';
|
|
49
|
+
export const CLAUDE_API_BACKEND_LABEL = 'Claude API';
|
|
50
|
+
// Where the relay's launch puts the key (session-worker-launch-service.mjs).
|
|
51
|
+
export const CLAUDE_API_KEY_ENV = 'OAR_CLAUDE_API_KEY';
|
|
52
|
+
// The stable code of a turn stopped because the CLI took another credential.
|
|
53
|
+
export const CLAUDE_API_KEY_SOURCE_CODE = 'claude-api-key-source';
|
|
54
|
+
// The only API key source the check lets through: the key in the CLI's
|
|
55
|
+
// environment, which is this backend's.
|
|
56
|
+
export const EXPECTED_API_KEY_SOURCE = 'ANTHROPIC_API_KEY';
|
|
57
|
+
// The sources the SDK's `system/init` can name (sdk.d.ts, ApiKeySource). A
|
|
58
|
+
// note names one of these, never a text of the CLI's own.
|
|
59
|
+
const KNOWN_API_KEY_SOURCES = Object.freeze([
|
|
60
|
+
'ANTHROPIC_API_KEY',
|
|
61
|
+
'apiKeyHelper',
|
|
62
|
+
'/login managed key',
|
|
63
|
+
'none',
|
|
64
|
+
'user',
|
|
65
|
+
'project',
|
|
66
|
+
'org',
|
|
67
|
+
'temporary',
|
|
68
|
+
'oauth',
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
const BILLING_URL = 'https://platform.claude.com/settings/billing';
|
|
72
|
+
const LIMITS_URL = 'https://platform.claude.com/settings/limits';
|
|
73
|
+
const SETTINGS_PLACE = 'Settings → Providers → Claude API';
|
|
74
|
+
const KEY_SOURCE_GUIDANCE = 'Remove the apiKeyHelper or other credential from the host\'s Claude Code settings '
|
|
75
|
+
+ `(settings.json), check the key in ${SETTINGS_PLACE}, then send the message again.`;
|
|
76
|
+
|
|
77
|
+
// Variables that would hand the CLI another credential or point it at another
|
|
78
|
+
// endpoint or cloud (the bundled CLI, 2026-10-25). Every ANTHROPIC_* is
|
|
79
|
+
// dropped as well (FOREIGN_CLI_PREFIXES).
|
|
80
|
+
const FOREIGN_CLI_VARIABLES = Object.freeze([
|
|
81
|
+
// The host's Claude login, and the other ways to hand the CLI a login or
|
|
82
|
+
// a key.
|
|
83
|
+
'CLAUDE_CODE_OAUTH_TOKEN',
|
|
84
|
+
'CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR',
|
|
85
|
+
'CLAUDE_CODE_OAUTH_REFRESH_TOKEN',
|
|
86
|
+
'CLAUDE_CODE_OAUTH_SCOPES',
|
|
87
|
+
'CLAUDE_CODE_OAUTH_CLIENT_ID',
|
|
88
|
+
'CLAUDE_CODE_CUSTOM_OAUTH_URL',
|
|
89
|
+
'CLAUDE_CODE_API_KEY_FILE_DESCRIPTOR',
|
|
90
|
+
'CLAUDE_CODE_API_KEY_HELPER_TTL_MS',
|
|
91
|
+
// Another provider for the CLI's requests, and their auth bypasses.
|
|
92
|
+
'CLAUDE_CODE_USE_BEDROCK',
|
|
93
|
+
'CLAUDE_CODE_USE_VERTEX',
|
|
94
|
+
'CLAUDE_CODE_USE_FOUNDRY',
|
|
95
|
+
'CLAUDE_CODE_USE_ANTHROPIC_AWS',
|
|
96
|
+
'CLAUDE_CODE_USE_ANTHROPIC_GOOGLE_CLOUD',
|
|
97
|
+
'CLAUDE_CODE_USE_GATEWAY',
|
|
98
|
+
'CLAUDE_CODE_USE_MANTLE',
|
|
99
|
+
'CLAUDE_CODE_USE_CCR_V2',
|
|
100
|
+
'CLAUDE_CODE_SKIP_BEDROCK_AUTH',
|
|
101
|
+
'CLAUDE_CODE_SKIP_VERTEX_AUTH',
|
|
102
|
+
'CLAUDE_CODE_SKIP_FOUNDRY_AUTH',
|
|
103
|
+
'CLAUDE_CODE_SKIP_MANTLE_AUTH',
|
|
104
|
+
'CLAUDE_CODE_SKIP_ANTHROPIC_AWS_AUTH',
|
|
105
|
+
'CLAUDE_CODE_SKIP_ANTHROPIC_GOOGLE_CLOUD_AUTH',
|
|
106
|
+
'CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST',
|
|
107
|
+
// A host's model and effort pins: the turn's model and effort come from
|
|
108
|
+
// the relay, which bills them to the key.
|
|
109
|
+
'CLAUDE_CODE_SUBAGENT_MODEL',
|
|
110
|
+
'CLAUDE_CODE_SUBAGENT_MODEL_FORCE',
|
|
111
|
+
'CLAUDE_CODE_WORKFLOW_SUBAGENT_MODEL',
|
|
112
|
+
'CLAUDE_CODE_EFFORT_LEVEL',
|
|
113
|
+
'CLAUDE_CODE_AUTO_COMPACT_WINDOW',
|
|
114
|
+
// Never set, and dropped when inherited: it forces the permission mode to
|
|
115
|
+
// default (plan mode is lost) and sandboxes Bash.
|
|
116
|
+
'CLAUDE_CODE_SUBPROCESS_ENV_SCRUB',
|
|
117
|
+
// What the relay hands its worker (session-worker-launch-service.mjs,
|
|
118
|
+
// TMUX_WORKER_EXPORTED_ENV_VARS): the worker's own settings, which neither
|
|
119
|
+
// Claude Code nor anything the agent runs reads. A shell of the agent's
|
|
120
|
+
// that starts a relay of its own would take the worker's kind, folders and
|
|
121
|
+
// tools file for its own. The relay's Claude configuration
|
|
122
|
+
// (CLAUDE_CONFIG_DIR, CLAUDE_CODE_EXECUTABLE) is not on it.
|
|
123
|
+
'COPILOT_ALLOW_ALL',
|
|
124
|
+
'GITHUB_COPILOT_PROMPT_MODE_EXTENSIONS',
|
|
125
|
+
'COPILOT_SDK_PATH',
|
|
126
|
+
'EXTENSION_PATH',
|
|
127
|
+
'COPILOT_MODEL',
|
|
128
|
+
'COPILOT_RELAY_MODEL',
|
|
129
|
+
'CLAUDE_RELAY_MODEL',
|
|
130
|
+
'CLAUDE_CLOUD_RELAY_MODEL',
|
|
131
|
+
'CLAUDE_API_RELAY_MODEL',
|
|
132
|
+
'CURSOR_RELAY_MODEL',
|
|
133
|
+
'CURSOR_AGENT_STORE_DIR',
|
|
134
|
+
'CURSOR_API_KEY',
|
|
135
|
+
'GROK_RELAY_MODEL',
|
|
136
|
+
'GROK_CLI_COMMAND',
|
|
137
|
+
'GROK_ALWAYS_APPROVE',
|
|
138
|
+
'SESSION_ID',
|
|
139
|
+
'COPILOT_WORKSPACE_ROOT',
|
|
140
|
+
'INIT_CWD',
|
|
141
|
+
]);
|
|
142
|
+
// The same by prefix: every Anthropic variable (the two the CLI needs are set
|
|
143
|
+
// again below), the relay's folders, config, tools file, worker kind and log
|
|
144
|
+
// file (COPILOT_WEB_RELAY_*), the Copilot provider settings, the DeepSeek
|
|
145
|
+
// worker's settings, every Claude gateway setting and the relay's own OAR_*
|
|
146
|
+
// switches (the key's own variable among them).
|
|
147
|
+
const FOREIGN_CLI_PREFIXES = Object.freeze([
|
|
148
|
+
'ANTHROPIC_',
|
|
149
|
+
'COPILOT_WEB_RELAY_',
|
|
150
|
+
'COPILOT_PROVIDER_',
|
|
151
|
+
'DEEPSEEK_',
|
|
152
|
+
'CLAUDE_GATEWAY_',
|
|
153
|
+
'OAR_',
|
|
154
|
+
]);
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Whether a variable of the worker's stays out of the CLI's environment:
|
|
158
|
+
* another credential, endpoint or provider, the relay's settings for its
|
|
159
|
+
* worker, or the host Claude Code session's variables when the relay was
|
|
160
|
+
* started from inside one (shared/host-claude-session-env.mjs; the launcher
|
|
161
|
+
* drops those for every worker, the backend again, however the worker was
|
|
162
|
+
* started).
|
|
163
|
+
*/
|
|
164
|
+
function isForeignCliVariable(key) {
|
|
165
|
+
const name = String(key).toUpperCase();
|
|
166
|
+
return FOREIGN_CLI_VARIABLES.includes(name)
|
|
167
|
+
|| FOREIGN_CLI_PREFIXES.some((prefix) => name.startsWith(prefix))
|
|
168
|
+
|| isHostClaudeSessionVariable(name);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function text(value) {
|
|
172
|
+
return String(value ?? '').trim();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The environment the CLI runs with: the worker's own without anything
|
|
177
|
+
* foreign, then the key and the endpoint. `OAR_ANTHROPIC_API_BASE_URL` names
|
|
178
|
+
* a fake API for tests; only a loopback URL is taken from it
|
|
179
|
+
* (resolveAnthropicApiBaseUrl), anything else is the Anthropic API.
|
|
180
|
+
*/
|
|
181
|
+
export function claudeApiCliEnv(baseEnv = process.env, { apiKey = '', baseUrl = '' } = {}) {
|
|
182
|
+
const key = text(apiKey);
|
|
183
|
+
if (!key) throw new Error('claude-api-key-not-configured');
|
|
184
|
+
const next = {};
|
|
185
|
+
for (const [name, value] of Object.entries(baseEnv || {})) {
|
|
186
|
+
if (isForeignCliVariable(name)) continue;
|
|
187
|
+
next[name] = value;
|
|
188
|
+
}
|
|
189
|
+
next.ANTHROPIC_API_KEY = key;
|
|
190
|
+
next.ANTHROPIC_BASE_URL = text(baseUrl) || resolveAnthropicApiBaseUrl(baseEnv || {});
|
|
191
|
+
return next;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The flag settings layer every CLI of this backend is spawned with, on its
|
|
196
|
+
* command line: no key in it. It outranks the user's, the project's and the
|
|
197
|
+
* local settings, so their `env` cannot send the requests elsewhere or swap
|
|
198
|
+
* the key for a bearer token, and their `apiKeyHelper` cannot swap it for
|
|
199
|
+
* its own (nor run at the CLI's startup). No hooks and no statusLine command
|
|
200
|
+
* run (CLAUDE_CLI_KEY_CHAT_SETTINGS). Skills, plugins and MCP servers are
|
|
201
|
+
* left as the other layers have them.
|
|
202
|
+
*/
|
|
203
|
+
export function claudeApiFlagSettings(baseUrl) {
|
|
204
|
+
return {
|
|
205
|
+
...CLAUDE_CLI_KEY_CHAT_SETTINGS,
|
|
206
|
+
env: {
|
|
207
|
+
ANTHROPIC_BASE_URL: text(baseUrl),
|
|
208
|
+
ANTHROPIC_AUTH_TOKEN: '',
|
|
209
|
+
},
|
|
210
|
+
apiKeyHelper: '',
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The same layer with the key, which the CLI takes over stdin before the
|
|
216
|
+
* first message (claude-sdk-adapter.mjs): a settings file's
|
|
217
|
+
* env.ANTHROPIC_API_KEY would otherwise replace the CLI's environment's.
|
|
218
|
+
* Every other variable that routes the CLI's requests, and with them the key,
|
|
219
|
+
* or decides whom it trusts on the way (the proxies, the other providers'
|
|
220
|
+
* switches and endpoints, the custom headers, the CA and TLS settings) holds
|
|
221
|
+
* the value of `cliEnv`, the environment the CLI was spawned with, or is
|
|
222
|
+
* blanked (claudeCliPinnedEnv).
|
|
223
|
+
*/
|
|
224
|
+
export function claudeApiLiveFlagSettings(baseUrl, apiKey, cliEnv = {}) {
|
|
225
|
+
const key = text(apiKey);
|
|
226
|
+
if (!key) throw new Error('claude-api-key-not-configured');
|
|
227
|
+
const settings = claudeApiFlagSettings(baseUrl);
|
|
228
|
+
return { ...settings, env: { ...claudeCliPinnedEnv(cliEnv), ...settings.env, ANTHROPIC_API_KEY: key } };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** The source the CLI named, as a note may quote it: a known name, or a stand-in. */
|
|
232
|
+
function quotedApiKeySource(value) {
|
|
233
|
+
const source = typeof value === 'string' ? value : '';
|
|
234
|
+
if (KNOWN_API_KEY_SOURCES.includes(source)) return `"${source}"`;
|
|
235
|
+
return source ? 'a source this relay does not know' : 'no source';
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Null when the CLI's `system/init` says it took the key from
|
|
240
|
+
* ANTHROPIC_API_KEY (the backend's), else the Error that stops the turn. Its
|
|
241
|
+
* message names the source the CLI reported, never a key. `afterTraffic`: the
|
|
242
|
+
* init came while the turn was already talking to the API, so one request
|
|
243
|
+
* may have gone out on the other credential.
|
|
244
|
+
*/
|
|
245
|
+
export function checkClaudeApiInit(sdkMessage, { afterTraffic = false } = {}) {
|
|
246
|
+
if (!sdkMessage || sdkMessage.type !== 'system' || sdkMessage.subtype !== 'init') return null;
|
|
247
|
+
const source = sdkMessage.apiKeySource;
|
|
248
|
+
if (source === EXPECTED_API_KEY_SOURCE) return null;
|
|
249
|
+
const error = new Error(
|
|
250
|
+
`Claude Code reported ${quotedApiKeySource(source)} as its API key source instead of the saved Claude API key, so the turn was stopped.`
|
|
251
|
+
+ (afterTraffic ? ' At most one request may have gone out on that credential.' : ''),
|
|
252
|
+
);
|
|
253
|
+
error.code = CLAUDE_API_KEY_SOURCE_CODE;
|
|
254
|
+
error.apiKeySource = KNOWN_API_KEY_SOURCES.includes(source) ? source : null;
|
|
255
|
+
error.guidance = KEY_SOURCE_GUIDANCE;
|
|
256
|
+
return error;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* `{ code, guidance }` for a failed request the Claude API has its own advice
|
|
261
|
+
* for, or null. Read from the error the runner stopped the turn with, the
|
|
262
|
+
* HTTP status the result reports, else from the status and the words the CLI
|
|
263
|
+
* quotes in its error text ("API Error: 401 …"). The account refusals come
|
|
264
|
+
* first: a spend limit can come as a 429, which is not a rate limit.
|
|
265
|
+
*/
|
|
266
|
+
export function describeClaudeApiFailure(errorText, { status = null, error = null } = {}) {
|
|
267
|
+
if (error?.code === CLAUDE_API_KEY_SOURCE_CODE) {
|
|
268
|
+
return { code: CLAUDE_API_KEY_SOURCE_CODE, guidance: error.guidance || KEY_SOURCE_GUIDANCE };
|
|
269
|
+
}
|
|
270
|
+
const message = text(errorText);
|
|
271
|
+
const code = Number(status);
|
|
272
|
+
if (/credit balance is too low/i.test(message)) {
|
|
273
|
+
return {
|
|
274
|
+
code: 'claude-api-credit',
|
|
275
|
+
guidance: `The Claude API account's credit balance is too low. Add credits in the Claude Console (${BILLING_URL}), then send the message again.`,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
if (/API usage limits|enforced_spend_limit_reached/i.test(message)) {
|
|
279
|
+
return {
|
|
280
|
+
code: 'claude-api-spend-limit',
|
|
281
|
+
guidance: `The Claude API account has reached its spend limit. Raise the limit in the Claude Console (${LIMITS_URL}) or wait until it resets, then send the message again.`,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
if (code === 402 || /\b402\b|billing_error/i.test(message)) {
|
|
285
|
+
return {
|
|
286
|
+
code: 'claude-api-billing',
|
|
287
|
+
guidance: `The Claude API refused the request for a billing reason. Check the payment details in the Claude Console (${BILLING_URL}), then send the message again.`,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
if (
|
|
291
|
+
code === 401
|
|
292
|
+
|| code === 403
|
|
293
|
+
|| /\b40[13]\b|authentication_error|permission_error|invalid x-api-key|invalid api key/i.test(message)
|
|
294
|
+
) {
|
|
295
|
+
return {
|
|
296
|
+
code: 'claude-api-key',
|
|
297
|
+
guidance: `The Claude API did not accept the API key. Check it in ${SETTINGS_PLACE}, then send the message again.`,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
if (code === 429 || code === 529 || /\b(?:429|529)\b|rate_limit_error|overloaded_error/i.test(message)) {
|
|
301
|
+
return {
|
|
302
|
+
code: 'claude-api-rate-limit',
|
|
303
|
+
guidance: `The Claude API is rate-limiting this key or is overloaded. Send the message again in a moment; the key's rate limits are in the Claude Console (${LIMITS_URL}).`,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function createClaudeApiClaudeBackend({ env = process.env } = {}) {
|
|
310
|
+
const apiKey = text(env[CLAUDE_API_KEY_ENV]);
|
|
311
|
+
const baseUrl = resolveAnthropicApiBaseUrl(env);
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
id: CLAUDE_API_BACKEND_ID,
|
|
315
|
+
providerType: CLAUDE_API_BACKEND_ID,
|
|
316
|
+
label: CLAUDE_API_BACKEND_LABEL,
|
|
317
|
+
// Claude's models, effort, thinking, `[1m]`, images and sub-agent models,
|
|
318
|
+
// exactly as the Claude worker has them: the runner and the adapter take
|
|
319
|
+
// none of those from this backend.
|
|
320
|
+
claudeModels: true,
|
|
321
|
+
// No plan windows and no subscription usage limit on an API key, and no
|
|
322
|
+
// usage route of its own.
|
|
323
|
+
reportsPlanUsage: false,
|
|
324
|
+
reportsUsageLimit: false,
|
|
325
|
+
usageRoute: null,
|
|
326
|
+
describeFailure: describeClaudeApiFailure,
|
|
327
|
+
checkInit: checkClaudeApiInit,
|
|
328
|
+
/** The settings layer pin, merged into the spawn's `settings` (no key in it). */
|
|
329
|
+
flagSettings: () => claudeApiFlagSettings(baseUrl),
|
|
330
|
+
/**
|
|
331
|
+
* The layer again with the key and the routing and trust pins for a CLI
|
|
332
|
+
* spawned with `cliEnv`, over stdin before the first message.
|
|
333
|
+
*/
|
|
334
|
+
liveFlagSettings: (cliEnv = claudeApiCliEnv(env, { apiKey, baseUrl })) => claudeApiLiveFlagSettings(baseUrl, apiKey, cliEnv),
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* The CLI's environment, built from the worker's own (which must not
|
|
338
|
+
* reach the CLI as it is: it carries OAR_CLAUDE_API_KEY and possibly the
|
|
339
|
+
* host's login or Anthropic settings).
|
|
340
|
+
*/
|
|
341
|
+
spawnEnv(baseEnv = process.env) {
|
|
342
|
+
return claudeApiCliEnv(baseEnv, { apiKey, baseUrl });
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Claude API session worker: the Claude session worker with its CLI on the
|
|
3
|
+
// Anthropic API and the key saved in Settings → Providers → Claude API
|
|
4
|
+
// (claude-api-backend.mjs). The relay's launch puts the key in
|
|
5
|
+
// OAR_CLAUDE_API_KEY (through the owner-only secret file under tmux) and the
|
|
6
|
+
// chat's model in CLAUDE_API_RELAY_MODEL. The CLI keeps the host's Claude
|
|
7
|
+
// Code state directory: the chats are Claude chats, resumed and read where
|
|
8
|
+
// the Claude worker reads them.
|
|
9
|
+
import { installWorkerLogFile, installWorkerLogRedaction } from '../../shared/worker-runtime/worker-log-file.mjs';
|
|
10
|
+
import { runClaudeSessionWorker } from '../claude-worker/claude-session-worker-main.mjs';
|
|
11
|
+
import { CLAUDE_API_KEY_ENV, createClaudeApiClaudeBackend } from './claude-api-backend.mjs';
|
|
12
|
+
|
|
13
|
+
installWorkerLogFile();
|
|
14
|
+
// The key never shows in this worker's console or log, whatever the CLI or
|
|
15
|
+
// the agent prints (installed after the log file, so its copy is redacted too).
|
|
16
|
+
installWorkerLogRedaction({ secrets: [process.env[CLAUDE_API_KEY_ENV]] });
|
|
17
|
+
|
|
18
|
+
const backend = createClaudeApiClaudeBackend({ env: process.env });
|
|
19
|
+
|
|
20
|
+
runClaudeSessionWorker({
|
|
21
|
+
logTag: 'claude-api-worker',
|
|
22
|
+
workerName: 'claude-api-session-worker',
|
|
23
|
+
// None set: the CLI's own default, as for a Claude chat without a model.
|
|
24
|
+
defaultModel: String(process.env.CLAUDE_API_RELAY_MODEL || '').trim(),
|
|
25
|
+
backend,
|
|
26
|
+
}).catch((error) => {
|
|
27
|
+
console.error('claude-api-session-worker fatal:', error?.stack || error?.message || error);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
});
|
|
@@ -52,6 +52,9 @@ function imageBlockFromDataUrl(att, maxInlineImageBytes) {
|
|
|
52
52
|
export function buildClaudeAttachmentContent(rawAttachments, {
|
|
53
53
|
fsImpl = fs,
|
|
54
54
|
maxInlineImageBytes = MAX_INLINE_IMAGE_BYTES,
|
|
55
|
+
// False for a model that takes no image input: an image block would fail
|
|
56
|
+
// the whole request, so every image goes by its path instead.
|
|
57
|
+
embedImages = true,
|
|
55
58
|
} = {}) {
|
|
56
59
|
const input = Array.isArray(rawAttachments) ? rawAttachments : [];
|
|
57
60
|
const imageBlocks = [];
|
|
@@ -67,7 +70,7 @@ export function buildClaudeAttachmentContent(rawAttachments, {
|
|
|
67
70
|
// Only media types the Anthropic image block accepts are embedded;
|
|
68
71
|
// everything else (svg, bmp, heic, …) would 400 the whole turn and
|
|
69
72
|
// falls back to a path reference instead.
|
|
70
|
-
if (filePath && isEmbeddableImageMediaType(mime) && fsImpl.existsSync(filePath)) {
|
|
73
|
+
if (embedImages && filePath && isEmbeddableImageMediaType(mime) && fsImpl.existsSync(filePath)) {
|
|
71
74
|
try {
|
|
72
75
|
const bytes = fsImpl.readFileSync(filePath);
|
|
73
76
|
if (Buffer.isBuffer(bytes) && bytes.length && bytes.length <= maxInlineImageBytes) {
|
|
@@ -85,7 +88,7 @@ export function buildClaudeAttachmentContent(rawAttachments, {
|
|
|
85
88
|
embedded = false;
|
|
86
89
|
}
|
|
87
90
|
}
|
|
88
|
-
if (!embedded) {
|
|
91
|
+
if (!embedded && embedImages) {
|
|
89
92
|
const blockFromDataUrl = imageBlockFromDataUrl(att, maxInlineImageBytes);
|
|
90
93
|
if (blockFromDataUrl) {
|
|
91
94
|
imageBlocks.push(blockFromDataUrl);
|
|
@@ -118,9 +121,9 @@ export function buildClaudeAttachmentContent(rawAttachments, {
|
|
|
118
121
|
* Text first (user text + attachment notes + server-provided attachment
|
|
119
122
|
* context), then inline image blocks.
|
|
120
123
|
*/
|
|
121
|
-
export function buildClaudeUserContent(message, { fsImpl = fs, maxInlineImageBytes = MAX_INLINE_IMAGE_BYTES } = {}) {
|
|
124
|
+
export function buildClaudeUserContent(message, { fsImpl = fs, maxInlineImageBytes = MAX_INLINE_IMAGE_BYTES, embedImages = true } = {}) {
|
|
122
125
|
const text = String(message?.text || '').trim();
|
|
123
|
-
const { imageBlocks, noteLines } = buildClaudeAttachmentContent(message?.attachments, { fsImpl, maxInlineImageBytes });
|
|
126
|
+
const { imageBlocks, noteLines } = buildClaudeAttachmentContent(message?.attachments, { fsImpl, maxInlineImageBytes, embedImages });
|
|
124
127
|
const attachmentPromptContext = String(message?.attachmentPromptContext || '').trim();
|
|
125
128
|
const textParts = [text];
|
|
126
129
|
if (noteLines.length) textParts.push(noteLines.join('\n'));
|