@oar-sh/oar 0.9.16 → 0.9.17
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/CHANGELOG.md +220 -0
- package/README.md +12 -11
- package/package.json +2 -1
- package/server/claude-worker/claude-attachments.mjs +7 -4
- package/server/claude-worker/claude-sdk-adapter.mjs +167 -9
- package/server/claude-worker/claude-session-process.mjs +127 -29
- package/server/claude-worker/claude-session-worker-main.mjs +266 -0
- package/server/claude-worker/claude-session-worker.mjs +2 -253
- package/server/claude-worker/claude-turn-failure.mjs +8 -0
- package/server/claude-worker/claude-turn-publisher.mjs +282 -13
- package/server/claude-worker/claude-usage-totals.mjs +108 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +55 -0
- package/server/copilot-worker/copilot-sdk-adapter.mjs +18 -4
- package/server/copilot-worker/copilot-sdk-session-process.mjs +71 -5
- package/server/db-schema.mjs +63 -0
- package/server/deepseek-worker/deepseek-claude-backend.mjs +433 -0
- package/server/deepseek-worker/deepseek-session-worker.mjs +37 -0
- 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/public/app/api-client.js +53 -0
- package/server/public/app/bootstrap.js +101 -13
- package/server/public/app/context-tier-options.mjs +4 -0
- package/server/public/app/conversation-provider-indicator.mjs +7 -0
- package/server/public/app/conversation-view.js +2 -0
- package/server/public/app/deepseek-settings-ui.js +342 -0
- package/server/public/app/inbox-ui.js +14 -3
- package/server/public/app/journal-view.js +31 -3
- package/server/public/app/model-selector-options.mjs +11 -0
- package/server/public/app/model-vision-support.mjs +2 -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 +4 -1
- package/server/public/app/relay-error-ctas.mjs +8 -0
- package/server/public/app/settings-modal.js +11 -0
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +10 -0
- package/server/public/index.html +408 -33
- package/server/repositories/conversation-group-repository.mjs +13 -6
- package/server/routes/conversation-group-routes.mjs +2 -1
- package/server/routes/deepseek-usage-import-routes.mjs +72 -0
- package/server/routes/messages-routes.mjs +192 -15
- package/server/routes/sessions-routes.mjs +364 -55
- package/server/server-runtime.mjs +123 -10
- package/server/services/about-service.mjs +1 -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-contract.mjs +100 -5
- package/server/services/plan-usage-deepseek.mjs +931 -0
- package/server/services/plan-usage-service.mjs +431 -0
- package/server/services/provider-model-selection.mjs +1 -0
- package/server/services/provider-reasoning-effort.mjs +15 -2
- package/server/services/remote-relay-dispatcher.mjs +2 -1
- package/server/services/session-worker-launch-service.mjs +233 -82
- package/server/services/worker-secret-redaction.mjs +318 -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 +2 -2
- package/shared/remote-relay-contract.mjs +1 -1
- package/shared/steer-settle-failure.mjs +1 -0
- package/shared/worker-runtime/direct-openai-image.mjs +128 -0
- package/shared/worker-runtime/worker-log-file.mjs +60 -0
|
@@ -114,6 +114,11 @@ import { shouldEmitStreamUpdate } from '../../shared/stream-emit-gating.mjs';
|
|
|
114
114
|
import { withSteerNote } from '../../shared/steer-note.mjs';
|
|
115
115
|
import { isRelayAutomatedDelivery } from '../../shared/relay-events.mjs';
|
|
116
116
|
import { isTransientRelayError } from '../../shared/worker-runtime/relay-errors.mjs';
|
|
117
|
+
import {
|
|
118
|
+
isOpenAIImageTerminalError,
|
|
119
|
+
runDirectOpenAIImageTurn,
|
|
120
|
+
shouldUseDirectOpenAIImageApi,
|
|
121
|
+
} from '../../shared/worker-runtime/direct-openai-image.mjs';
|
|
117
122
|
import {
|
|
118
123
|
createTurnLiveness,
|
|
119
124
|
createTurnStalledError,
|
|
@@ -556,6 +561,12 @@ export function createCopilotSdkSessionRunner({
|
|
|
556
561
|
// terminal error until the publish lands, so recovery can never re-run them
|
|
557
562
|
// (see publishSettleMarker).
|
|
558
563
|
const settlingMessages = new Map();
|
|
564
|
+
// Image-model rows running on the relay's image routes (id → message). They
|
|
565
|
+
// never reach the runtime, so no turn owns them; the heartbeat and the crash
|
|
566
|
+
// guard still have to.
|
|
567
|
+
const directImageMessages = new Map();
|
|
568
|
+
// `openai` on a BYOK worker; the failure notes name the provider.
|
|
569
|
+
const workerProvider = String(env.COPILOT_PROVIDER_TYPE || '').trim().toLowerCase();
|
|
559
570
|
// Settle markers that could not be posted even terminally; the heartbeat
|
|
560
571
|
// hands them to the relay to fail (id → terminalError).
|
|
561
572
|
const settleFailedTerminals = new Map();
|
|
@@ -3728,7 +3739,7 @@ export function createCopilotSdkSessionRunner({
|
|
|
3728
3739
|
// afterwards: a runtime that died or went silent under a Stop used to put
|
|
3729
3740
|
// the watchdog's failure on every row instead.
|
|
3730
3741
|
if (failure && !turn.aborted) {
|
|
3731
|
-
const classified = classifyCopilotTurnException(failure);
|
|
3742
|
+
const classified = classifyCopilotTurnException(failure, { provider: workerProvider });
|
|
3732
3743
|
dbg('copilot turn failed', turn.message?.id || '(continuation)', classified.detail);
|
|
3733
3744
|
const running = turn.currentOwner || turn.primaryEntry;
|
|
3734
3745
|
for (const entry of pending) {
|
|
@@ -3747,7 +3758,7 @@ export function createCopilotSdkSessionRunner({
|
|
|
3747
3758
|
return;
|
|
3748
3759
|
}
|
|
3749
3760
|
if (result?.isError) {
|
|
3750
|
-
const classified = classifyCopilotSessionError(result.errorData || { message: result.errorMessage });
|
|
3761
|
+
const classified = classifyCopilotSessionError(result.errorData || { message: result.errorMessage }, { provider: workerProvider });
|
|
3751
3762
|
dbg('turn failed', turn.message?.id || '(continuation)', classified.stableCode);
|
|
3752
3763
|
const running = turn.currentOwner || turn.primaryEntry;
|
|
3753
3764
|
for (const entry of pending) {
|
|
@@ -4117,6 +4128,7 @@ export function createCopilotSdkSessionRunner({
|
|
|
4117
4128
|
dbg('continuation output dropped (no relay row)');
|
|
4118
4129
|
const classified = classifyCopilotTurnException(
|
|
4119
4130
|
turn.failure || new Error('the relay refused a continuation row for this turn'),
|
|
4131
|
+
{ provider: workerProvider },
|
|
4120
4132
|
);
|
|
4121
4133
|
for (const entry of turn.entries) {
|
|
4122
4134
|
if (entry.settled || entry.turn !== turn) continue;
|
|
@@ -4821,6 +4833,9 @@ export function createCopilotSdkSessionRunner({
|
|
|
4821
4833
|
async function handlePendingPayload(pending) {
|
|
4822
4834
|
const message = pending?.message || null;
|
|
4823
4835
|
if (!message) return false;
|
|
4836
|
+
// An image model is no chat model: its row goes to the relay's image
|
|
4837
|
+
// routes, whatever the runtime is doing, and never into the runtime.
|
|
4838
|
+
if (shouldUseDirectOpenAIImageApi(message)) return runDirectImageTurn(message);
|
|
4824
4839
|
// Held (a card is open, a compaction is running, a tool of the main agent
|
|
4825
4840
|
// is running, the turn-opening prompt is not sent yet): hand the row back
|
|
4826
4841
|
// with no retry penalty; the relay re-delivers it when the hold ends. An
|
|
@@ -4850,7 +4865,7 @@ export function createCopilotSdkSessionRunner({
|
|
|
4850
4865
|
if (outcome !== NOT_STEERED) return outcome;
|
|
4851
4866
|
} catch (error) {
|
|
4852
4867
|
dbg('steering failed', message.id, error?.message || String(error));
|
|
4853
|
-
const classified = classifyCopilotTurnException(error);
|
|
4868
|
+
const classified = classifyCopilotTurnException(error, { provider: workerProvider });
|
|
4854
4869
|
await publishResponse(message, {
|
|
4855
4870
|
text: classified.text,
|
|
4856
4871
|
model: null,
|
|
@@ -4863,6 +4878,54 @@ export function createCopilotSdkSessionRunner({
|
|
|
4863
4878
|
return runTurn(turn);
|
|
4864
4879
|
}
|
|
4865
4880
|
|
|
4881
|
+
/**
|
|
4882
|
+
* One image-model row (shared/worker-runtime/direct-openai-image.mjs). With
|
|
4883
|
+
* an image operation the relay's execute route publishes the reply, the
|
|
4884
|
+
* failure or the requeue itself; without one the shared helper publishes
|
|
4885
|
+
* the reply, and a refusal by the provider is published here as the row's
|
|
4886
|
+
* failure. Anything else requeues the row, as on the extension engine.
|
|
4887
|
+
*/
|
|
4888
|
+
async function runDirectImageTurn(message) {
|
|
4889
|
+
const id = String(message.id || '');
|
|
4890
|
+
directImageMessages.set(id, message);
|
|
4891
|
+
touch();
|
|
4892
|
+
try {
|
|
4893
|
+
const result = await runDirectOpenAIImageTurn({
|
|
4894
|
+
api,
|
|
4895
|
+
message,
|
|
4896
|
+
// An edit can take minutes, longer than fetch waits for headers.
|
|
4897
|
+
requestOptions: { longCall: true },
|
|
4898
|
+
responseFields: attemptFields(message),
|
|
4899
|
+
});
|
|
4900
|
+
dbg('image turn', message.id, result.via, result.outcome);
|
|
4901
|
+
} catch (error) {
|
|
4902
|
+
if (isOpenAIImageTerminalError(error)) {
|
|
4903
|
+
dbg('image turn refused by OpenAI', message.id, error.message);
|
|
4904
|
+
await publishResponse(message, {
|
|
4905
|
+
text: error.message,
|
|
4906
|
+
model: null,
|
|
4907
|
+
terminalError: {
|
|
4908
|
+
kind: 'openai-image-failed',
|
|
4909
|
+
// The code the relay's image operation uses for the same refusal.
|
|
4910
|
+
code: 'image-provider-rejected',
|
|
4911
|
+
stableCode: 'image-provider-rejected',
|
|
4912
|
+
message: error.message,
|
|
4913
|
+
...(error.detail ? { detail: String(error.detail) } : {}),
|
|
4914
|
+
failedAt: new Date().toISOString(),
|
|
4915
|
+
queueMessageId: id || null,
|
|
4916
|
+
},
|
|
4917
|
+
});
|
|
4918
|
+
} else {
|
|
4919
|
+
dbg('image turn failed; requeueing', message.id, error?.message || String(error));
|
|
4920
|
+
await api('POST', '/api/requeue', { messageId: message.id, ...attemptFields(message) }).catch(() => {});
|
|
4921
|
+
}
|
|
4922
|
+
} finally {
|
|
4923
|
+
directImageMessages.delete(id);
|
|
4924
|
+
touch();
|
|
4925
|
+
}
|
|
4926
|
+
return true;
|
|
4927
|
+
}
|
|
4928
|
+
|
|
4866
4929
|
// --------------------------------------------------------------- teardown --
|
|
4867
4930
|
|
|
4868
4931
|
function getActiveQueueMessageId() {
|
|
@@ -4872,6 +4935,7 @@ export function createCopilotSdkSessionRunner({
|
|
|
4872
4935
|
const id = String(entry.message?.id || '');
|
|
4873
4936
|
if (id && !entry.released) return id;
|
|
4874
4937
|
}
|
|
4938
|
+
for (const id of directImageMessages.keys()) return id;
|
|
4875
4939
|
return '';
|
|
4876
4940
|
}
|
|
4877
4941
|
|
|
@@ -4911,6 +4975,7 @@ export function createCopilotSdkSessionRunner({
|
|
|
4911
4975
|
for (const { message, variant } of settlingMessages.values()) {
|
|
4912
4976
|
push(message, buildSteerSettleFailure(message, { variant, agentLabel: SETTLE_AGENT_LABEL }));
|
|
4913
4977
|
}
|
|
4978
|
+
for (const message of directImageMessages.values()) push(message);
|
|
4914
4979
|
return entries;
|
|
4915
4980
|
}
|
|
4916
4981
|
|
|
@@ -4949,8 +5014,9 @@ export function createCopilotSdkSessionRunner({
|
|
|
4949
5014
|
// registry and refuses ids it does not know.
|
|
4950
5015
|
stopBackgroundTask,
|
|
4951
5016
|
// "Active" spans both ownerships: a settled turn still publishing must
|
|
4952
|
-
// keep the worker's idle/shutdown gates closed just like a running one
|
|
4953
|
-
|
|
5017
|
+
// keep the worker's idle/shutdown gates closed just like a running one,
|
|
5018
|
+
// and so must an image row on the relay's image routes.
|
|
5019
|
+
isTurnActive: () => !!activeTurn || publishingTurns.size > 0 || directImageMessages.size > 0,
|
|
4954
5020
|
dispose,
|
|
4955
5021
|
// The turn's tokens/cost/TTFT, as posted to `/api/copilot-plan-usage`.
|
|
4956
5022
|
getLastTurnUsage: () => lastTurnUsage,
|
package/server/db-schema.mjs
CHANGED
|
@@ -29,6 +29,13 @@ import { migrateConversationMayCreateSessions } from './migrations/0020-conversa
|
|
|
29
29
|
import { migrateConversationGroupAgentMarks } from './migrations/0021-conversation-group-agent-marks.mjs';
|
|
30
30
|
import { migrateRelaySchedules } from './migrations/0022-relay-schedules.mjs';
|
|
31
31
|
import { migrateRelayAsks } from './migrations/0023-relay-asks.mjs';
|
|
32
|
+
import { migrateDeepSeekUsageTotals } from './migrations/0024-deepseek-usage-totals.mjs';
|
|
33
|
+
import { migrateDeepSeekUsageRequestsDaily } from './migrations/0025-deepseek-usage-requests-daily.mjs';
|
|
34
|
+
import { migrateDeepSeekUsageServedModels } from './migrations/0026-deepseek-usage-served-models.mjs';
|
|
35
|
+
import { migrateDeepSeekUsageCheckpointRuns } from './migrations/0027-deepseek-usage-checkpoint-runs.mjs';
|
|
36
|
+
import { migrateDeepSeekUsageCheckpointRequestRuns } from './migrations/0028-deepseek-usage-checkpoint-request-runs.mjs';
|
|
37
|
+
import { migrateDeepSeekBalanceRises } from './migrations/0029-deepseek-balance-rises.mjs';
|
|
38
|
+
import { migrateDeepSeekUsageActuals } from './migrations/0030-deepseek-usage-actuals.mjs';
|
|
32
39
|
import { migrateImageConversationSchema } from './repositories/image-conversation-repository.mjs';
|
|
33
40
|
|
|
34
41
|
// Mirrors DEFAULT_RELAY_MODE in server-runtime.mjs; used only by the one-time
|
|
@@ -907,6 +914,62 @@ try {
|
|
|
907
914
|
console.warn(`[relay-asks] migration 0023 failed; retrying on next boot: ${error?.message || error}`);
|
|
908
915
|
}
|
|
909
916
|
|
|
917
|
+
// DeepSeek usage totals: one new table. The plan usage service creates it
|
|
918
|
+
// itself as well, so a failure here is logged and retried next boot like 0023.
|
|
919
|
+
try {
|
|
920
|
+
migrateDeepSeekUsageTotals(db);
|
|
921
|
+
} catch (error) {
|
|
922
|
+
console.warn(`[deepseek-usage] migration 0024 failed; retrying on next boot: ${error?.message || error}`);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// DeepSeek search/fetch counts, daily spend and balance reference: four
|
|
926
|
+
// columns on 0024's table and two tables, handled like 0024.
|
|
927
|
+
try {
|
|
928
|
+
migrateDeepSeekUsageRequestsDaily(db);
|
|
929
|
+
} catch (error) {
|
|
930
|
+
console.warn(`[deepseek-usage] migration 0025 failed; retrying on next boot: ${error?.message || error}`);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// DeepSeek usage booked under a Claude model name is folded into the DeepSeek
|
|
934
|
+
// model that served it, handled like 0024.
|
|
935
|
+
try {
|
|
936
|
+
migrateDeepSeekUsageServedModels(db);
|
|
937
|
+
} catch (error) {
|
|
938
|
+
console.warn(`[deepseek-usage] migration 0026 failed; retrying on next boot: ${error?.message || error}`);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// The DeepSeek usage checkpoint learns the worker process's run id and its
|
|
942
|
+
// search and fetch counts: three columns on 0024's table, handled like 0024.
|
|
943
|
+
try {
|
|
944
|
+
migrateDeepSeekUsageCheckpointRuns(db);
|
|
945
|
+
} catch (error) {
|
|
946
|
+
console.warn(`[deepseek-usage] migration 0027 failed; retrying on next boot: ${error?.message || error}`);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// The checkpoint's search and fetch counts get a run id of their own: one
|
|
950
|
+
// column on 0024's table, handled like 0024.
|
|
951
|
+
try {
|
|
952
|
+
migrateDeepSeekUsageCheckpointRequestRuns(db);
|
|
953
|
+
} catch (error) {
|
|
954
|
+
console.warn(`[deepseek-usage] migration 0028 failed; retrying on next boot: ${error?.message || error}`);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// The balance reference notes when the balance last rose: one column on
|
|
958
|
+
// 0025's table, handled like 0025.
|
|
959
|
+
try {
|
|
960
|
+
migrateDeepSeekBalanceRises(db);
|
|
961
|
+
} catch (error) {
|
|
962
|
+
console.warn(`[deepseek-usage] migration 0029 failed; retrying on next boot: ${error?.message || error}`);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// The imported figures of DeepSeek's usage export: a table of its own, handled
|
|
966
|
+
// like 0024.
|
|
967
|
+
try {
|
|
968
|
+
migrateDeepSeekUsageActuals(db);
|
|
969
|
+
} catch (error) {
|
|
970
|
+
console.warn(`[deepseek-usage] migration 0030 failed; retrying on next boot: ${error?.message || error}`);
|
|
971
|
+
}
|
|
972
|
+
|
|
910
973
|
const runtimeSessionColumns = db.prepare(`PRAGMA table_info(runtime_sessions)`).all().map((c) => c.name);
|
|
911
974
|
if (runtimeSessionColumns.length) {
|
|
912
975
|
if (!runtimeSessionColumns.includes('strategy')) {
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
// The DeepSeek backend of the Claude session runner: everything that differs
|
|
2
|
+
// when the CLI the runner drives talks to DeepSeek's Anthropic-compatible
|
|
3
|
+
// endpoint instead of Anthropic.
|
|
4
|
+
//
|
|
5
|
+
// - The CLI's environment: the endpoint and the key as Claude Code's
|
|
6
|
+
// documented ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN, a Claude Code state
|
|
7
|
+
// directory of its own, no nonessential traffic, and nothing of the host's
|
|
8
|
+
// Claude login or of another Anthropic configuration. The endpoint, the
|
|
9
|
+
// key and everything else that routes the CLI's requests or decides whom
|
|
10
|
+
// it trusts (DEEPSEEK_CLI_PINNED_ENV: provider switches, credentials,
|
|
11
|
+
// proxies, TLS) are pinned again in the CLI's flag settings
|
|
12
|
+
// (liveFlagSettings), over the settings of the chat's folder.
|
|
13
|
+
// - The model: DeepSeek's ids, with Claude Code's `[1m]` suffix so the CLI
|
|
14
|
+
// plans with the 1M window; helper calls (titles, summaries) go to flash.
|
|
15
|
+
// Claude Code's model aliases are pinned to the session's model
|
|
16
|
+
// (ANTHROPIC_MODEL, ANTHROPIC_DEFAULT_OPUS/SONNET/FABLE_MODEL,
|
|
17
|
+
// CLAUDE_CODE_SUBAGENT_MODEL, CLAUDE_CODE_WORKFLOW_SUBAGENT_MODEL), so a
|
|
18
|
+
// sub-agent asked for as "opus" runs on it and not, by DeepSeek's mapping
|
|
19
|
+
// of a Claude id, on V4 Pro; a model switch respawns the CLI for the pins.
|
|
20
|
+
// A Claude id the CLI still names (a session spawned before the pins) is
|
|
21
|
+
// read as the DeepSeek model DeepSeek serves it with (deepseekServedModel).
|
|
22
|
+
// - The effort: DeepSeek's low/high/max, which Claude Code sends as
|
|
23
|
+
// `output_config.effort` with adaptive thinking (captured 2026-10-24). It
|
|
24
|
+
// is applied at spawn (CLAUDE_CODE_EFFORT_LEVEL), so a change respawns the
|
|
25
|
+
// CLI between turns on the same session. There is no thinking-off level:
|
|
26
|
+
// Claude Code never sends `thinking: {type: disabled}` for these models.
|
|
27
|
+
// - What it reports: no Claude plan usage and no Claude usage limit, but the
|
|
28
|
+
// CLI's token totals per model after each turn (POST
|
|
29
|
+
// /api/deepseek-plan-usage) for the DeepSeek card's estimated spend; images
|
|
30
|
+
// only to models that take them; DeepSeek's errors in its own words.
|
|
31
|
+
//
|
|
32
|
+
// Source for the endpoint and the variables: DeepSeek's Claude Code guide
|
|
33
|
+
// (api-docs.deepseek.com, quick_start/agent_integrations/claude_code).
|
|
34
|
+
|
|
35
|
+
import { deepseekAnthropicBaseUrl } from '../../shared/deepseek-api.mjs';
|
|
36
|
+
import { isHostClaudeSessionVariable } from '../../shared/host-claude-session-env.mjs';
|
|
37
|
+
import {
|
|
38
|
+
DEEPSEEK_PROVIDER_TYPE,
|
|
39
|
+
DEFAULT_DEEPSEEK_MODEL,
|
|
40
|
+
deepseekModelAcceptsImages,
|
|
41
|
+
deepseekReasoningEffortFor,
|
|
42
|
+
deepseekServedModel,
|
|
43
|
+
isDeepSeekModelId,
|
|
44
|
+
} from '../../shared/deepseek-models.mjs';
|
|
45
|
+
|
|
46
|
+
const LONG_CONTEXT_SUFFIX = '[1m]';
|
|
47
|
+
const LONG_CONTEXT_SUFFIX_PATTERN = /\[1m\]$/i;
|
|
48
|
+
// Claude Code's helper model; DeepSeek's guide points it at flash.
|
|
49
|
+
const HELPER_MODEL = 'deepseek-flash';
|
|
50
|
+
// Claude Code's alias for its helper model. The other aliases ("opus",
|
|
51
|
+
// "sonnet", "fable", and a sub-agent without a model) resolve to the pins
|
|
52
|
+
// spawnEnv sets, that is to the session's model.
|
|
53
|
+
const HELPER_ALIAS = 'haiku';
|
|
54
|
+
// Where Claude Code reads the model behind each of its aliases, and the model
|
|
55
|
+
// of sub-agents and workflow agents (the bundled CLI, 2026-10-24). All pinned
|
|
56
|
+
// to the session's model; CLAUDE_CODE_SUBAGENT_MODEL_FORCE is left unset, so
|
|
57
|
+
// a sub-agent asked for as "haiku" still runs on the helper model.
|
|
58
|
+
const SESSION_MODEL_VARIABLES = Object.freeze([
|
|
59
|
+
'ANTHROPIC_MODEL',
|
|
60
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
61
|
+
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
62
|
+
'ANTHROPIC_DEFAULT_FABLE_MODEL',
|
|
63
|
+
'CLAUDE_CODE_SUBAGENT_MODEL',
|
|
64
|
+
'CLAUDE_CODE_WORKFLOW_SUBAGENT_MODEL',
|
|
65
|
+
]);
|
|
66
|
+
const HELPER_MODEL_VARIABLES = Object.freeze(['ANTHROPIC_DEFAULT_HAIKU_MODEL', 'ANTHROPIC_SMALL_FAST_MODEL']);
|
|
67
|
+
|
|
68
|
+
// Variables that would point the CLI at another Anthropic configuration or
|
|
69
|
+
// hand it another credential. Every ANTHROPIC_* is dropped as well.
|
|
70
|
+
const FOREIGN_CLI_VARIABLES = Object.freeze([
|
|
71
|
+
'CLAUDE_CODE_OAUTH_TOKEN',
|
|
72
|
+
'CLAUDE_CODE_USE_BEDROCK',
|
|
73
|
+
'CLAUDE_CODE_USE_VERTEX',
|
|
74
|
+
'CLAUDE_CODE_USE_FOUNDRY',
|
|
75
|
+
'CLAUDE_CODE_USE_ANTHROPIC_AWS',
|
|
76
|
+
'CLAUDE_CODE_USE_ANTHROPIC_GOOGLE_CLOUD',
|
|
77
|
+
'CLAUDE_CODE_USE_MANTLE',
|
|
78
|
+
'CLAUDE_CODE_USE_GATEWAY',
|
|
79
|
+
'CLAUDE_CODE_SKIP_BEDROCK_AUTH',
|
|
80
|
+
'CLAUDE_CODE_SKIP_VERTEX_AUTH',
|
|
81
|
+
'CLAUDE_CODE_SKIP_FOUNDRY_AUTH',
|
|
82
|
+
'CLAUDE_CODE_SKIP_ANTHROPIC_AWS_AUTH',
|
|
83
|
+
'CLAUDE_CODE_SKIP_ANTHROPIC_GOOGLE_CLOUD_AUTH',
|
|
84
|
+
'CLAUDE_CODE_SKIP_MANTLE_AUTH',
|
|
85
|
+
'CLAUDE_CODE_SUBAGENT_MODEL',
|
|
86
|
+
'CLAUDE_CODE_SUBAGENT_MODEL_FORCE',
|
|
87
|
+
'CLAUDE_CODE_WORKFLOW_SUBAGENT_MODEL',
|
|
88
|
+
'CLAUDE_CODE_EFFORT_LEVEL',
|
|
89
|
+
'CLAUDE_CODE_AUTO_COMPACT_WINDOW',
|
|
90
|
+
// Claude Code's subprocess env scrub, if the host exports it: it forces the
|
|
91
|
+
// permission mode to default (a chat in plan mode could edit files) and
|
|
92
|
+
// runs every Bash command in a bubblewrap sandbox.
|
|
93
|
+
'CLAUDE_CODE_SUBPROCESS_ENV_SCRUB',
|
|
94
|
+
// What the relay hands its worker (session-worker-launch-service.mjs,
|
|
95
|
+
// TMUX_WORKER_EXPORTED_ENV_VARS): the worker's own settings, which neither
|
|
96
|
+
// Claude Code nor anything the agent runs reads. A shell of the agent's
|
|
97
|
+
// that starts a relay of its own would take the worker's kind, folders and
|
|
98
|
+
// tools file for its own. The relay's Claude configuration
|
|
99
|
+
// (CLAUDE_CONFIG_DIR, replaced below; CLAUDE_CODE_EXECUTABLE) is not on it.
|
|
100
|
+
'COPILOT_ALLOW_ALL',
|
|
101
|
+
'GITHUB_COPILOT_PROMPT_MODE_EXTENSIONS',
|
|
102
|
+
'COPILOT_SDK_PATH',
|
|
103
|
+
'EXTENSION_PATH',
|
|
104
|
+
'COPILOT_MODEL',
|
|
105
|
+
'COPILOT_RELAY_MODEL',
|
|
106
|
+
'CLAUDE_RELAY_MODEL',
|
|
107
|
+
'CLAUDE_CLOUD_RELAY_MODEL',
|
|
108
|
+
'CURSOR_RELAY_MODEL',
|
|
109
|
+
'CURSOR_AGENT_STORE_DIR',
|
|
110
|
+
'GROK_RELAY_MODEL',
|
|
111
|
+
'GROK_CLI_COMMAND',
|
|
112
|
+
'GROK_ALWAYS_APPROVE',
|
|
113
|
+
'SESSION_ID',
|
|
114
|
+
'COPILOT_WORKSPACE_ROOT',
|
|
115
|
+
'INIT_CWD',
|
|
116
|
+
]);
|
|
117
|
+
// The same by prefix: the relay's folders, config, tools file, worker kind
|
|
118
|
+
// and log file (COPILOT_WEB_RELAY_*), the Copilot provider settings, every
|
|
119
|
+
// DeepSeek setting of the worker's (the key included: the CLI gets it as
|
|
120
|
+
// ANTHROPIC_AUTH_TOKEN) and the relay's own OAR_* switches.
|
|
121
|
+
const FOREIGN_CLI_PREFIXES = Object.freeze(['ANTHROPIC_', 'COPILOT_WEB_RELAY_', 'COPILOT_PROVIDER_', 'DEEPSEEK_', 'OAR_']);
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Every variable through which the env of the chat folder's settings
|
|
125
|
+
* (.claude/settings.json, settings.local.json) could send the CLI's requests,
|
|
126
|
+
* and with them the key, somewhere else, or change whom the CLI trusts on the
|
|
127
|
+
* way: the endpoints, the provider switches, the credentials, the proxies and
|
|
128
|
+
* the TLS trust. liveFlagSettings pins each one in the flag settings layer,
|
|
129
|
+
* which the CLI applies over the folder's, to the value the CLI was spawned
|
|
130
|
+
* with: the worker's own, else the other case's for a proxy variable, else ''.
|
|
131
|
+
* '' is unset to the CLI for each of these (truthy or trimmed reads), except
|
|
132
|
+
* SSL_CERT_FILE and SSL_CERT_DIR: '' empties OpenSSL's and Node's system
|
|
133
|
+
* store, so those two are pinned only when the worker sets them.
|
|
134
|
+
*
|
|
135
|
+
* Source: the names Claude Code 2.1.304 (the bundled CLI) reads, by a grep
|
|
136
|
+
* of the binary; the CLI's own lists of endpoints (with their selection
|
|
137
|
+
* switches and companions) and credentials are in it as they are there. A
|
|
138
|
+
* deny-list, because a settings layer can only pin names known in advance.
|
|
139
|
+
* Left out, each for its reason: the model variables (same endpoint, same
|
|
140
|
+
* key), what the CLI already drops from a folder's settings or from all
|
|
141
|
+
* settings (CLAUDE_CONFIG_DIR, CLAUDE_CODE_CUSTOM_OAUTH_URL, the OTEL
|
|
142
|
+
* exporter endpoints, ANTHROPIC_UNIX_SOCKET, the host-managed provider
|
|
143
|
+
* switches, the file-descriptor credentials), a provider's own SDK settings
|
|
144
|
+
* (AWS_*, GOOGLE_*, AZURE_*, read only for the provider the switches here
|
|
145
|
+
* turn off), NODE_OPTIONS (at runtime the CLI reads only its system-CA
|
|
146
|
+
* flags, which add no CA), and what routes only other programs' traffic
|
|
147
|
+
* (NPM_CONFIG_*, GIT_*, the resolver's variables).
|
|
148
|
+
*/
|
|
149
|
+
export const DEEPSEEK_CLI_PINNED_ENV = Object.freeze([
|
|
150
|
+
// Endpoints: the CLI's list, its fallback and the headers sent with them.
|
|
151
|
+
'ANTHROPIC_BASE_URL',
|
|
152
|
+
'_CLAUDE_CODE_ASSUME_FIRST_PARTY_BASE_URL',
|
|
153
|
+
'ANTHROPIC_CUSTOM_HEADERS',
|
|
154
|
+
'ANTHROPIC_BEDROCK_BASE_URL',
|
|
155
|
+
'ANTHROPIC_VERTEX_BASE_URL',
|
|
156
|
+
'ANTHROPIC_FOUNDRY_BASE_URL',
|
|
157
|
+
'ANTHROPIC_AWS_BASE_URL',
|
|
158
|
+
'ANTHROPIC_GOOGLE_CLOUD_BASE_URL',
|
|
159
|
+
'ANTHROPIC_BEDROCK_MANTLE_BASE_URL',
|
|
160
|
+
'CLAUDE_CODE_API_BASE_URL',
|
|
161
|
+
'CLAUDE_CODE_ARTIFACTS_API_BASE_URL',
|
|
162
|
+
'CLAUDE_CODE_ARTIFACTS_API_TOKEN',
|
|
163
|
+
'CLAUDE_CODE_ARTIFACT_ASSET_BASE_URL',
|
|
164
|
+
'CLAUDE_CODE_ARTIFACT_LIVE_BASE_URL',
|
|
165
|
+
'CLAUDE_CODE_ARTIFACT_SYNC_BASE_URL',
|
|
166
|
+
'CLAUDE_CODE_ARTIFACT_VIEWER_BASE_URL',
|
|
167
|
+
'CLAUDE_CODE_MEMORY_API_BASE_URL',
|
|
168
|
+
'CLAUDE_CODE_MEMORY_API_TOKEN',
|
|
169
|
+
// Provider switches.
|
|
170
|
+
'CLAUDE_CODE_USE_BEDROCK',
|
|
171
|
+
'CLAUDE_CODE_USE_VERTEX',
|
|
172
|
+
'CLAUDE_CODE_USE_FOUNDRY',
|
|
173
|
+
'CLAUDE_CODE_USE_ANTHROPIC_AWS',
|
|
174
|
+
'CLAUDE_CODE_USE_ANTHROPIC_GOOGLE_CLOUD',
|
|
175
|
+
'CLAUDE_CODE_USE_MANTLE',
|
|
176
|
+
'CLAUDE_CODE_USE_GATEWAY',
|
|
177
|
+
'CLAUDE_CODE_SKIP_BEDROCK_AUTH',
|
|
178
|
+
'CLAUDE_CODE_SKIP_VERTEX_AUTH',
|
|
179
|
+
'CLAUDE_CODE_SKIP_FOUNDRY_AUTH',
|
|
180
|
+
'CLAUDE_CODE_SKIP_ANTHROPIC_AWS_AUTH',
|
|
181
|
+
'CLAUDE_CODE_SKIP_ANTHROPIC_GOOGLE_CLOUD_AUTH',
|
|
182
|
+
'CLAUDE_CODE_SKIP_MANTLE_AUTH',
|
|
183
|
+
// Credentials: the CLI's list, the OAuth login, the federation profile and
|
|
184
|
+
// the proxy credential helper.
|
|
185
|
+
'ANTHROPIC_API_KEY',
|
|
186
|
+
'ANTHROPIC_AUTH_TOKEN',
|
|
187
|
+
'CLAUDE_CODE_OAUTH_TOKEN',
|
|
188
|
+
'AWS_BEARER_TOKEN_BEDROCK',
|
|
189
|
+
'ANTHROPIC_FOUNDRY_API_KEY',
|
|
190
|
+
'ANTHROPIC_FOUNDRY_AUTH_TOKEN',
|
|
191
|
+
'ANTHROPIC_AWS_API_KEY',
|
|
192
|
+
'CLAUDE_CODE_OAUTH_REFRESH_TOKEN',
|
|
193
|
+
'CLAUDE_CODE_OAUTH_SCOPES',
|
|
194
|
+
'CLAUDE_CODE_OAUTH_CLIENT_ID',
|
|
195
|
+
'ANTHROPIC_PROFILE',
|
|
196
|
+
'ANTHROPIC_IDENTITY_TOKEN',
|
|
197
|
+
'ANTHROPIC_IDENTITY_TOKEN_FILE',
|
|
198
|
+
'ANTHROPIC_FEDERATION_RULE_ID',
|
|
199
|
+
'ANTHROPIC_ORGANIZATION_ID',
|
|
200
|
+
'ANTHROPIC_SERVICE_ACCOUNT_ID',
|
|
201
|
+
'ANTHROPIC_SCOPE',
|
|
202
|
+
'ANTHROPIC_WORKSPACE_ID',
|
|
203
|
+
'CLAUDE_CODE_ENABLE_PROXY_AUTH_HELPER',
|
|
204
|
+
// Proxies: the CLI reads both cases, the lower one first.
|
|
205
|
+
'HTTPS_PROXY',
|
|
206
|
+
'https_proxy',
|
|
207
|
+
'HTTP_PROXY',
|
|
208
|
+
'http_proxy',
|
|
209
|
+
'ALL_PROXY',
|
|
210
|
+
'all_proxy',
|
|
211
|
+
'NO_PROXY',
|
|
212
|
+
'no_proxy',
|
|
213
|
+
'CLAUDE_CODE_HTTPS_PROXY',
|
|
214
|
+
'CLAUDE_CODE_HTTP_PROXY',
|
|
215
|
+
// TLS trust and the client certificate.
|
|
216
|
+
'NODE_TLS_REJECT_UNAUTHORIZED',
|
|
217
|
+
'NODE_EXTRA_CA_CERTS',
|
|
218
|
+
'CLAUDE_CODE_CERT_STORE',
|
|
219
|
+
'CLAUDE_CODE_CLIENT_CERT',
|
|
220
|
+
'CLAUDE_CODE_CLIENT_KEY',
|
|
221
|
+
'CLAUDE_CODE_CLIENT_KEY_PASSPHRASE',
|
|
222
|
+
'SSL_CERT_FILE',
|
|
223
|
+
'SSL_CERT_DIR',
|
|
224
|
+
]);
|
|
225
|
+
// The two of DEEPSEEK_CLI_PINNED_ENV for which '' is not unset (see there).
|
|
226
|
+
const PINNED_ONLY_WHEN_SET = new Set(['SSL_CERT_FILE', 'SSL_CERT_DIR']);
|
|
227
|
+
// The proxy variables the CLI and the tools it runs read in either case.
|
|
228
|
+
const OTHER_CASE = new Map(
|
|
229
|
+
['HTTPS_PROXY', 'HTTP_PROXY', 'ALL_PROXY', 'NO_PROXY'].flatMap((name) => [
|
|
230
|
+
[name, name.toLowerCase()],
|
|
231
|
+
[name.toLowerCase(), name],
|
|
232
|
+
]),
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* The pins of DEEPSEEK_CLI_PINNED_ENV for a CLI spawned with `cliEnv`: the
|
|
237
|
+
* value it was spawned with, else the other case's (a proxy the worker sets
|
|
238
|
+
* as https_proxy holds for HTTPS_PROXY as well), else ''.
|
|
239
|
+
*/
|
|
240
|
+
export function deepseekPinnedEnv(cliEnv = {}) {
|
|
241
|
+
const pins = {};
|
|
242
|
+
for (const name of DEEPSEEK_CLI_PINNED_ENV) {
|
|
243
|
+
const own = cliEnv[name] ?? cliEnv[OTHER_CASE.get(name)];
|
|
244
|
+
if (own !== undefined) pins[name] = String(own);
|
|
245
|
+
else if (!PINNED_ONLY_WHEN_SET.has(name)) pins[name] = '';
|
|
246
|
+
}
|
|
247
|
+
return pins;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Whether a variable of the worker's stays out of the CLI's environment:
|
|
252
|
+
* another Anthropic configuration or credential, the relay's settings for
|
|
253
|
+
* its worker, or the host Claude Code session's variables when the relay was
|
|
254
|
+
* started from inside one (shared/host-claude-session-env.mjs; the launcher
|
|
255
|
+
* drops those for every worker, the backend again, so a CLI talking to
|
|
256
|
+
* DeepSeek never reads itself as a child of the host's session, however the
|
|
257
|
+
* worker was started).
|
|
258
|
+
*/
|
|
259
|
+
function isForeignCliVariable(key) {
|
|
260
|
+
return FOREIGN_CLI_VARIABLES.includes(key)
|
|
261
|
+
|| FOREIGN_CLI_PREFIXES.some((prefix) => key.startsWith(prefix))
|
|
262
|
+
|| isHostClaudeSessionVariable(key);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function text(value) {
|
|
266
|
+
return String(value ?? '').trim();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function withoutSuffix(value) {
|
|
270
|
+
return text(value).replace(LONG_CONTEXT_SUFFIX_PATTERN, '');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* The relay's id of a model the CLI names: "deepseek-flash[1m]" →
|
|
275
|
+
* "deepseek-flash"; a Claude id ("claude-opus-5-5", from a CLI spawned
|
|
276
|
+
* without the alias pins) → the DeepSeek model that served it; anything else
|
|
277
|
+
* as given, without the suffix.
|
|
278
|
+
*/
|
|
279
|
+
export function deepseekRelayModel(value) {
|
|
280
|
+
return deepseekServedModel(value) || withoutSuffix(value);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** The id the CLI gets: the relay's id with the 1M suffix. */
|
|
284
|
+
export function deepseekCliModel(value) {
|
|
285
|
+
const model = deepseekRelayModel(value);
|
|
286
|
+
return model ? `${model}${LONG_CONTEXT_SUFFIX}` : '';
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* The DeepSeek model a sub-agent runs on, from what the runner knows of it:
|
|
291
|
+
* the model its own frames name (a DeepSeek id, or a Claude id DeepSeek
|
|
292
|
+
* served), else the alias the spawning tool pinned ("haiku" is the helper
|
|
293
|
+
* model, every other alias the session's model), else the session's model.
|
|
294
|
+
*/
|
|
295
|
+
export function deepseekSubagentModel(value, sessionModel = '') {
|
|
296
|
+
const served = deepseekServedModel(value);
|
|
297
|
+
if (served) return served;
|
|
298
|
+
if (withoutSuffix(value).toLowerCase() === HELPER_ALIAS) return HELPER_MODEL;
|
|
299
|
+
return deepseekRelayModel(sessionModel);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** The session model a CLI is pinned to, in the CLI's terms; unknown or none is the default model. */
|
|
303
|
+
function pinnedCliModel(model) {
|
|
304
|
+
const relayModel = deepseekRelayModel(model);
|
|
305
|
+
return deepseekCliModel(isDeepSeekModelId(relayModel) ? relayModel : DEFAULT_DEEPSEEK_MODEL);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* `{ code, guidance }` for a failed request DeepSeek has its own advice for,
|
|
310
|
+
* or null. Read from the HTTP status the result reports, else from the
|
|
311
|
+
* status the CLI quotes in its error text ("API Error: 402 …").
|
|
312
|
+
*/
|
|
313
|
+
export function describeDeepSeekFailure(errorText, { status = null } = {}) {
|
|
314
|
+
const message = text(errorText);
|
|
315
|
+
const code = Number(status);
|
|
316
|
+
if (code === 402 || /\b402\b|insufficient[ _]balance/i.test(message)) {
|
|
317
|
+
return {
|
|
318
|
+
code: 'deepseek-balance',
|
|
319
|
+
guidance: 'The DeepSeek account balance is used up. Top it up at https://platform.deepseek.com/top_up, then send the message again.',
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
if (code === 401 || /\b401\b|authentication[_ ]error|invalid api key/i.test(message)) {
|
|
323
|
+
return {
|
|
324
|
+
code: 'deepseek-api-key',
|
|
325
|
+
guidance: 'DeepSeek did not accept the API key. Check it in Settings → Providers → DeepSeek, then send the message again.',
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
if (code === 429 || /\b429\b|rate[_ ]limit/i.test(message)) {
|
|
329
|
+
return {
|
|
330
|
+
code: 'deepseek-rate-limit',
|
|
331
|
+
guidance: 'DeepSeek is at its concurrency limit for this account. Send the message again in a moment.',
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function createDeepSeekClaudeBackend({ env = process.env } = {}) {
|
|
338
|
+
const apiKey = text(env.DEEPSEEK_API_KEY);
|
|
339
|
+
const anthropicBaseUrl = deepseekAnthropicBaseUrl(text(env.DEEPSEEK_BASE_URL));
|
|
340
|
+
const configDir = text(env.DEEPSEEK_CLAUDE_CONFIG_DIR);
|
|
341
|
+
|
|
342
|
+
/** DeepSeek's level for a requested effort; unset and unknown are high. */
|
|
343
|
+
function effortFor(value) {
|
|
344
|
+
return deepseekReasoningEffortFor(value) || 'high';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* The CLI's environment. Built from the worker's own (which must not reach
|
|
349
|
+
* the CLI as it is: it carries DEEPSEEK_API_KEY and possibly the host's
|
|
350
|
+
* Anthropic settings), so every Anthropic variable is replaced. `model` is
|
|
351
|
+
* the session's model, which every alias is pinned to.
|
|
352
|
+
*/
|
|
353
|
+
function spawnEnv(baseEnv = process.env, { effort = '', model = '' } = {}) {
|
|
354
|
+
if (!apiKey) throw new Error('deepseek-api-key-not-configured');
|
|
355
|
+
if (!configDir) throw new Error('deepseek-config-dir-not-configured');
|
|
356
|
+
const next = {};
|
|
357
|
+
for (const [key, value] of Object.entries(baseEnv || {})) {
|
|
358
|
+
if (isForeignCliVariable(key)) continue;
|
|
359
|
+
next[key] = value;
|
|
360
|
+
}
|
|
361
|
+
next.ANTHROPIC_BASE_URL = anthropicBaseUrl;
|
|
362
|
+
next.ANTHROPIC_AUTH_TOKEN = apiKey;
|
|
363
|
+
// Every alias Claude Code resolves on its own ("opus", a sub-agent
|
|
364
|
+
// without a model) lands on the session's model: without a pin the CLI
|
|
365
|
+
// sends a Claude id, which DeepSeek serves by its own mapping
|
|
366
|
+
// (claude-opus → V4 Pro), off the model the chat is locked to.
|
|
367
|
+
const sessionModel = pinnedCliModel(model);
|
|
368
|
+
for (const key of SESSION_MODEL_VARIABLES) next[key] = sessionModel;
|
|
369
|
+
for (const key of HELPER_MODEL_VARIABLES) next[key] = HELPER_MODEL;
|
|
370
|
+
next.CLAUDE_CONFIG_DIR = configDir;
|
|
371
|
+
next.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = '1';
|
|
372
|
+
next.CLAUDE_CODE_EFFORT_LEVEL = effortFor(effort);
|
|
373
|
+
return next;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
id: DEEPSEEK_PROVIDER_TYPE,
|
|
378
|
+
providerType: DEEPSEEK_PROVIDER_TYPE,
|
|
379
|
+
label: 'DeepSeek',
|
|
380
|
+
configDir,
|
|
381
|
+
defaultModel: DEFAULT_DEEPSEEK_MODEL,
|
|
382
|
+
// Neither is DeepSeek's: Claude's plan windows and its usage limit.
|
|
383
|
+
reportsPlanUsage: false,
|
|
384
|
+
reportsUsageLimit: false,
|
|
385
|
+
// What it reports instead: the CLI's token totals per model, by the
|
|
386
|
+
// relay's model ids (relayModel); the relay books what each report adds.
|
|
387
|
+
// Claude Code's `total_cost_usd` is at Claude's prices and is not sent.
|
|
388
|
+
usageRoute: '/api/deepseek-plan-usage',
|
|
389
|
+
|
|
390
|
+
// A Claude id is not a model of DeepSeek's to request, only one it may
|
|
391
|
+
// have served (relayModel).
|
|
392
|
+
isModel: (value) => isDeepSeekModelId(withoutSuffix(value)),
|
|
393
|
+
cliModel: deepseekCliModel,
|
|
394
|
+
relayModel: deepseekRelayModel,
|
|
395
|
+
subagentModel: deepseekSubagentModel,
|
|
396
|
+
acceptsImages: (model) => deepseekModelAcceptsImages(deepseekRelayModel(model)),
|
|
397
|
+
describeFailure: describeDeepSeekFailure,
|
|
398
|
+
|
|
399
|
+
effortFor,
|
|
400
|
+
/** The SDK's `effort` option: DeepSeek's level. */
|
|
401
|
+
effortOption: effortFor,
|
|
402
|
+
/** DeepSeek always thinks (see the effort note above). */
|
|
403
|
+
thinkingEnabled: () => true,
|
|
404
|
+
/**
|
|
405
|
+
* What only a fresh CLI takes, the effort and the model the aliases are
|
|
406
|
+
* pinned to: a change respawns it between turns. `model` is the turn's
|
|
407
|
+
* resolved model (the runner's, not the message's alone).
|
|
408
|
+
*/
|
|
409
|
+
spawnKey: (message = {}, { model = '' } = {}) => `${effortFor(message.reasoningEffort)} ${pinnedCliModel(model)}`,
|
|
410
|
+
|
|
411
|
+
spawnEnv,
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* The CLI's routing, trust and credentials once more, in its flag
|
|
415
|
+
* settings layer, which outranks the settings of the chat's folder: a
|
|
416
|
+
* .claude/settings.json or settings.local.json there whose
|
|
417
|
+
* env.ANTHROPIC_BASE_URL, proxy or CA points elsewhere would otherwise
|
|
418
|
+
* get the DeepSeek key as its bearer token (scripts/deepseek-cli-proof.mjs).
|
|
419
|
+
* Every variable of DEEPSEEK_CLI_PINNED_ENV holds the value of `cliEnv`,
|
|
420
|
+
* the environment the CLI was spawned with (spawnEnv), or is blanked, and
|
|
421
|
+
* so is an apiKeyHelper of the folder's. The key may not go on the
|
|
422
|
+
* command line, so the adapter sends these over the control channel
|
|
423
|
+
* before the first message (startClaudeSession).
|
|
424
|
+
*/
|
|
425
|
+
liveFlagSettings(cliEnv = spawnEnv(env)) {
|
|
426
|
+
if (!apiKey) throw new Error('deepseek-api-key-not-configured');
|
|
427
|
+
return {
|
|
428
|
+
env: { ...deepseekPinnedEnv(cliEnv), ANTHROPIC_BASE_URL: anthropicBaseUrl, ANTHROPIC_AUTH_TOKEN: apiKey },
|
|
429
|
+
apiKeyHelper: '',
|
|
430
|
+
};
|
|
431
|
+
},
|
|
432
|
+
};
|
|
433
|
+
}
|