@oar-sh/oar 0.9.17 → 0.9.19
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/server-lifecycle/managed-server.mjs +13 -1
- package/.github/extensions/web-relay/skills/tool-activity.mjs +5 -4
- package/CHANGELOG.md +234 -0
- package/README.md +13 -12
- package/package.json +1 -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-cli-pinned-env.mjs +150 -0
- package/server/claude-worker/claude-remote-relay-tool.mjs +5 -5
- package/server/claude-worker/claude-sdk-adapter.mjs +35 -9
- package/server/claude-worker/claude-session-process.mjs +166 -21
- package/server/claude-worker/claude-session-worker-main.mjs +3 -0
- package/server/claude-worker/claude-subscription-key-check.mjs +64 -0
- package/server/claude-worker/claude-turn-publisher.mjs +117 -5
- package/server/claude-worker/claude-usage-totals.mjs +53 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +1 -1
- package/server/copilot-worker/copilot-remote-relay-tool.mjs +2 -2
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +1 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +9 -9
- 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 +33 -1
- package/server/deepseek-worker/deepseek-claude-backend.mjs +14 -130
- 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/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 +52 -0
- package/server/public/app/bootstrap.js +153 -27
- 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 +6 -2
- package/server/public/app/context-usage-view.mjs +1 -1
- package/server/public/app/conversation-provider-indicator.mjs +11 -0
- package/server/public/app/conversation-view.js +29 -3
- package/server/public/app/inbox-view.mjs +1 -1
- package/server/public/app/journal-view.js +127 -10
- package/server/public/app/model-selector-options.mjs +17 -1
- package/server/public/app/new-conversation-model-choice.mjs +21 -0
- package/server/public/app/provider-readiness.mjs +5 -2
- package/server/public/app/relay-error-ctas.mjs +50 -0
- package/server/public/app/relay-schedules-ui.js +1 -1
- 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 +18 -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 +189 -3
- package/server/relay-heap-guard.mjs +338 -0
- package/server/relay-self-restart.mjs +33 -7
- package/server/relay-tools.md +6 -6
- package/server/repositories/conversation-group-repository.mjs +1 -1
- 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 +1 -1
- package/server/routes/messages-routes.mjs +678 -31
- 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 +365 -53
- package/server/server-runtime.mjs +373 -26
- package/server/services/about-service.mjs +1 -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/plan-usage-claude-api.mjs +445 -0
- package/server/services/plan-usage-service.mjs +5 -0
- package/server/services/provider-model-selection.mjs +3 -2
- package/server/services/provider-reasoning-effort.mjs +9 -0
- 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 +111 -17
- package/server/services/remote-relay-registry-service.mjs +1 -1
- package/server/services/session-worker-launch-service.mjs +61 -2
- 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 +2 -0
- package/shared/anthropic-api.mjs +202 -0
- 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 +54 -12
- package/shared/remote-relay-tool-core.mjs +23 -6
- package/shared/steer-settle-failure.mjs +1 -0
- package/shared/worker-runtime/api-client.mjs +2 -2
- package/shared/worker-runtime/heartbeat.mjs +5 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +6 -0
|
@@ -47,6 +47,11 @@ import { createClaudeAuthService } from '../services/claude-auth-service.mjs';
|
|
|
47
47
|
import { createCliInstallService } from '../services/cli-install-service.mjs';
|
|
48
48
|
import { createGrokAuthService } from '../services/grok-auth-service.mjs';
|
|
49
49
|
import { parseDeepSeekSettingsUpdateRequest } from '../services/deepseek-settings-service.mjs';
|
|
50
|
+
import {
|
|
51
|
+
CLAUDE_API_PROVIDER_LABEL,
|
|
52
|
+
CLAUDE_API_PROVIDER_TYPE,
|
|
53
|
+
parseClaudeApiSettingsUpdateRequest,
|
|
54
|
+
} from '../services/claude-api-settings-service.mjs';
|
|
50
55
|
import {
|
|
51
56
|
DEEPSEEK_PROVIDER_TYPE,
|
|
52
57
|
DEEPSEEK_REASONING_EFFORTS,
|
|
@@ -496,15 +501,34 @@ export function parseGrokSettingsUpdateRequest(body = {}) {
|
|
|
496
501
|
};
|
|
497
502
|
}
|
|
498
503
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
+
/**
|
|
505
|
+
* The lower-cased ids Copilot itself lists in `modelState`: those an earlier
|
|
506
|
+
* provider layer did not add. A layer tags every id it adds, so an id with
|
|
507
|
+
* tags but none of them Copilot's is another provider's ("claude-opus-5-5"
|
|
508
|
+
* from the Claude layer), not one Copilot serves too.
|
|
509
|
+
*/
|
|
510
|
+
function copilotListedModelIds(modelState = {}) {
|
|
511
|
+
const providersByModel = modelState?.providersByModel && typeof modelState.providersByModel === 'object'
|
|
512
|
+
? modelState.providersByModel
|
|
513
|
+
: {};
|
|
514
|
+
return new Set(
|
|
504
515
|
(Array.isArray(modelState?.models) ? modelState.models : [])
|
|
516
|
+
.filter((value) => {
|
|
517
|
+
const id = String(value || '').trim();
|
|
518
|
+
const providers = providersByModel[id] || providersByModel[id.toLowerCase()];
|
|
519
|
+
return !Array.isArray(providers) || providers.length === 0
|
|
520
|
+
|| providers.some((provider) => String(provider || '').trim().toLowerCase() === 'github-copilot');
|
|
521
|
+
})
|
|
505
522
|
.map((value) => String(value || '').trim().toLowerCase())
|
|
506
523
|
.filter((value) => value && value.toLowerCase() !== 'auto'),
|
|
507
524
|
);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export function buildModelCatalogWithClaudeProvider(modelState = {}, claudeSettings = {}) {
|
|
528
|
+
const configured = claudeSettings?.enabled === true;
|
|
529
|
+
const model = String(claudeSettings?.model || '').trim();
|
|
530
|
+
if (!configured || !model) return { ...modelState };
|
|
531
|
+
const baseModels = copilotListedModelIds(modelState);
|
|
508
532
|
const claudeModels = Array.isArray(claudeSettings?.models)
|
|
509
533
|
? claudeSettings.models.map((value) => String(value || '').trim()).filter(Boolean)
|
|
510
534
|
: [];
|
|
@@ -600,11 +624,7 @@ export function buildModelCatalogWithOpenAIProvider(modelState = {}, openAISetti
|
|
|
600
624
|
const configured = openAISettings?.enabled === true;
|
|
601
625
|
const model = String(openAISettings?.model || '').trim();
|
|
602
626
|
if (!configured || !model) return { ...modelState };
|
|
603
|
-
const baseModels =
|
|
604
|
-
(Array.isArray(modelState?.models) ? modelState.models : [])
|
|
605
|
-
.map((value) => String(value || '').trim().toLowerCase())
|
|
606
|
-
.filter((value) => value && value.toLowerCase() !== 'auto'),
|
|
607
|
-
);
|
|
627
|
+
const baseModels = copilotListedModelIds(modelState);
|
|
608
628
|
const openAIModels = Array.isArray(openAISettings?.models)
|
|
609
629
|
? openAISettings.models.map((value) => String(value || '').trim()).filter(Boolean)
|
|
610
630
|
: [];
|
|
@@ -654,11 +674,7 @@ export function buildModelCatalogWithCursorProvider(modelState = {}, cursorSetti
|
|
|
654
674
|
const configured = cursorSettings?.enabled === true;
|
|
655
675
|
const model = String(cursorSettings?.model || '').trim();
|
|
656
676
|
if (!configured || !model) return { ...modelState };
|
|
657
|
-
const baseModels =
|
|
658
|
-
(Array.isArray(modelState?.models) ? modelState.models : [])
|
|
659
|
-
.map((value) => String(value || '').trim().toLowerCase())
|
|
660
|
-
.filter((value) => value && value.toLowerCase() !== 'auto'),
|
|
661
|
-
);
|
|
677
|
+
const baseModels = copilotListedModelIds(modelState);
|
|
662
678
|
const cursorModelList = Array.isArray(cursorSettings?.models)
|
|
663
679
|
? cursorSettings.models.map((value) => String(value || '').trim()).filter(Boolean)
|
|
664
680
|
: [];
|
|
@@ -743,11 +759,7 @@ export function buildModelCatalogWithGrokProvider(modelState = {}, grokSettings
|
|
|
743
759
|
const configured = grokSettings?.enabled === true;
|
|
744
760
|
const model = String(grokSettings?.model || '').trim();
|
|
745
761
|
if (!configured || !model) return { ...modelState };
|
|
746
|
-
const baseModels =
|
|
747
|
-
(Array.isArray(modelState?.models) ? modelState.models : [])
|
|
748
|
-
.map((value) => String(value || '').trim().toLowerCase())
|
|
749
|
-
.filter((value) => value && value.toLowerCase() !== 'auto'),
|
|
750
|
-
);
|
|
762
|
+
const baseModels = copilotListedModelIds(modelState);
|
|
751
763
|
const grokModelList = Array.isArray(grokSettings?.models)
|
|
752
764
|
? grokSettings.models.map((value) => String(value || '').trim()).filter(Boolean)
|
|
753
765
|
: [];
|
|
@@ -855,6 +867,82 @@ export function buildModelCatalogWithDeepSeekProvider(modelState = {}, deepseekS
|
|
|
855
867
|
};
|
|
856
868
|
}
|
|
857
869
|
|
|
870
|
+
/**
|
|
871
|
+
* Claude API (provider `claude-api`) serves the Claude provider's model ids
|
|
872
|
+
* on an API key. Its layer tags them `claude-api` beside whatever else serves
|
|
873
|
+
* them and keeps its own efforts and context tiers: `[1m]` variants fold into
|
|
874
|
+
* their base model as a long_context tier, as on the Claude layer. An id is
|
|
875
|
+
* never enough to pick this provider; only an explicit `claude-api` does.
|
|
876
|
+
*/
|
|
877
|
+
export function buildModelCatalogWithClaudeApiProvider(modelState = {}, claudeApiSettings = {}) {
|
|
878
|
+
const configured = claudeApiSettings?.enabled === true;
|
|
879
|
+
const model = String(claudeApiSettings?.model || '').trim();
|
|
880
|
+
if (!configured || !model) return { ...modelState };
|
|
881
|
+
const listed = Array.isArray(claudeApiSettings?.models) ? claudeApiSettings.models : [];
|
|
882
|
+
const longContextBases = new Set();
|
|
883
|
+
const defaultContextBases = new Set();
|
|
884
|
+
const baseModels = [];
|
|
885
|
+
const seenBases = new Set();
|
|
886
|
+
for (const candidate of [model, ...listed]) {
|
|
887
|
+
const modelId = String(candidate || '').trim();
|
|
888
|
+
if (!modelId) continue;
|
|
889
|
+
const isLongContextVariant = CLAUDE_LONG_CONTEXT_SUFFIX_PATTERN.test(modelId);
|
|
890
|
+
const baseId = modelId.replace(CLAUDE_LONG_CONTEXT_SUFFIX_PATTERN, '');
|
|
891
|
+
if (!baseId) continue;
|
|
892
|
+
if (isLongContextVariant) longContextBases.add(baseId.toLowerCase());
|
|
893
|
+
else defaultContextBases.add(baseId.toLowerCase());
|
|
894
|
+
if (seenBases.has(baseId.toLowerCase())) continue;
|
|
895
|
+
seenBases.add(baseId.toLowerCase());
|
|
896
|
+
baseModels.push(baseId);
|
|
897
|
+
}
|
|
898
|
+
const models = Array.from(new Set([...(Array.isArray(modelState?.models) ? modelState.models : []), ...baseModels]));
|
|
899
|
+
const reasoningByModel = { ...(modelState?.reasoningByModel || {}) };
|
|
900
|
+
const claudeApiReasoningByModel = {};
|
|
901
|
+
const modelMetadataByModel = { ...(modelState?.modelMetadataByModel || {}) };
|
|
902
|
+
const providersByModel = { ...(modelState?.providersByModel || {}) };
|
|
903
|
+
const claudeApiContextTiersByModel = {};
|
|
904
|
+
const effortsByModel = claudeApiSettings?.effortsByModel && typeof claudeApiSettings.effortsByModel === 'object'
|
|
905
|
+
? claudeApiSettings.effortsByModel
|
|
906
|
+
: {};
|
|
907
|
+
for (const baseId of baseModels) {
|
|
908
|
+
const lowerKey = baseId.toLowerCase();
|
|
909
|
+
claudeApiContextTiersByModel[lowerKey] = [
|
|
910
|
+
...(defaultContextBases.has(lowerKey) ? [{ value: 'default' }] : []),
|
|
911
|
+
...(longContextBases.has(lowerKey) ? [{ value: 'long_context' }] : []),
|
|
912
|
+
];
|
|
913
|
+
const efforts = Array.isArray(effortsByModel[lowerKey]) && effortsByModel[lowerKey].length
|
|
914
|
+
? effortsByModel[lowerKey]
|
|
915
|
+
: effortsByModel[`${lowerKey}[1m]`];
|
|
916
|
+
const claudeApiEfforts = Array.isArray(efforts) && efforts.length
|
|
917
|
+
? withClaudeUltracodeTier(efforts)
|
|
918
|
+
: [...DEFAULT_CLAUDE_REASONING_EFFORTS];
|
|
919
|
+
claudeApiReasoningByModel[lowerKey] = [...claudeApiEfforts];
|
|
920
|
+
if (!Array.isArray(reasoningByModel[lowerKey]) || reasoningByModel[lowerKey].length === 0) {
|
|
921
|
+
reasoningByModel[lowerKey] = [...claudeApiEfforts];
|
|
922
|
+
}
|
|
923
|
+
const existingProviders = Array.isArray(providersByModel[baseId])
|
|
924
|
+
? providersByModel[baseId]
|
|
925
|
+
: (Array.isArray(providersByModel[lowerKey]) ? providersByModel[lowerKey] : []);
|
|
926
|
+
providersByModel[baseId] = Array.from(new Set([...existingProviders, CLAUDE_API_PROVIDER_TYPE]));
|
|
927
|
+
modelMetadataByModel[baseId] = {
|
|
928
|
+
...(modelMetadataByModel[baseId] || {}),
|
|
929
|
+
provider: modelMetadataByModel[baseId]?.provider || CLAUDE_API_PROVIDER_TYPE,
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
return {
|
|
933
|
+
...modelState,
|
|
934
|
+
models,
|
|
935
|
+
reasoningByModel,
|
|
936
|
+
reasoningByProvider: {
|
|
937
|
+
...(modelState?.reasoningByProvider || {}),
|
|
938
|
+
[CLAUDE_API_PROVIDER_TYPE]: claudeApiReasoningByModel,
|
|
939
|
+
},
|
|
940
|
+
modelMetadataByModel,
|
|
941
|
+
providersByModel,
|
|
942
|
+
claudeApiContextTiersByModel,
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
|
|
858
946
|
function normalizeRequestedProviderType(value = '') {
|
|
859
947
|
const normalized = String(value || '').trim().toLowerCase();
|
|
860
948
|
if (normalized === 'openai' || normalized === 'openai-byok') return 'openai';
|
|
@@ -865,6 +953,8 @@ function normalizeRequestedProviderType(value = '') {
|
|
|
865
953
|
if (normalized === 'cursor') return 'cursor';
|
|
866
954
|
if (normalized === 'grok' || normalized === 'xai' || normalized === 'xai-grok') return 'grok';
|
|
867
955
|
if (normalized === 'deepseek') return 'deepseek';
|
|
956
|
+
// Its own provider, never read as `claude`.
|
|
957
|
+
if (normalized === 'claude-api') return 'claude-api';
|
|
868
958
|
if (normalized === 'github' || normalized === 'github-copilot') return 'github';
|
|
869
959
|
return '';
|
|
870
960
|
}
|
|
@@ -1506,9 +1596,10 @@ export function buildConversationSessionRootPayload({
|
|
|
1506
1596
|
}
|
|
1507
1597
|
// Claude sessions have no ~/.copilot/session-state entry — only the Copilot
|
|
1508
1598
|
// CLI creates those — so they resolve against the Agent SDK's own layout and
|
|
1509
|
-
// never fall through to the branch below.
|
|
1599
|
+
// never fall through to the branch below. A Claude API session is a Claude
|
|
1600
|
+
// session on an API key, in the same place.
|
|
1510
1601
|
const claudeCodeProvider = String(providerType || '').trim().toLowerCase();
|
|
1511
|
-
if (claudeCodeProvider === 'claude' || claudeCodeProvider === 'deepseek') {
|
|
1602
|
+
if (claudeCodeProvider === 'claude' || claudeCodeProvider === 'deepseek' || claudeCodeProvider === 'claude-api') {
|
|
1512
1603
|
const nativeSessionId = String(claudeNativeSessionId || '').trim();
|
|
1513
1604
|
const resolveRoot = claudeCodeProvider === 'deepseek' ? resolveDeepSeekSessionRoot : resolveClaudeSessionRoot;
|
|
1514
1605
|
if (!nativeSessionId || typeof resolveRoot !== 'function') return null;
|
|
@@ -2138,6 +2229,20 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2138
2229
|
// attribution (services/deepseek-settings-service.mjs). Absent in older
|
|
2139
2230
|
// wiring and most tests, where the provider is simply off.
|
|
2140
2231
|
deepseekSettingsService = null,
|
|
2232
|
+
// Claude API (provider `claude-api`): its key, models and monthly cap
|
|
2233
|
+
// (services/claude-api-settings-service.mjs). Absent in older wiring and
|
|
2234
|
+
// most tests, where the provider is simply off.
|
|
2235
|
+
claudeApiSettingsService = null,
|
|
2236
|
+
// The Claude API card and monthly cap (services/plan-usage-claude-api.mjs),
|
|
2237
|
+
// and the cap's one seam (`{ reached, error? }`; a throw counts as
|
|
2238
|
+
// reached). Absent in older wiring and most tests: no card, no cap.
|
|
2239
|
+
claudeApiPlanUsage = null,
|
|
2240
|
+
getClaudeApiCapState = () => ({ reached: false }),
|
|
2241
|
+
// Asks whether the cap flipped after the Claude API settings changed.
|
|
2242
|
+
checkClaudeApiCap = () => {},
|
|
2243
|
+
// The once-per-episode notes for stopped self-started turns on the key
|
|
2244
|
+
// (plan-usage-claude-api.mjs): a saved setting starts every chat afresh.
|
|
2245
|
+
claudeApiStopLedger = null,
|
|
2141
2246
|
reconcileUnstartedConversationProviders = async () => ({
|
|
2142
2247
|
updatedUnstartedConversations: 0,
|
|
2143
2248
|
skippedStartedConversations: 0,
|
|
@@ -2248,6 +2353,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2248
2353
|
hostSuspendService = null,
|
|
2249
2354
|
collectHostActivity = null,
|
|
2250
2355
|
usageLimitPauseService = null,
|
|
2356
|
+
claudeBillingSwitchService = null,
|
|
2251
2357
|
hostSuspendPlatform = process.platform,
|
|
2252
2358
|
// Remote relays (server/services/remote-relay-inbound.mjs): origin on
|
|
2253
2359
|
// conversations another relay's agent created, and the inbound switch.
|
|
@@ -2504,20 +2610,28 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2504
2610
|
|| { configured: false, enabled: false, model: DEFAULT_DEEPSEEK_MODEL, models: [], availableModels: [] };
|
|
2505
2611
|
}
|
|
2506
2612
|
|
|
2613
|
+
function getClaudeApiProviderSettings() {
|
|
2614
|
+
return claudeApiSettingsService?.getSettings?.()
|
|
2615
|
+
|| { configured: false, enabled: false, model: '', models: [], availableModels: [], effortsByModel: {} };
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2507
2618
|
// Layer every enabled provider's models onto the base Copilot catalog.
|
|
2508
2619
|
function buildModelCatalogWithProviders(modelState, openAISettings = getOpenAIProviderSettings()) {
|
|
2509
|
-
return
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2620
|
+
return buildModelCatalogWithClaudeApiProvider(
|
|
2621
|
+
buildModelCatalogWithDeepSeekProvider(
|
|
2622
|
+
buildModelCatalogWithGrokProvider(
|
|
2623
|
+
buildModelCatalogWithCursorProvider(
|
|
2624
|
+
buildModelCatalogWithClaudeProvider(
|
|
2625
|
+
buildModelCatalogWithOpenAIProvider(modelState, openAISettings),
|
|
2626
|
+
getClaudeProviderSettings(),
|
|
2627
|
+
),
|
|
2628
|
+
getCursorProviderSettings(),
|
|
2515
2629
|
),
|
|
2516
|
-
|
|
2630
|
+
getGrokProviderSettings(),
|
|
2517
2631
|
),
|
|
2518
|
-
|
|
2632
|
+
getDeepSeekProviderSettings(),
|
|
2519
2633
|
),
|
|
2520
|
-
|
|
2634
|
+
getClaudeApiProviderSettings(),
|
|
2521
2635
|
);
|
|
2522
2636
|
}
|
|
2523
2637
|
|
|
@@ -2708,7 +2822,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2708
2822
|
async function deleteCliSession({ conversation, conversationId, sdkSessionId, runtimeSession }) {
|
|
2709
2823
|
const provider = String(runtimeSession?.provider_type || '').trim().toLowerCase();
|
|
2710
2824
|
try {
|
|
2711
|
-
if (provider === 'claude') return deleteClaudeCliSession({ conversation, runtimeSession });
|
|
2825
|
+
if (provider === 'claude' || provider === 'claude-api') return deleteClaudeCliSession({ conversation, runtimeSession });
|
|
2712
2826
|
if (provider === 'deepseek') return deleteDeepSeekCliSession({ conversation, runtimeSession });
|
|
2713
2827
|
if (!sdkSessionId) return { deleted: null };
|
|
2714
2828
|
if (!provider || provider === 'github' || provider === 'openai') {
|
|
@@ -2782,7 +2896,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2782
2896
|
/**
|
|
2783
2897
|
* The `group` field of a single conversation payload: the summary and the
|
|
2784
2898
|
* conversation's role. The live members, with what each is doing, only
|
|
2785
|
-
* when asked for (`groupMembers=1`, the
|
|
2899
|
+
* when asked for (`groupMembers=1`, the relay_tool's read_session)
|
|
2786
2900
|
* and never for a paired relay's agent: the browser does not read them,
|
|
2787
2901
|
* and archived members' titles stay on this relay.
|
|
2788
2902
|
*/
|
|
@@ -3644,9 +3758,11 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
3644
3758
|
// The cursor and grok workers post the same Claude-shaped usage blob, and
|
|
3645
3759
|
// so does the Claude Cloud worker (the cloud session's context and token
|
|
3646
3760
|
// usage), and so does the DeepSeek worker (the Claude one, pointed at
|
|
3647
|
-
// DeepSeek), so
|
|
3761
|
+
// DeepSeek), and so does the Claude API one (the Claude worker on an API
|
|
3762
|
+
// key), so all six providers share the stored reader.
|
|
3648
3763
|
const usesStoredContextUsage = providerType === 'claude' || providerType === 'cursor'
|
|
3649
|
-
|| providerType === 'grok' || providerType === 'claude-cloud' || providerType === 'deepseek'
|
|
3764
|
+
|| providerType === 'grok' || providerType === 'claude-cloud' || providerType === 'deepseek'
|
|
3765
|
+
|| providerType === 'claude-api';
|
|
3650
3766
|
|
|
3651
3767
|
const parsed = usesStoredContextUsage
|
|
3652
3768
|
? (() => {
|
|
@@ -3675,12 +3791,12 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
3675
3791
|
String(runtimeSession?.conversation_id || '').trim() || lookupId,
|
|
3676
3792
|
);
|
|
3677
3793
|
const autoCompactWindow = parseAutoCompactWindow(conversationRow?.auto_compact_window);
|
|
3678
|
-
// Only the local Claude worker (and the DeepSeek
|
|
3679
|
-
// runs a CLI whose window the slider sets; it reports that
|
|
3680
|
-
// heartbeat's steering snapshot.
|
|
3794
|
+
// Only the local Claude worker (and the DeepSeek and Claude API ones, the
|
|
3795
|
+
// same runner) runs a CLI whose window the slider sets; it reports that
|
|
3796
|
+
// CLI on its heartbeat's steering snapshot.
|
|
3681
3797
|
const windowStatus = resolveAutoCompactWindowStatus({
|
|
3682
3798
|
storedWindow: autoCompactWindow,
|
|
3683
|
-
steering: (providerType === 'claude' || providerType === 'deepseek') && copilotSessionId
|
|
3799
|
+
steering: (providerType === 'claude' || providerType === 'deepseek' || providerType === 'claude-api') && copilotSessionId
|
|
3684
3800
|
? sessionWorkerRegistry?.getWorker?.(copilotSessionId)?.steering || null
|
|
3685
3801
|
: null,
|
|
3686
3802
|
});
|
|
@@ -3700,7 +3816,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
3700
3816
|
// What this folder's Claude commits say (the 🧠 modal's override row).
|
|
3701
3817
|
// A Claude Cloud chat has the row when it was started from a folder,
|
|
3702
3818
|
// with its own model in the example.
|
|
3703
|
-
...(providerType === 'claude'
|
|
3819
|
+
...(providerType === 'claude' || providerType === 'claude-api'
|
|
3704
3820
|
? { attribution: workspaceRootAttributionFor(conversationRow) }
|
|
3705
3821
|
: {}),
|
|
3706
3822
|
...(providerType === 'claude-cloud' && cloudConversationFolder(conversationRow)
|
|
@@ -4728,6 +4844,9 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
4728
4844
|
try { return getTaskStallMinutes(); } catch { return DEFAULT_TASK_STALL_MINUTES; }
|
|
4729
4845
|
})(),
|
|
4730
4846
|
usageLimitPause: usageLimitPauseService?.getPause?.(resolvedConversationId) || null,
|
|
4847
|
+
// A Claude chat's billing (subscription or API key) and whether the
|
|
4848
|
+
// composer's toggle may switch it now, with the reason when not.
|
|
4849
|
+
billing: claudeBillingSwitchService?.billingState?.(resolvedConversationId) || null,
|
|
4731
4850
|
previews: listConversationPreviews?.(resolvedConversationId) || [],
|
|
4732
4851
|
// Every pin of the conversation, also those outside this history page.
|
|
4733
4852
|
pins: listConversationPins(resolvedConversationId),
|
|
@@ -5014,7 +5133,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
5014
5133
|
return res.json({ ok: true, ...persisted });
|
|
5015
5134
|
});
|
|
5016
5135
|
|
|
5017
|
-
// ─── PATCH /api/conversation/:id/settings — the
|
|
5136
|
+
// ─── PATCH /api/conversation/:id/settings — the relay_tool's update_session ───
|
|
5018
5137
|
//
|
|
5019
5138
|
// `{ settings: { model, effort, mode, auto_compact_window, title, group,
|
|
5020
5139
|
// thinking_enabled, thinking_display, cwd }, dry_run, owner_approved }`.
|
|
@@ -5135,7 +5254,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
5135
5254
|
plan.preferences.thinkingDisplay = wanted.thinking_display;
|
|
5136
5255
|
change('thinking_display');
|
|
5137
5256
|
}
|
|
5138
|
-
if (provider !== 'claude' && ['auto_compact_window', 'thinking_enabled', 'thinking_display'].some((key) => wanted[key] !== undefined)) {
|
|
5257
|
+
if (provider !== 'claude' && provider !== 'claude-api' && ['auto_compact_window', 'thinking_enabled', 'thinking_display'].some((key) => wanted[key] !== undefined)) {
|
|
5139
5258
|
plan.notes.push(`auto_compact_window and thinking apply to Claude sessions only; this one runs on ${provider}, so they are stored but change nothing there.`);
|
|
5140
5259
|
}
|
|
5141
5260
|
if (wanted.title !== undefined) {
|
|
@@ -6267,6 +6386,35 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6267
6386
|
environmentId: cloudEnvironmentId,
|
|
6268
6387
|
});
|
|
6269
6388
|
}
|
|
6389
|
+
// Claude API is asked for by name only, like Claude Cloud: its model ids
|
|
6390
|
+
// are the Claude provider's own, so a model never selects it. Refused
|
|
6391
|
+
// first while it has no key or is switched off.
|
|
6392
|
+
const useClaudeApiProvider = requestedProviderType === CLAUDE_API_PROVIDER_TYPE;
|
|
6393
|
+
const claudeApiSettings = useClaudeApiProvider ? getClaudeApiProviderSettings() : null;
|
|
6394
|
+
if (useClaudeApiProvider && claudeApiSettings?.enabled !== true) {
|
|
6395
|
+
return res.status(400).json({
|
|
6396
|
+
ok: false,
|
|
6397
|
+
error: claudeApiSettings?.configured === true
|
|
6398
|
+
? 'Claude API is switched off for new conversations.'
|
|
6399
|
+
: 'Claude API key is not configured.',
|
|
6400
|
+
code: 'CLAUDE_API_NOT_CONFIGURED',
|
|
6401
|
+
});
|
|
6402
|
+
}
|
|
6403
|
+
// This month's cap (plan decision 12): no new Claude API chat while it is
|
|
6404
|
+
// reached, for New Chat and an agent's create_session alike. A cap that
|
|
6405
|
+
// cannot be read counts as reached.
|
|
6406
|
+
if (useClaudeApiProvider) {
|
|
6407
|
+
let cap = null;
|
|
6408
|
+
try { cap = getClaudeApiCapState() || null; } catch { cap = { reached: true }; }
|
|
6409
|
+
if (cap?.reached === true) {
|
|
6410
|
+
return res.status(409).json({
|
|
6411
|
+
ok: false,
|
|
6412
|
+
// The remedy names the place, so the reason does not.
|
|
6413
|
+
error: `${String(cap.reason || cap.error || '').trim() || 'This month\'s Claude API spend has reached its cap.'} No Claude API chat was started; start it on the Claude subscription, or raise the cap in Settings → Providers → Claude API.`,
|
|
6414
|
+
code: 'CLAUDE_API_CAP_REACHED',
|
|
6415
|
+
});
|
|
6416
|
+
}
|
|
6417
|
+
}
|
|
6270
6418
|
const modelState = buildModelCatalogWithProviders(
|
|
6271
6419
|
getModelCatalogState(),
|
|
6272
6420
|
openAISettings,
|
|
@@ -6312,6 +6460,13 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6312
6460
|
...(Array.isArray(deepseekSettings?.models) ? deepseekSettings.models : []),
|
|
6313
6461
|
].map((value) => String(value || '').trim()).filter(Boolean);
|
|
6314
6462
|
const availableDeepSeekModels = new Set(deepseekModelCatalog.map((value) => value.toLowerCase()));
|
|
6463
|
+
// Base ids of "[1m]" variants count too, as for the Claude provider.
|
|
6464
|
+
const availableClaudeApiModels = new Set((useClaudeApiProvider ? [
|
|
6465
|
+
String(claudeApiSettings?.model || '').trim(),
|
|
6466
|
+
...(Array.isArray(claudeApiSettings?.models) ? claudeApiSettings.models : []),
|
|
6467
|
+
] : []).map((value) => String(value || '').trim()).filter(Boolean)
|
|
6468
|
+
.flatMap((value) => [value, value.replace(CLAUDE_LONG_CONTEXT_SUFFIX_PATTERN, '')])
|
|
6469
|
+
.filter(Boolean));
|
|
6315
6470
|
const useOpenAIProvider = requestedProviderType === 'openai'
|
|
6316
6471
|
|| requestedProviderType === 'openai-image'
|
|
6317
6472
|
|| (
|
|
@@ -6376,6 +6531,12 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6376
6531
|
configuredModel: claudeCloudSettings.defaultModel,
|
|
6377
6532
|
availableModels: Array.isArray(claudeCloudSettings.models) ? claudeCloudSettings.models : [],
|
|
6378
6533
|
})
|
|
6534
|
+
: useClaudeApiProvider
|
|
6535
|
+
? resolveProviderModelSelection({
|
|
6536
|
+
requestedModel: providerRequestedModel(availableClaudeApiModels),
|
|
6537
|
+
configuredModel: claudeApiSettings.model,
|
|
6538
|
+
availableModels: availableClaudeApiModels,
|
|
6539
|
+
})
|
|
6379
6540
|
: useOpenAIProvider
|
|
6380
6541
|
? resolveProviderModelSelection({
|
|
6381
6542
|
requestedModel: providerRequestedModel(availableOpenAIModels),
|
|
@@ -6414,11 +6575,15 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6414
6575
|
if (providerModelSelection && !providerModelSelection.ok) {
|
|
6415
6576
|
const providerLabel = useClaudeCloudProvider
|
|
6416
6577
|
? 'Claude Cloud'
|
|
6578
|
+
: useClaudeApiProvider
|
|
6579
|
+
? CLAUDE_API_PROVIDER_LABEL
|
|
6417
6580
|
: (useOpenAIProvider
|
|
6418
6581
|
? 'OpenAI'
|
|
6419
6582
|
: (useClaudeProvider ? 'Claude' : (useCursorProvider ? 'Cursor' : (useGrokProvider ? 'Grok' : 'DeepSeek'))));
|
|
6420
6583
|
const providerCode = useClaudeCloudProvider
|
|
6421
6584
|
? 'CLAUDE_CLOUD_MODEL_UNAVAILABLE'
|
|
6585
|
+
: useClaudeApiProvider
|
|
6586
|
+
? 'CLAUDE_API_MODEL_UNAVAILABLE'
|
|
6422
6587
|
: (useOpenAIProvider
|
|
6423
6588
|
? 'OPENAI_MODEL_UNAVAILABLE'
|
|
6424
6589
|
: (useClaudeProvider
|
|
@@ -6442,10 +6607,11 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6442
6607
|
// lookup below knows exact base ids only and would fall back too.
|
|
6443
6608
|
let copilotBoundModel = '';
|
|
6444
6609
|
let copilotVariantEffort = '';
|
|
6610
|
+
let copilotRefusal = null;
|
|
6445
6611
|
if (
|
|
6446
6612
|
remoteRequest.remote
|
|
6447
6613
|
&& !useClaudeCloudProvider && !useOpenAIProvider && !useClaudeProvider && !useCursorProvider && !useGrokProvider
|
|
6448
|
-
&& !useDeepSeekProvider
|
|
6614
|
+
&& !useDeepSeekProvider && !useClaudeApiProvider
|
|
6449
6615
|
) {
|
|
6450
6616
|
const mergedModels = Array.isArray(modelState?.models) ? modelState.models : [];
|
|
6451
6617
|
const providersByModel = modelState?.providersByModel && typeof modelState.providersByModel === 'object'
|
|
@@ -6467,20 +6633,23 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6467
6633
|
effortsFor: (base) => reasoningByModel[base] || reasoningByModel[String(base).toLowerCase()] || null,
|
|
6468
6634
|
});
|
|
6469
6635
|
// Another provider's model is answered by the checks below ("requires
|
|
6470
|
-
// the Claude provider"), not as an unknown one.
|
|
6636
|
+
// the Claude provider"), not as an unknown one. One that several other
|
|
6637
|
+
// providers list ("claude-opus-5-5": Claude and Cursor) passes those
|
|
6638
|
+
// checks and is refused as unknown after them.
|
|
6471
6639
|
const otherProviders = mergedModels.some((modelId) => String(modelId || '').trim().toLowerCase() === requestedBootstrapModel.toLowerCase());
|
|
6472
6640
|
if (copilotCheck.status === 'known' && copilotCheck.model && !isAutoModelId(copilotCheck.model)) {
|
|
6473
6641
|
copilotBoundModel = copilotCheck.base || copilotCheck.model;
|
|
6474
6642
|
copilotVariantEffort = copilotCheck.effort || '';
|
|
6475
6643
|
}
|
|
6476
|
-
if (copilotCheck.status === 'unknown'
|
|
6477
|
-
|
|
6644
|
+
if (copilotCheck.status === 'unknown') {
|
|
6645
|
+
copilotRefusal = {
|
|
6478
6646
|
ok: false,
|
|
6479
6647
|
error: unknownProviderModelText({ model: copilotCheck.model, provider: 'github', suggestions: copilotCheck.suggestions, models: copilotCheck.models }),
|
|
6480
6648
|
code: 'COPILOT_MODEL_UNAVAILABLE',
|
|
6481
6649
|
supportedModels: copilotCheck.models,
|
|
6482
6650
|
...(copilotCheck.suggestions.length ? { suggestedModels: copilotCheck.suggestions } : {}),
|
|
6483
|
-
}
|
|
6651
|
+
};
|
|
6652
|
+
if (!otherProviders) return res.status(400).json(copilotRefusal);
|
|
6484
6653
|
}
|
|
6485
6654
|
}
|
|
6486
6655
|
if (useOpenAIProvider && openAISettings?.configured !== true) {
|
|
@@ -6574,7 +6743,10 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6574
6743
|
code: 'OPENAI_IMAGE_MODEL_REQUIRED',
|
|
6575
6744
|
});
|
|
6576
6745
|
}
|
|
6577
|
-
if (
|
|
6746
|
+
if (
|
|
6747
|
+
!useClaudeCloudProvider && !useOpenAIProvider && !useClaudeProvider && !useCursorProvider && !useGrokProvider
|
|
6748
|
+
&& !useDeepSeekProvider && !useClaudeApiProvider
|
|
6749
|
+
) {
|
|
6578
6750
|
const selectedProviders = Array.isArray(modelState?.providersByModel?.[String(selectedModel || '').trim().toLowerCase()])
|
|
6579
6751
|
? modelState.providersByModel[String(selectedModel || '').trim().toLowerCase()]
|
|
6580
6752
|
: [];
|
|
@@ -6587,8 +6759,12 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6587
6759
|
code: 'OPENAI_PROVIDER_REQUIRED',
|
|
6588
6760
|
});
|
|
6589
6761
|
}
|
|
6590
|
-
|
|
6591
|
-
|
|
6762
|
+
// Claude API serves the Claude provider's ids: a model both list is
|
|
6763
|
+
// still Claude's, and one only Claude API lists names that provider.
|
|
6764
|
+
const selectedProviderIds = selectedProviders.map((provider) => String(provider || '').trim().toLowerCase());
|
|
6765
|
+
const claudeOnlySelection = selectedProviderIds.length > 0
|
|
6766
|
+
&& selectedProviderIds.includes('claude')
|
|
6767
|
+
&& selectedProviderIds.every((provider) => provider === 'claude' || provider === CLAUDE_API_PROVIDER_TYPE);
|
|
6592
6768
|
if (claudeOnlySelection) {
|
|
6593
6769
|
return res.status(400).json({
|
|
6594
6770
|
ok: false,
|
|
@@ -6596,6 +6772,15 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6596
6772
|
code: 'CLAUDE_PROVIDER_REQUIRED',
|
|
6597
6773
|
});
|
|
6598
6774
|
}
|
|
6775
|
+
const claudeApiOnlySelection = selectedProviderIds.length > 0
|
|
6776
|
+
&& selectedProviderIds.every((provider) => provider === CLAUDE_API_PROVIDER_TYPE);
|
|
6777
|
+
if (claudeApiOnlySelection) {
|
|
6778
|
+
return res.status(400).json({
|
|
6779
|
+
ok: false,
|
|
6780
|
+
error: `Model "${selectedModel}" requires the Claude API provider`,
|
|
6781
|
+
code: 'CLAUDE_API_PROVIDER_REQUIRED',
|
|
6782
|
+
});
|
|
6783
|
+
}
|
|
6599
6784
|
const cursorOnlySelection = selectedProviders.length > 0
|
|
6600
6785
|
&& selectedProviders.every((provider) => String(provider || '').trim().toLowerCase() === 'cursor');
|
|
6601
6786
|
if (cursorOnlySelection) {
|
|
@@ -6624,9 +6809,14 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6624
6809
|
});
|
|
6625
6810
|
}
|
|
6626
6811
|
}
|
|
6812
|
+
// No single other provider claims it: a Copilot session with a model
|
|
6813
|
+
// Copilot does not list would be refused at its first send.
|
|
6814
|
+
if (copilotRefusal) return res.status(400).json(copilotRefusal);
|
|
6627
6815
|
|
|
6628
6816
|
const bootstrapProviderType = useClaudeCloudProvider
|
|
6629
6817
|
? 'claude-cloud'
|
|
6818
|
+
: useClaudeApiProvider
|
|
6819
|
+
? CLAUDE_API_PROVIDER_TYPE
|
|
6630
6820
|
: (useOpenAIProvider
|
|
6631
6821
|
? 'openai'
|
|
6632
6822
|
: (useClaudeProvider
|
|
@@ -6657,6 +6847,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6657
6847
|
model: selectedModel,
|
|
6658
6848
|
cursorSettings,
|
|
6659
6849
|
claudeSettings,
|
|
6850
|
+
claudeApiSettings,
|
|
6660
6851
|
grokSettings,
|
|
6661
6852
|
reasoningByModel: modelState?.reasoningByModel || null,
|
|
6662
6853
|
}),
|
|
@@ -6701,7 +6892,10 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6701
6892
|
{
|
|
6702
6893
|
assignConfiguredProvider: true,
|
|
6703
6894
|
providerType: bootstrapProviderType,
|
|
6704
|
-
providerModel: (
|
|
6895
|
+
providerModel: (
|
|
6896
|
+
useClaudeCloudProvider || useOpenAIProvider || useClaudeProvider || useCursorProvider || useGrokProvider
|
|
6897
|
+
|| useDeepSeekProvider || useClaudeApiProvider
|
|
6898
|
+
) ? selectedModel : null,
|
|
6705
6899
|
},
|
|
6706
6900
|
);
|
|
6707
6901
|
// What the cloud session will clone, stored with the conversation so
|
|
@@ -7680,6 +7874,11 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
7680
7874
|
// workers of the provider keep the old key and fail each turn after the
|
|
7681
7875
|
// rotation. Stops them; the primer or the next delivery respawns them with
|
|
7682
7876
|
// the fresh key from settings.
|
|
7877
|
+
//
|
|
7878
|
+
// The workers are picked by what they run: a worker that states its kind
|
|
7879
|
+
// is taken at its word (a Claude chat switched to the API key may still
|
|
7880
|
+
// have its subscription worker finishing a turn, and one switched back
|
|
7881
|
+
// its API worker); one that does not is taken at its chat's provider.
|
|
7683
7882
|
async function stopLiveProviderWorkersForKeyRotation(providerType) {
|
|
7684
7883
|
const rows = db.prepare(`
|
|
7685
7884
|
SELECT DISTINCT NULLIF(sdk_session_id, '') AS sdk_session_id
|
|
@@ -7687,8 +7886,16 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
7687
7886
|
WHERE LOWER(COALESCE(provider_type, '')) = ?
|
|
7688
7887
|
AND NULLIF(sdk_session_id, '') IS NOT NULL
|
|
7689
7888
|
`).all(providerType);
|
|
7889
|
+
const sids = new Set();
|
|
7890
|
+
for (const worker of sessionWorkerRegistry?.listWorkers?.() || []) {
|
|
7891
|
+
if (worker?.kind === providerType && worker.sdkSessionId) sids.add(worker.sdkSessionId);
|
|
7892
|
+
}
|
|
7690
7893
|
for (const row of rows) {
|
|
7691
7894
|
const sid = String(row.sdk_session_id || '').trim();
|
|
7895
|
+
const kind = sid ? (sessionWorkerRegistry?.getWorker?.(sid)?.kind || null) : null;
|
|
7896
|
+
if (sid && (!kind || kind === providerType)) sids.add(sid);
|
|
7897
|
+
}
|
|
7898
|
+
for (const sid of sids) {
|
|
7692
7899
|
if (!sid) continue;
|
|
7693
7900
|
const workerState = sessionWorkerSupervisor?.getWorkerState?.(sid) || null;
|
|
7694
7901
|
if (!workerState) continue;
|
|
@@ -7889,6 +8096,74 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
7889
8096
|
});
|
|
7890
8097
|
});
|
|
7891
8098
|
|
|
8099
|
+
// --- Claude API ----------------------------------------------------------
|
|
8100
|
+
// The key is written here and never read back: GET and every answer carry
|
|
8101
|
+
// `configured` only. A pasted Claude login token or Admin key is refused by
|
|
8102
|
+
// its prefix. A rotated key reaches a worker only at spawn, so the live
|
|
8103
|
+
// Claude API workers are stopped; the next message starts them again.
|
|
8104
|
+
app.get('/api/settings/claude-api', auth, (_req, res) => {
|
|
8105
|
+
if (!claudeApiSettingsService) return res.status(501).json({ error: 'Claude API settings are unavailable' });
|
|
8106
|
+
return res.json(claudeApiSettingsService.describe());
|
|
8107
|
+
});
|
|
8108
|
+
|
|
8109
|
+
app.post('/api/settings/claude-api', auth, async (req, res) => {
|
|
8110
|
+
if (!claudeApiSettingsService) return res.status(501).json({ error: 'Claude API settings are unavailable' });
|
|
8111
|
+
const parsed = parseClaudeApiSettingsUpdateRequest(req.body);
|
|
8112
|
+
if (!parsed.ok) return res.status(400).json({ error: parsed.error, code: parsed.code || null });
|
|
8113
|
+
const result = claudeApiSettingsService.update(parsed);
|
|
8114
|
+
if (!result?.ok) {
|
|
8115
|
+
return res.status(result?.code === 'claude-api-key-removal-blocked' ? 409 : 400).json({
|
|
8116
|
+
error: result?.error || 'Failed to update Claude API settings',
|
|
8117
|
+
code: result?.code || null,
|
|
8118
|
+
activeConversationCount: Number(result?.activeConversationCount || 0),
|
|
8119
|
+
startedConversationCount: Number(result?.startedConversationCount || 0),
|
|
8120
|
+
activeQueueConversationCount: Number(result?.activeQueueConversationCount || 0),
|
|
8121
|
+
});
|
|
8122
|
+
}
|
|
8123
|
+
if (result.keyChanged) await stopLiveProviderWorkersForKeyRotation(CLAUDE_API_PROVIDER_TYPE);
|
|
8124
|
+
const discovery = result.discover
|
|
8125
|
+
? await claudeApiSettingsService.refreshModels()
|
|
8126
|
+
: { ok: true, models: [], error: null };
|
|
8127
|
+
// Removing the key or switching the provider off moves unstarted Claude
|
|
8128
|
+
// API conversations back to the default provider, as for Claude itself.
|
|
8129
|
+
const reconciliationResult = result.removed === true || result.disabled === true
|
|
8130
|
+
? await reconcileUnstartedConversationProviders({ enabled: false, model: '', provider: CLAUDE_API_PROVIDER_TYPE })
|
|
8131
|
+
: { updatedUnstartedConversations: 0, skippedStartedConversations: 0, skippedActiveQueueConversations: 0, failedConversations: [] };
|
|
8132
|
+
const failedConversations = Array.isArray(reconciliationResult?.failedConversations) ? reconciliationResult.failedConversations : [];
|
|
8133
|
+
const settingsPayload = {
|
|
8134
|
+
...claudeApiSettingsService.describe(),
|
|
8135
|
+
reconciliation: {
|
|
8136
|
+
updatedUnstartedConversations: Number(reconciliationResult?.updatedUnstartedConversations || 0),
|
|
8137
|
+
skippedStartedConversations: Number(reconciliationResult?.skippedStartedConversations || 0),
|
|
8138
|
+
skippedActiveQueueConversations: Number(reconciliationResult?.skippedActiveQueueConversations || 0),
|
|
8139
|
+
failedConversations,
|
|
8140
|
+
},
|
|
8141
|
+
};
|
|
8142
|
+
io.emit('models_updated', buildModelCatalogWithProviders(getModelCatalogState()));
|
|
8143
|
+
io.emit('claude_api_settings_updated', settingsPayload);
|
|
8144
|
+
try { claudeApiStopLedger?.clearAll?.(); } catch {}
|
|
8145
|
+
try { checkClaudeApiCap('settings'); } catch {}
|
|
8146
|
+
// The key is saved whatever discovery said; Anthropic's refusal (a 401,
|
|
8147
|
+
// a key that needs a workspace) is the tab's status until a discovery
|
|
8148
|
+
// succeeds. A list dropped because the key changed meanwhile is no verdict.
|
|
8149
|
+
// A default the fallback list could not keep is named with the refusal.
|
|
8150
|
+
const previousModel = String(result.previousModel || '').trim();
|
|
8151
|
+
const defaultModelChanged = previousModel && settingsPayload.model && previousModel !== settingsPayload.model
|
|
8152
|
+
? { from: previousModel, to: settingsPayload.model }
|
|
8153
|
+
: null;
|
|
8154
|
+
const discoveryError = result.discover && discovery?.ok === false && discovery?.stale !== true
|
|
8155
|
+
? { error: discovery.error || 'Claude API model discovery failed', code: discovery.code || null, defaultModelChanged }
|
|
8156
|
+
: null;
|
|
8157
|
+
return res.json({
|
|
8158
|
+
ok: true,
|
|
8159
|
+
...settingsPayload,
|
|
8160
|
+
discoveryError,
|
|
8161
|
+
warning: failedConversations.length
|
|
8162
|
+
? `${failedConversations.length} unstarted conversation(s) could not switch provider.`
|
|
8163
|
+
: (discovery?.ok ? null : (discovery?.error || 'Claude API model discovery failed')),
|
|
8164
|
+
});
|
|
8165
|
+
});
|
|
8166
|
+
|
|
7892
8167
|
app.get('/api/settings/turn-ceiling', auth, (_req, res) => {
|
|
7893
8168
|
res.json({
|
|
7894
8169
|
ceilingMinutes: getTurnCeilingMinutes(),
|
|
@@ -8457,6 +8732,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8457
8732
|
const cursorSettings = getCursorProviderSettings();
|
|
8458
8733
|
const grokSettings = getGrokProviderSettings();
|
|
8459
8734
|
const deepseekSettings = getDeepSeekProviderSettings();
|
|
8735
|
+
const claudeApiSettings = getClaudeApiProviderSettings();
|
|
8460
8736
|
return {
|
|
8461
8737
|
...buildModelVariantCatalogPayload({
|
|
8462
8738
|
rows,
|
|
@@ -8493,6 +8769,16 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8493
8769
|
enabledModels: Array.isArray(deepseekSettings.models) ? deepseekSettings.models : [],
|
|
8494
8770
|
}
|
|
8495
8771
|
: null,
|
|
8772
|
+
// The Select Models → Claude API tab: its own list, "[1m]" ids included
|
|
8773
|
+
// (the tab shows them as on the Claude tab).
|
|
8774
|
+
claudeApiModels: claudeApiSettings?.enabled === true
|
|
8775
|
+
? {
|
|
8776
|
+
defaultModel: claudeApiSettings.model,
|
|
8777
|
+
availableModels: Array.isArray(claudeApiSettings.availableModels) ? claudeApiSettings.availableModels : [],
|
|
8778
|
+
enabledModels: Array.isArray(claudeApiSettings.models) ? claudeApiSettings.models : [],
|
|
8779
|
+
discovered: claudeApiSettings.discovered === true,
|
|
8780
|
+
}
|
|
8781
|
+
: null,
|
|
8496
8782
|
};
|
|
8497
8783
|
}
|
|
8498
8784
|
|
|
@@ -8512,6 +8798,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8512
8798
|
const cursorSettings = getCursorProviderSettings();
|
|
8513
8799
|
const grokSettings = getGrokProviderSettings();
|
|
8514
8800
|
const deepseekSettings = getDeepSeekProviderSettings();
|
|
8801
|
+
const claudeApiSettings = getClaudeApiProviderSettings();
|
|
8515
8802
|
const refreshTasks = [
|
|
8516
8803
|
refreshModelVariantCatalogFromCli(),
|
|
8517
8804
|
openAISettings?.enabled === true
|
|
@@ -8535,8 +8822,13 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8535
8822
|
deepseekSettings?.enabled === true && deepseekSettingsService
|
|
8536
8823
|
? deepseekSettingsService.refreshModels()
|
|
8537
8824
|
: Promise.resolve({ ok: true, skipped: true, models: [], error: null }),
|
|
8825
|
+
claudeApiSettings?.enabled === true && claudeApiSettingsService
|
|
8826
|
+
? claudeApiSettingsService.refreshModels()
|
|
8827
|
+
: Promise.resolve({ ok: true, skipped: true, models: [], error: null }),
|
|
8538
8828
|
];
|
|
8539
|
-
const [
|
|
8829
|
+
const [
|
|
8830
|
+
cliRefresh, openAIRefresh, claudeRefresh, cursorRefresh, grokRefresh, copilotDiscoveryRefresh, deepseekRefresh, claudeApiRefresh,
|
|
8831
|
+
] = await Promise.allSettled(refreshTasks);
|
|
8540
8832
|
if (cliRefresh.status === 'rejected') throw cliRefresh.reason;
|
|
8541
8833
|
const openAIModelDiscovery = openAIRefresh.status === 'fulfilled'
|
|
8542
8834
|
? openAIRefresh.value
|
|
@@ -8573,6 +8865,13 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8573
8865
|
models: Array.isArray(deepseekSettings?.models) ? deepseekSettings.models : [],
|
|
8574
8866
|
error: deepseekRefresh.reason?.message || 'DeepSeek model discovery failed',
|
|
8575
8867
|
};
|
|
8868
|
+
const claudeApiModelDiscovery = claudeApiRefresh.status === 'fulfilled'
|
|
8869
|
+
? claudeApiRefresh.value
|
|
8870
|
+
: {
|
|
8871
|
+
ok: false,
|
|
8872
|
+
models: Array.isArray(claudeApiSettings?.models) ? claudeApiSettings.models : [],
|
|
8873
|
+
error: claudeApiRefresh.reason?.message || 'Claude API model discovery failed',
|
|
8874
|
+
};
|
|
8576
8875
|
const copilotModelDiscovery = copilotDiscoveryRefresh.status === 'fulfilled'
|
|
8577
8876
|
? copilotDiscoveryRefresh.value
|
|
8578
8877
|
: { ok: false, models: [], error: copilotDiscoveryRefresh.reason?.message || 'Copilot model discovery failed' };
|
|
@@ -8587,6 +8886,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8587
8886
|
cursorModelDiscovery,
|
|
8588
8887
|
grokModelDiscovery,
|
|
8589
8888
|
deepseekModelDiscovery,
|
|
8889
|
+
claudeApiModelDiscovery,
|
|
8590
8890
|
copilotModelDiscovery,
|
|
8591
8891
|
});
|
|
8592
8892
|
} catch (error) {
|
|
@@ -8634,6 +8934,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8634
8934
|
// stay in models[] (a selection must not vanish) but pickers grey them.
|
|
8635
8935
|
unavailableModels: Array.isArray(modelState.unavailableModels) ? modelState.unavailableModels : [],
|
|
8636
8936
|
claudeContextTiersByModel: modelState.claudeContextTiersByModel || {},
|
|
8937
|
+
claudeApiContextTiersByModel: modelState.claudeApiContextTiersByModel || {},
|
|
8637
8938
|
providersByModel: modelState.providersByModel || {},
|
|
8638
8939
|
stale: modelState.stale,
|
|
8639
8940
|
metadataValid: modelState.metadataValid === true,
|
|
@@ -8684,7 +8985,8 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8684
8985
|
cursorBilling: null,
|
|
8685
8986
|
deepseekBalance: null,
|
|
8686
8987
|
};
|
|
8687
|
-
|
|
8988
|
+
// `claude-api`'s card is read from the relay's own books on every answer.
|
|
8989
|
+
const USAGE_PROVIDER_IDS = ['github', 'claude', 'claude-cloud', 'cursor', 'grok', 'deepseek', 'claude-api'];
|
|
8688
8990
|
|
|
8689
8991
|
function parseUsageProviders(raw) {
|
|
8690
8992
|
const text = Array.isArray(raw) ? raw.join(',') : String(raw ?? '');
|
|
@@ -8803,6 +9105,14 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8803
9105
|
}
|
|
8804
9106
|
}
|
|
8805
9107
|
|
|
9108
|
+
// Built from the database on every answer, so it is always live.
|
|
9109
|
+
let claudeApiCard = null;
|
|
9110
|
+
try {
|
|
9111
|
+
claudeApiCard = claudeApiPlanUsage?.buildCard?.() || null;
|
|
9112
|
+
} catch {
|
|
9113
|
+
claudeApiCard = null;
|
|
9114
|
+
}
|
|
9115
|
+
|
|
8806
9116
|
const report = planUsageService.buildReport({
|
|
8807
9117
|
copilotSummary: copilot.summary,
|
|
8808
9118
|
copilotError: copilot.error,
|
|
@@ -8829,6 +9139,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8829
9139
|
? { amount: deepseekSettings.startingBalance, setAt: deepseekSettings.startingBalanceSetAt }
|
|
8830
9140
|
: null,
|
|
8831
9141
|
deepseekActuals: deepseekEnabled ? readDeepSeekActuals() : null,
|
|
9142
|
+
claudeApiCard,
|
|
8832
9143
|
});
|
|
8833
9144
|
|
|
8834
9145
|
// Legacy top-level fields stay alongside `providers` so an older cached
|
|
@@ -8844,6 +9155,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8844
9155
|
cursor: cursorEntry.at,
|
|
8845
9156
|
grok: grokEntry.at,
|
|
8846
9157
|
deepseek: deepseekEntry.value?.fetchedAt || deepseekEntry.at,
|
|
9158
|
+
'claude-api': nowIso(),
|
|
8847
9159
|
},
|
|
8848
9160
|
});
|
|
8849
9161
|
});
|