@oar-sh/oar 0.9.17 → 0.9.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/extensions/web-relay/skills/tool-activity.mjs +5 -4
- package/CHANGELOG.md +192 -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 +3 -3
- 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-tools.md +4 -4
- 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 +645 -27
- 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 +327 -29
- package/server/server-runtime.mjs +363 -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 +83 -14
- 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 +29 -9
- 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,
|
|
@@ -855,6 +860,82 @@ export function buildModelCatalogWithDeepSeekProvider(modelState = {}, deepseekS
|
|
|
855
860
|
};
|
|
856
861
|
}
|
|
857
862
|
|
|
863
|
+
/**
|
|
864
|
+
* Claude API (provider `claude-api`) serves the Claude provider's model ids
|
|
865
|
+
* on an API key. Its layer tags them `claude-api` beside whatever else serves
|
|
866
|
+
* them and keeps its own efforts and context tiers: `[1m]` variants fold into
|
|
867
|
+
* their base model as a long_context tier, as on the Claude layer. An id is
|
|
868
|
+
* never enough to pick this provider; only an explicit `claude-api` does.
|
|
869
|
+
*/
|
|
870
|
+
export function buildModelCatalogWithClaudeApiProvider(modelState = {}, claudeApiSettings = {}) {
|
|
871
|
+
const configured = claudeApiSettings?.enabled === true;
|
|
872
|
+
const model = String(claudeApiSettings?.model || '').trim();
|
|
873
|
+
if (!configured || !model) return { ...modelState };
|
|
874
|
+
const listed = Array.isArray(claudeApiSettings?.models) ? claudeApiSettings.models : [];
|
|
875
|
+
const longContextBases = new Set();
|
|
876
|
+
const defaultContextBases = new Set();
|
|
877
|
+
const baseModels = [];
|
|
878
|
+
const seenBases = new Set();
|
|
879
|
+
for (const candidate of [model, ...listed]) {
|
|
880
|
+
const modelId = String(candidate || '').trim();
|
|
881
|
+
if (!modelId) continue;
|
|
882
|
+
const isLongContextVariant = CLAUDE_LONG_CONTEXT_SUFFIX_PATTERN.test(modelId);
|
|
883
|
+
const baseId = modelId.replace(CLAUDE_LONG_CONTEXT_SUFFIX_PATTERN, '');
|
|
884
|
+
if (!baseId) continue;
|
|
885
|
+
if (isLongContextVariant) longContextBases.add(baseId.toLowerCase());
|
|
886
|
+
else defaultContextBases.add(baseId.toLowerCase());
|
|
887
|
+
if (seenBases.has(baseId.toLowerCase())) continue;
|
|
888
|
+
seenBases.add(baseId.toLowerCase());
|
|
889
|
+
baseModels.push(baseId);
|
|
890
|
+
}
|
|
891
|
+
const models = Array.from(new Set([...(Array.isArray(modelState?.models) ? modelState.models : []), ...baseModels]));
|
|
892
|
+
const reasoningByModel = { ...(modelState?.reasoningByModel || {}) };
|
|
893
|
+
const claudeApiReasoningByModel = {};
|
|
894
|
+
const modelMetadataByModel = { ...(modelState?.modelMetadataByModel || {}) };
|
|
895
|
+
const providersByModel = { ...(modelState?.providersByModel || {}) };
|
|
896
|
+
const claudeApiContextTiersByModel = {};
|
|
897
|
+
const effortsByModel = claudeApiSettings?.effortsByModel && typeof claudeApiSettings.effortsByModel === 'object'
|
|
898
|
+
? claudeApiSettings.effortsByModel
|
|
899
|
+
: {};
|
|
900
|
+
for (const baseId of baseModels) {
|
|
901
|
+
const lowerKey = baseId.toLowerCase();
|
|
902
|
+
claudeApiContextTiersByModel[lowerKey] = [
|
|
903
|
+
...(defaultContextBases.has(lowerKey) ? [{ value: 'default' }] : []),
|
|
904
|
+
...(longContextBases.has(lowerKey) ? [{ value: 'long_context' }] : []),
|
|
905
|
+
];
|
|
906
|
+
const efforts = Array.isArray(effortsByModel[lowerKey]) && effortsByModel[lowerKey].length
|
|
907
|
+
? effortsByModel[lowerKey]
|
|
908
|
+
: effortsByModel[`${lowerKey}[1m]`];
|
|
909
|
+
const claudeApiEfforts = Array.isArray(efforts) && efforts.length
|
|
910
|
+
? withClaudeUltracodeTier(efforts)
|
|
911
|
+
: [...DEFAULT_CLAUDE_REASONING_EFFORTS];
|
|
912
|
+
claudeApiReasoningByModel[lowerKey] = [...claudeApiEfforts];
|
|
913
|
+
if (!Array.isArray(reasoningByModel[lowerKey]) || reasoningByModel[lowerKey].length === 0) {
|
|
914
|
+
reasoningByModel[lowerKey] = [...claudeApiEfforts];
|
|
915
|
+
}
|
|
916
|
+
const existingProviders = Array.isArray(providersByModel[baseId])
|
|
917
|
+
? providersByModel[baseId]
|
|
918
|
+
: (Array.isArray(providersByModel[lowerKey]) ? providersByModel[lowerKey] : []);
|
|
919
|
+
providersByModel[baseId] = Array.from(new Set([...existingProviders, CLAUDE_API_PROVIDER_TYPE]));
|
|
920
|
+
modelMetadataByModel[baseId] = {
|
|
921
|
+
...(modelMetadataByModel[baseId] || {}),
|
|
922
|
+
provider: modelMetadataByModel[baseId]?.provider || CLAUDE_API_PROVIDER_TYPE,
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
return {
|
|
926
|
+
...modelState,
|
|
927
|
+
models,
|
|
928
|
+
reasoningByModel,
|
|
929
|
+
reasoningByProvider: {
|
|
930
|
+
...(modelState?.reasoningByProvider || {}),
|
|
931
|
+
[CLAUDE_API_PROVIDER_TYPE]: claudeApiReasoningByModel,
|
|
932
|
+
},
|
|
933
|
+
modelMetadataByModel,
|
|
934
|
+
providersByModel,
|
|
935
|
+
claudeApiContextTiersByModel,
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
|
|
858
939
|
function normalizeRequestedProviderType(value = '') {
|
|
859
940
|
const normalized = String(value || '').trim().toLowerCase();
|
|
860
941
|
if (normalized === 'openai' || normalized === 'openai-byok') return 'openai';
|
|
@@ -865,6 +946,8 @@ function normalizeRequestedProviderType(value = '') {
|
|
|
865
946
|
if (normalized === 'cursor') return 'cursor';
|
|
866
947
|
if (normalized === 'grok' || normalized === 'xai' || normalized === 'xai-grok') return 'grok';
|
|
867
948
|
if (normalized === 'deepseek') return 'deepseek';
|
|
949
|
+
// Its own provider, never read as `claude`.
|
|
950
|
+
if (normalized === 'claude-api') return 'claude-api';
|
|
868
951
|
if (normalized === 'github' || normalized === 'github-copilot') return 'github';
|
|
869
952
|
return '';
|
|
870
953
|
}
|
|
@@ -1506,9 +1589,10 @@ export function buildConversationSessionRootPayload({
|
|
|
1506
1589
|
}
|
|
1507
1590
|
// Claude sessions have no ~/.copilot/session-state entry — only the Copilot
|
|
1508
1591
|
// CLI creates those — so they resolve against the Agent SDK's own layout and
|
|
1509
|
-
// never fall through to the branch below.
|
|
1592
|
+
// never fall through to the branch below. A Claude API session is a Claude
|
|
1593
|
+
// session on an API key, in the same place.
|
|
1510
1594
|
const claudeCodeProvider = String(providerType || '').trim().toLowerCase();
|
|
1511
|
-
if (claudeCodeProvider === 'claude' || claudeCodeProvider === 'deepseek') {
|
|
1595
|
+
if (claudeCodeProvider === 'claude' || claudeCodeProvider === 'deepseek' || claudeCodeProvider === 'claude-api') {
|
|
1512
1596
|
const nativeSessionId = String(claudeNativeSessionId || '').trim();
|
|
1513
1597
|
const resolveRoot = claudeCodeProvider === 'deepseek' ? resolveDeepSeekSessionRoot : resolveClaudeSessionRoot;
|
|
1514
1598
|
if (!nativeSessionId || typeof resolveRoot !== 'function') return null;
|
|
@@ -2138,6 +2222,20 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2138
2222
|
// attribution (services/deepseek-settings-service.mjs). Absent in older
|
|
2139
2223
|
// wiring and most tests, where the provider is simply off.
|
|
2140
2224
|
deepseekSettingsService = null,
|
|
2225
|
+
// Claude API (provider `claude-api`): its key, models and monthly cap
|
|
2226
|
+
// (services/claude-api-settings-service.mjs). Absent in older wiring and
|
|
2227
|
+
// most tests, where the provider is simply off.
|
|
2228
|
+
claudeApiSettingsService = null,
|
|
2229
|
+
// The Claude API card and monthly cap (services/plan-usage-claude-api.mjs),
|
|
2230
|
+
// and the cap's one seam (`{ reached, error? }`; a throw counts as
|
|
2231
|
+
// reached). Absent in older wiring and most tests: no card, no cap.
|
|
2232
|
+
claudeApiPlanUsage = null,
|
|
2233
|
+
getClaudeApiCapState = () => ({ reached: false }),
|
|
2234
|
+
// Asks whether the cap flipped after the Claude API settings changed.
|
|
2235
|
+
checkClaudeApiCap = () => {},
|
|
2236
|
+
// The once-per-episode notes for stopped self-started turns on the key
|
|
2237
|
+
// (plan-usage-claude-api.mjs): a saved setting starts every chat afresh.
|
|
2238
|
+
claudeApiStopLedger = null,
|
|
2141
2239
|
reconcileUnstartedConversationProviders = async () => ({
|
|
2142
2240
|
updatedUnstartedConversations: 0,
|
|
2143
2241
|
skippedStartedConversations: 0,
|
|
@@ -2248,6 +2346,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2248
2346
|
hostSuspendService = null,
|
|
2249
2347
|
collectHostActivity = null,
|
|
2250
2348
|
usageLimitPauseService = null,
|
|
2349
|
+
claudeBillingSwitchService = null,
|
|
2251
2350
|
hostSuspendPlatform = process.platform,
|
|
2252
2351
|
// Remote relays (server/services/remote-relay-inbound.mjs): origin on
|
|
2253
2352
|
// conversations another relay's agent created, and the inbound switch.
|
|
@@ -2504,20 +2603,28 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2504
2603
|
|| { configured: false, enabled: false, model: DEFAULT_DEEPSEEK_MODEL, models: [], availableModels: [] };
|
|
2505
2604
|
}
|
|
2506
2605
|
|
|
2606
|
+
function getClaudeApiProviderSettings() {
|
|
2607
|
+
return claudeApiSettingsService?.getSettings?.()
|
|
2608
|
+
|| { configured: false, enabled: false, model: '', models: [], availableModels: [], effortsByModel: {} };
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2507
2611
|
// Layer every enabled provider's models onto the base Copilot catalog.
|
|
2508
2612
|
function buildModelCatalogWithProviders(modelState, openAISettings = getOpenAIProviderSettings()) {
|
|
2509
|
-
return
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2613
|
+
return buildModelCatalogWithClaudeApiProvider(
|
|
2614
|
+
buildModelCatalogWithDeepSeekProvider(
|
|
2615
|
+
buildModelCatalogWithGrokProvider(
|
|
2616
|
+
buildModelCatalogWithCursorProvider(
|
|
2617
|
+
buildModelCatalogWithClaudeProvider(
|
|
2618
|
+
buildModelCatalogWithOpenAIProvider(modelState, openAISettings),
|
|
2619
|
+
getClaudeProviderSettings(),
|
|
2620
|
+
),
|
|
2621
|
+
getCursorProviderSettings(),
|
|
2515
2622
|
),
|
|
2516
|
-
|
|
2623
|
+
getGrokProviderSettings(),
|
|
2517
2624
|
),
|
|
2518
|
-
|
|
2625
|
+
getDeepSeekProviderSettings(),
|
|
2519
2626
|
),
|
|
2520
|
-
|
|
2627
|
+
getClaudeApiProviderSettings(),
|
|
2521
2628
|
);
|
|
2522
2629
|
}
|
|
2523
2630
|
|
|
@@ -2708,7 +2815,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2708
2815
|
async function deleteCliSession({ conversation, conversationId, sdkSessionId, runtimeSession }) {
|
|
2709
2816
|
const provider = String(runtimeSession?.provider_type || '').trim().toLowerCase();
|
|
2710
2817
|
try {
|
|
2711
|
-
if (provider === 'claude') return deleteClaudeCliSession({ conversation, runtimeSession });
|
|
2818
|
+
if (provider === 'claude' || provider === 'claude-api') return deleteClaudeCliSession({ conversation, runtimeSession });
|
|
2712
2819
|
if (provider === 'deepseek') return deleteDeepSeekCliSession({ conversation, runtimeSession });
|
|
2713
2820
|
if (!sdkSessionId) return { deleted: null };
|
|
2714
2821
|
if (!provider || provider === 'github' || provider === 'openai') {
|
|
@@ -2782,7 +2889,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2782
2889
|
/**
|
|
2783
2890
|
* The `group` field of a single conversation payload: the summary and the
|
|
2784
2891
|
* conversation's role. The live members, with what each is doing, only
|
|
2785
|
-
* when asked for (`groupMembers=1`, the
|
|
2892
|
+
* when asked for (`groupMembers=1`, the relay_tool's read_session)
|
|
2786
2893
|
* and never for a paired relay's agent: the browser does not read them,
|
|
2787
2894
|
* and archived members' titles stay on this relay.
|
|
2788
2895
|
*/
|
|
@@ -3644,9 +3751,11 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
3644
3751
|
// The cursor and grok workers post the same Claude-shaped usage blob, and
|
|
3645
3752
|
// so does the Claude Cloud worker (the cloud session's context and token
|
|
3646
3753
|
// usage), and so does the DeepSeek worker (the Claude one, pointed at
|
|
3647
|
-
// DeepSeek), so
|
|
3754
|
+
// DeepSeek), and so does the Claude API one (the Claude worker on an API
|
|
3755
|
+
// key), so all six providers share the stored reader.
|
|
3648
3756
|
const usesStoredContextUsage = providerType === 'claude' || providerType === 'cursor'
|
|
3649
|
-
|| providerType === 'grok' || providerType === 'claude-cloud' || providerType === 'deepseek'
|
|
3757
|
+
|| providerType === 'grok' || providerType === 'claude-cloud' || providerType === 'deepseek'
|
|
3758
|
+
|| providerType === 'claude-api';
|
|
3650
3759
|
|
|
3651
3760
|
const parsed = usesStoredContextUsage
|
|
3652
3761
|
? (() => {
|
|
@@ -3675,12 +3784,12 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
3675
3784
|
String(runtimeSession?.conversation_id || '').trim() || lookupId,
|
|
3676
3785
|
);
|
|
3677
3786
|
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.
|
|
3787
|
+
// Only the local Claude worker (and the DeepSeek and Claude API ones, the
|
|
3788
|
+
// same runner) runs a CLI whose window the slider sets; it reports that
|
|
3789
|
+
// CLI on its heartbeat's steering snapshot.
|
|
3681
3790
|
const windowStatus = resolveAutoCompactWindowStatus({
|
|
3682
3791
|
storedWindow: autoCompactWindow,
|
|
3683
|
-
steering: (providerType === 'claude' || providerType === 'deepseek') && copilotSessionId
|
|
3792
|
+
steering: (providerType === 'claude' || providerType === 'deepseek' || providerType === 'claude-api') && copilotSessionId
|
|
3684
3793
|
? sessionWorkerRegistry?.getWorker?.(copilotSessionId)?.steering || null
|
|
3685
3794
|
: null,
|
|
3686
3795
|
});
|
|
@@ -3700,7 +3809,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
3700
3809
|
// What this folder's Claude commits say (the 🧠 modal's override row).
|
|
3701
3810
|
// A Claude Cloud chat has the row when it was started from a folder,
|
|
3702
3811
|
// with its own model in the example.
|
|
3703
|
-
...(providerType === 'claude'
|
|
3812
|
+
...(providerType === 'claude' || providerType === 'claude-api'
|
|
3704
3813
|
? { attribution: workspaceRootAttributionFor(conversationRow) }
|
|
3705
3814
|
: {}),
|
|
3706
3815
|
...(providerType === 'claude-cloud' && cloudConversationFolder(conversationRow)
|
|
@@ -4728,6 +4837,9 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
4728
4837
|
try { return getTaskStallMinutes(); } catch { return DEFAULT_TASK_STALL_MINUTES; }
|
|
4729
4838
|
})(),
|
|
4730
4839
|
usageLimitPause: usageLimitPauseService?.getPause?.(resolvedConversationId) || null,
|
|
4840
|
+
// A Claude chat's billing (subscription or API key) and whether the
|
|
4841
|
+
// composer's toggle may switch it now, with the reason when not.
|
|
4842
|
+
billing: claudeBillingSwitchService?.billingState?.(resolvedConversationId) || null,
|
|
4731
4843
|
previews: listConversationPreviews?.(resolvedConversationId) || [],
|
|
4732
4844
|
// Every pin of the conversation, also those outside this history page.
|
|
4733
4845
|
pins: listConversationPins(resolvedConversationId),
|
|
@@ -5014,7 +5126,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
5014
5126
|
return res.json({ ok: true, ...persisted });
|
|
5015
5127
|
});
|
|
5016
5128
|
|
|
5017
|
-
// ─── PATCH /api/conversation/:id/settings — the
|
|
5129
|
+
// ─── PATCH /api/conversation/:id/settings — the relay_tool's update_session ───
|
|
5018
5130
|
//
|
|
5019
5131
|
// `{ settings: { model, effort, mode, auto_compact_window, title, group,
|
|
5020
5132
|
// thinking_enabled, thinking_display, cwd }, dry_run, owner_approved }`.
|
|
@@ -5135,7 +5247,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
5135
5247
|
plan.preferences.thinkingDisplay = wanted.thinking_display;
|
|
5136
5248
|
change('thinking_display');
|
|
5137
5249
|
}
|
|
5138
|
-
if (provider !== 'claude' && ['auto_compact_window', 'thinking_enabled', 'thinking_display'].some((key) => wanted[key] !== undefined)) {
|
|
5250
|
+
if (provider !== 'claude' && provider !== 'claude-api' && ['auto_compact_window', 'thinking_enabled', 'thinking_display'].some((key) => wanted[key] !== undefined)) {
|
|
5139
5251
|
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
5252
|
}
|
|
5141
5253
|
if (wanted.title !== undefined) {
|
|
@@ -6267,6 +6379,35 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6267
6379
|
environmentId: cloudEnvironmentId,
|
|
6268
6380
|
});
|
|
6269
6381
|
}
|
|
6382
|
+
// Claude API is asked for by name only, like Claude Cloud: its model ids
|
|
6383
|
+
// are the Claude provider's own, so a model never selects it. Refused
|
|
6384
|
+
// first while it has no key or is switched off.
|
|
6385
|
+
const useClaudeApiProvider = requestedProviderType === CLAUDE_API_PROVIDER_TYPE;
|
|
6386
|
+
const claudeApiSettings = useClaudeApiProvider ? getClaudeApiProviderSettings() : null;
|
|
6387
|
+
if (useClaudeApiProvider && claudeApiSettings?.enabled !== true) {
|
|
6388
|
+
return res.status(400).json({
|
|
6389
|
+
ok: false,
|
|
6390
|
+
error: claudeApiSettings?.configured === true
|
|
6391
|
+
? 'Claude API is switched off for new conversations.'
|
|
6392
|
+
: 'Claude API key is not configured.',
|
|
6393
|
+
code: 'CLAUDE_API_NOT_CONFIGURED',
|
|
6394
|
+
});
|
|
6395
|
+
}
|
|
6396
|
+
// This month's cap (plan decision 12): no new Claude API chat while it is
|
|
6397
|
+
// reached, for New Chat and an agent's create_session alike. A cap that
|
|
6398
|
+
// cannot be read counts as reached.
|
|
6399
|
+
if (useClaudeApiProvider) {
|
|
6400
|
+
let cap = null;
|
|
6401
|
+
try { cap = getClaudeApiCapState() || null; } catch { cap = { reached: true }; }
|
|
6402
|
+
if (cap?.reached === true) {
|
|
6403
|
+
return res.status(409).json({
|
|
6404
|
+
ok: false,
|
|
6405
|
+
// The remedy names the place, so the reason does not.
|
|
6406
|
+
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.`,
|
|
6407
|
+
code: 'CLAUDE_API_CAP_REACHED',
|
|
6408
|
+
});
|
|
6409
|
+
}
|
|
6410
|
+
}
|
|
6270
6411
|
const modelState = buildModelCatalogWithProviders(
|
|
6271
6412
|
getModelCatalogState(),
|
|
6272
6413
|
openAISettings,
|
|
@@ -6312,6 +6453,13 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6312
6453
|
...(Array.isArray(deepseekSettings?.models) ? deepseekSettings.models : []),
|
|
6313
6454
|
].map((value) => String(value || '').trim()).filter(Boolean);
|
|
6314
6455
|
const availableDeepSeekModels = new Set(deepseekModelCatalog.map((value) => value.toLowerCase()));
|
|
6456
|
+
// Base ids of "[1m]" variants count too, as for the Claude provider.
|
|
6457
|
+
const availableClaudeApiModels = new Set((useClaudeApiProvider ? [
|
|
6458
|
+
String(claudeApiSettings?.model || '').trim(),
|
|
6459
|
+
...(Array.isArray(claudeApiSettings?.models) ? claudeApiSettings.models : []),
|
|
6460
|
+
] : []).map((value) => String(value || '').trim()).filter(Boolean)
|
|
6461
|
+
.flatMap((value) => [value, value.replace(CLAUDE_LONG_CONTEXT_SUFFIX_PATTERN, '')])
|
|
6462
|
+
.filter(Boolean));
|
|
6315
6463
|
const useOpenAIProvider = requestedProviderType === 'openai'
|
|
6316
6464
|
|| requestedProviderType === 'openai-image'
|
|
6317
6465
|
|| (
|
|
@@ -6376,6 +6524,12 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6376
6524
|
configuredModel: claudeCloudSettings.defaultModel,
|
|
6377
6525
|
availableModels: Array.isArray(claudeCloudSettings.models) ? claudeCloudSettings.models : [],
|
|
6378
6526
|
})
|
|
6527
|
+
: useClaudeApiProvider
|
|
6528
|
+
? resolveProviderModelSelection({
|
|
6529
|
+
requestedModel: providerRequestedModel(availableClaudeApiModels),
|
|
6530
|
+
configuredModel: claudeApiSettings.model,
|
|
6531
|
+
availableModels: availableClaudeApiModels,
|
|
6532
|
+
})
|
|
6379
6533
|
: useOpenAIProvider
|
|
6380
6534
|
? resolveProviderModelSelection({
|
|
6381
6535
|
requestedModel: providerRequestedModel(availableOpenAIModels),
|
|
@@ -6414,11 +6568,15 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6414
6568
|
if (providerModelSelection && !providerModelSelection.ok) {
|
|
6415
6569
|
const providerLabel = useClaudeCloudProvider
|
|
6416
6570
|
? 'Claude Cloud'
|
|
6571
|
+
: useClaudeApiProvider
|
|
6572
|
+
? CLAUDE_API_PROVIDER_LABEL
|
|
6417
6573
|
: (useOpenAIProvider
|
|
6418
6574
|
? 'OpenAI'
|
|
6419
6575
|
: (useClaudeProvider ? 'Claude' : (useCursorProvider ? 'Cursor' : (useGrokProvider ? 'Grok' : 'DeepSeek'))));
|
|
6420
6576
|
const providerCode = useClaudeCloudProvider
|
|
6421
6577
|
? 'CLAUDE_CLOUD_MODEL_UNAVAILABLE'
|
|
6578
|
+
: useClaudeApiProvider
|
|
6579
|
+
? 'CLAUDE_API_MODEL_UNAVAILABLE'
|
|
6422
6580
|
: (useOpenAIProvider
|
|
6423
6581
|
? 'OPENAI_MODEL_UNAVAILABLE'
|
|
6424
6582
|
: (useClaudeProvider
|
|
@@ -6445,7 +6603,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6445
6603
|
if (
|
|
6446
6604
|
remoteRequest.remote
|
|
6447
6605
|
&& !useClaudeCloudProvider && !useOpenAIProvider && !useClaudeProvider && !useCursorProvider && !useGrokProvider
|
|
6448
|
-
&& !useDeepSeekProvider
|
|
6606
|
+
&& !useDeepSeekProvider && !useClaudeApiProvider
|
|
6449
6607
|
) {
|
|
6450
6608
|
const mergedModels = Array.isArray(modelState?.models) ? modelState.models : [];
|
|
6451
6609
|
const providersByModel = modelState?.providersByModel && typeof modelState.providersByModel === 'object'
|
|
@@ -6574,7 +6732,10 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6574
6732
|
code: 'OPENAI_IMAGE_MODEL_REQUIRED',
|
|
6575
6733
|
});
|
|
6576
6734
|
}
|
|
6577
|
-
if (
|
|
6735
|
+
if (
|
|
6736
|
+
!useClaudeCloudProvider && !useOpenAIProvider && !useClaudeProvider && !useCursorProvider && !useGrokProvider
|
|
6737
|
+
&& !useDeepSeekProvider && !useClaudeApiProvider
|
|
6738
|
+
) {
|
|
6578
6739
|
const selectedProviders = Array.isArray(modelState?.providersByModel?.[String(selectedModel || '').trim().toLowerCase()])
|
|
6579
6740
|
? modelState.providersByModel[String(selectedModel || '').trim().toLowerCase()]
|
|
6580
6741
|
: [];
|
|
@@ -6587,8 +6748,12 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6587
6748
|
code: 'OPENAI_PROVIDER_REQUIRED',
|
|
6588
6749
|
});
|
|
6589
6750
|
}
|
|
6590
|
-
|
|
6591
|
-
|
|
6751
|
+
// Claude API serves the Claude provider's ids: a model both list is
|
|
6752
|
+
// still Claude's, and one only Claude API lists names that provider.
|
|
6753
|
+
const selectedProviderIds = selectedProviders.map((provider) => String(provider || '').trim().toLowerCase());
|
|
6754
|
+
const claudeOnlySelection = selectedProviderIds.length > 0
|
|
6755
|
+
&& selectedProviderIds.includes('claude')
|
|
6756
|
+
&& selectedProviderIds.every((provider) => provider === 'claude' || provider === CLAUDE_API_PROVIDER_TYPE);
|
|
6592
6757
|
if (claudeOnlySelection) {
|
|
6593
6758
|
return res.status(400).json({
|
|
6594
6759
|
ok: false,
|
|
@@ -6596,6 +6761,15 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6596
6761
|
code: 'CLAUDE_PROVIDER_REQUIRED',
|
|
6597
6762
|
});
|
|
6598
6763
|
}
|
|
6764
|
+
const claudeApiOnlySelection = selectedProviderIds.length > 0
|
|
6765
|
+
&& selectedProviderIds.every((provider) => provider === CLAUDE_API_PROVIDER_TYPE);
|
|
6766
|
+
if (claudeApiOnlySelection) {
|
|
6767
|
+
return res.status(400).json({
|
|
6768
|
+
ok: false,
|
|
6769
|
+
error: `Model "${selectedModel}" requires the Claude API provider`,
|
|
6770
|
+
code: 'CLAUDE_API_PROVIDER_REQUIRED',
|
|
6771
|
+
});
|
|
6772
|
+
}
|
|
6599
6773
|
const cursorOnlySelection = selectedProviders.length > 0
|
|
6600
6774
|
&& selectedProviders.every((provider) => String(provider || '').trim().toLowerCase() === 'cursor');
|
|
6601
6775
|
if (cursorOnlySelection) {
|
|
@@ -6627,6 +6801,8 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6627
6801
|
|
|
6628
6802
|
const bootstrapProviderType = useClaudeCloudProvider
|
|
6629
6803
|
? 'claude-cloud'
|
|
6804
|
+
: useClaudeApiProvider
|
|
6805
|
+
? CLAUDE_API_PROVIDER_TYPE
|
|
6630
6806
|
: (useOpenAIProvider
|
|
6631
6807
|
? 'openai'
|
|
6632
6808
|
: (useClaudeProvider
|
|
@@ -6657,6 +6833,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6657
6833
|
model: selectedModel,
|
|
6658
6834
|
cursorSettings,
|
|
6659
6835
|
claudeSettings,
|
|
6836
|
+
claudeApiSettings,
|
|
6660
6837
|
grokSettings,
|
|
6661
6838
|
reasoningByModel: modelState?.reasoningByModel || null,
|
|
6662
6839
|
}),
|
|
@@ -6701,7 +6878,10 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6701
6878
|
{
|
|
6702
6879
|
assignConfiguredProvider: true,
|
|
6703
6880
|
providerType: bootstrapProviderType,
|
|
6704
|
-
providerModel: (
|
|
6881
|
+
providerModel: (
|
|
6882
|
+
useClaudeCloudProvider || useOpenAIProvider || useClaudeProvider || useCursorProvider || useGrokProvider
|
|
6883
|
+
|| useDeepSeekProvider || useClaudeApiProvider
|
|
6884
|
+
) ? selectedModel : null,
|
|
6705
6885
|
},
|
|
6706
6886
|
);
|
|
6707
6887
|
// What the cloud session will clone, stored with the conversation so
|
|
@@ -7680,6 +7860,11 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
7680
7860
|
// workers of the provider keep the old key and fail each turn after the
|
|
7681
7861
|
// rotation. Stops them; the primer or the next delivery respawns them with
|
|
7682
7862
|
// the fresh key from settings.
|
|
7863
|
+
//
|
|
7864
|
+
// The workers are picked by what they run: a worker that states its kind
|
|
7865
|
+
// is taken at its word (a Claude chat switched to the API key may still
|
|
7866
|
+
// have its subscription worker finishing a turn, and one switched back
|
|
7867
|
+
// its API worker); one that does not is taken at its chat's provider.
|
|
7683
7868
|
async function stopLiveProviderWorkersForKeyRotation(providerType) {
|
|
7684
7869
|
const rows = db.prepare(`
|
|
7685
7870
|
SELECT DISTINCT NULLIF(sdk_session_id, '') AS sdk_session_id
|
|
@@ -7687,8 +7872,16 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
7687
7872
|
WHERE LOWER(COALESCE(provider_type, '')) = ?
|
|
7688
7873
|
AND NULLIF(sdk_session_id, '') IS NOT NULL
|
|
7689
7874
|
`).all(providerType);
|
|
7875
|
+
const sids = new Set();
|
|
7876
|
+
for (const worker of sessionWorkerRegistry?.listWorkers?.() || []) {
|
|
7877
|
+
if (worker?.kind === providerType && worker.sdkSessionId) sids.add(worker.sdkSessionId);
|
|
7878
|
+
}
|
|
7690
7879
|
for (const row of rows) {
|
|
7691
7880
|
const sid = String(row.sdk_session_id || '').trim();
|
|
7881
|
+
const kind = sid ? (sessionWorkerRegistry?.getWorker?.(sid)?.kind || null) : null;
|
|
7882
|
+
if (sid && (!kind || kind === providerType)) sids.add(sid);
|
|
7883
|
+
}
|
|
7884
|
+
for (const sid of sids) {
|
|
7692
7885
|
if (!sid) continue;
|
|
7693
7886
|
const workerState = sessionWorkerSupervisor?.getWorkerState?.(sid) || null;
|
|
7694
7887
|
if (!workerState) continue;
|
|
@@ -7889,6 +8082,74 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
7889
8082
|
});
|
|
7890
8083
|
});
|
|
7891
8084
|
|
|
8085
|
+
// --- Claude API ----------------------------------------------------------
|
|
8086
|
+
// The key is written here and never read back: GET and every answer carry
|
|
8087
|
+
// `configured` only. A pasted Claude login token or Admin key is refused by
|
|
8088
|
+
// its prefix. A rotated key reaches a worker only at spawn, so the live
|
|
8089
|
+
// Claude API workers are stopped; the next message starts them again.
|
|
8090
|
+
app.get('/api/settings/claude-api', auth, (_req, res) => {
|
|
8091
|
+
if (!claudeApiSettingsService) return res.status(501).json({ error: 'Claude API settings are unavailable' });
|
|
8092
|
+
return res.json(claudeApiSettingsService.describe());
|
|
8093
|
+
});
|
|
8094
|
+
|
|
8095
|
+
app.post('/api/settings/claude-api', auth, async (req, res) => {
|
|
8096
|
+
if (!claudeApiSettingsService) return res.status(501).json({ error: 'Claude API settings are unavailable' });
|
|
8097
|
+
const parsed = parseClaudeApiSettingsUpdateRequest(req.body);
|
|
8098
|
+
if (!parsed.ok) return res.status(400).json({ error: parsed.error, code: parsed.code || null });
|
|
8099
|
+
const result = claudeApiSettingsService.update(parsed);
|
|
8100
|
+
if (!result?.ok) {
|
|
8101
|
+
return res.status(result?.code === 'claude-api-key-removal-blocked' ? 409 : 400).json({
|
|
8102
|
+
error: result?.error || 'Failed to update Claude API settings',
|
|
8103
|
+
code: result?.code || null,
|
|
8104
|
+
activeConversationCount: Number(result?.activeConversationCount || 0),
|
|
8105
|
+
startedConversationCount: Number(result?.startedConversationCount || 0),
|
|
8106
|
+
activeQueueConversationCount: Number(result?.activeQueueConversationCount || 0),
|
|
8107
|
+
});
|
|
8108
|
+
}
|
|
8109
|
+
if (result.keyChanged) await stopLiveProviderWorkersForKeyRotation(CLAUDE_API_PROVIDER_TYPE);
|
|
8110
|
+
const discovery = result.discover
|
|
8111
|
+
? await claudeApiSettingsService.refreshModels()
|
|
8112
|
+
: { ok: true, models: [], error: null };
|
|
8113
|
+
// Removing the key or switching the provider off moves unstarted Claude
|
|
8114
|
+
// API conversations back to the default provider, as for Claude itself.
|
|
8115
|
+
const reconciliationResult = result.removed === true || result.disabled === true
|
|
8116
|
+
? await reconcileUnstartedConversationProviders({ enabled: false, model: '', provider: CLAUDE_API_PROVIDER_TYPE })
|
|
8117
|
+
: { updatedUnstartedConversations: 0, skippedStartedConversations: 0, skippedActiveQueueConversations: 0, failedConversations: [] };
|
|
8118
|
+
const failedConversations = Array.isArray(reconciliationResult?.failedConversations) ? reconciliationResult.failedConversations : [];
|
|
8119
|
+
const settingsPayload = {
|
|
8120
|
+
...claudeApiSettingsService.describe(),
|
|
8121
|
+
reconciliation: {
|
|
8122
|
+
updatedUnstartedConversations: Number(reconciliationResult?.updatedUnstartedConversations || 0),
|
|
8123
|
+
skippedStartedConversations: Number(reconciliationResult?.skippedStartedConversations || 0),
|
|
8124
|
+
skippedActiveQueueConversations: Number(reconciliationResult?.skippedActiveQueueConversations || 0),
|
|
8125
|
+
failedConversations,
|
|
8126
|
+
},
|
|
8127
|
+
};
|
|
8128
|
+
io.emit('models_updated', buildModelCatalogWithProviders(getModelCatalogState()));
|
|
8129
|
+
io.emit('claude_api_settings_updated', settingsPayload);
|
|
8130
|
+
try { claudeApiStopLedger?.clearAll?.(); } catch {}
|
|
8131
|
+
try { checkClaudeApiCap('settings'); } catch {}
|
|
8132
|
+
// The key is saved whatever discovery said; Anthropic's refusal (a 401,
|
|
8133
|
+
// a key that needs a workspace) is the tab's status until a discovery
|
|
8134
|
+
// succeeds. A list dropped because the key changed meanwhile is no verdict.
|
|
8135
|
+
// A default the fallback list could not keep is named with the refusal.
|
|
8136
|
+
const previousModel = String(result.previousModel || '').trim();
|
|
8137
|
+
const defaultModelChanged = previousModel && settingsPayload.model && previousModel !== settingsPayload.model
|
|
8138
|
+
? { from: previousModel, to: settingsPayload.model }
|
|
8139
|
+
: null;
|
|
8140
|
+
const discoveryError = result.discover && discovery?.ok === false && discovery?.stale !== true
|
|
8141
|
+
? { error: discovery.error || 'Claude API model discovery failed', code: discovery.code || null, defaultModelChanged }
|
|
8142
|
+
: null;
|
|
8143
|
+
return res.json({
|
|
8144
|
+
ok: true,
|
|
8145
|
+
...settingsPayload,
|
|
8146
|
+
discoveryError,
|
|
8147
|
+
warning: failedConversations.length
|
|
8148
|
+
? `${failedConversations.length} unstarted conversation(s) could not switch provider.`
|
|
8149
|
+
: (discovery?.ok ? null : (discovery?.error || 'Claude API model discovery failed')),
|
|
8150
|
+
});
|
|
8151
|
+
});
|
|
8152
|
+
|
|
7892
8153
|
app.get('/api/settings/turn-ceiling', auth, (_req, res) => {
|
|
7893
8154
|
res.json({
|
|
7894
8155
|
ceilingMinutes: getTurnCeilingMinutes(),
|
|
@@ -8457,6 +8718,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8457
8718
|
const cursorSettings = getCursorProviderSettings();
|
|
8458
8719
|
const grokSettings = getGrokProviderSettings();
|
|
8459
8720
|
const deepseekSettings = getDeepSeekProviderSettings();
|
|
8721
|
+
const claudeApiSettings = getClaudeApiProviderSettings();
|
|
8460
8722
|
return {
|
|
8461
8723
|
...buildModelVariantCatalogPayload({
|
|
8462
8724
|
rows,
|
|
@@ -8493,6 +8755,16 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8493
8755
|
enabledModels: Array.isArray(deepseekSettings.models) ? deepseekSettings.models : [],
|
|
8494
8756
|
}
|
|
8495
8757
|
: null,
|
|
8758
|
+
// The Select Models → Claude API tab: its own list, "[1m]" ids included
|
|
8759
|
+
// (the tab shows them as on the Claude tab).
|
|
8760
|
+
claudeApiModels: claudeApiSettings?.enabled === true
|
|
8761
|
+
? {
|
|
8762
|
+
defaultModel: claudeApiSettings.model,
|
|
8763
|
+
availableModels: Array.isArray(claudeApiSettings.availableModels) ? claudeApiSettings.availableModels : [],
|
|
8764
|
+
enabledModels: Array.isArray(claudeApiSettings.models) ? claudeApiSettings.models : [],
|
|
8765
|
+
discovered: claudeApiSettings.discovered === true,
|
|
8766
|
+
}
|
|
8767
|
+
: null,
|
|
8496
8768
|
};
|
|
8497
8769
|
}
|
|
8498
8770
|
|
|
@@ -8512,6 +8784,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8512
8784
|
const cursorSettings = getCursorProviderSettings();
|
|
8513
8785
|
const grokSettings = getGrokProviderSettings();
|
|
8514
8786
|
const deepseekSettings = getDeepSeekProviderSettings();
|
|
8787
|
+
const claudeApiSettings = getClaudeApiProviderSettings();
|
|
8515
8788
|
const refreshTasks = [
|
|
8516
8789
|
refreshModelVariantCatalogFromCli(),
|
|
8517
8790
|
openAISettings?.enabled === true
|
|
@@ -8535,8 +8808,13 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8535
8808
|
deepseekSettings?.enabled === true && deepseekSettingsService
|
|
8536
8809
|
? deepseekSettingsService.refreshModels()
|
|
8537
8810
|
: Promise.resolve({ ok: true, skipped: true, models: [], error: null }),
|
|
8811
|
+
claudeApiSettings?.enabled === true && claudeApiSettingsService
|
|
8812
|
+
? claudeApiSettingsService.refreshModels()
|
|
8813
|
+
: Promise.resolve({ ok: true, skipped: true, models: [], error: null }),
|
|
8538
8814
|
];
|
|
8539
|
-
const [
|
|
8815
|
+
const [
|
|
8816
|
+
cliRefresh, openAIRefresh, claudeRefresh, cursorRefresh, grokRefresh, copilotDiscoveryRefresh, deepseekRefresh, claudeApiRefresh,
|
|
8817
|
+
] = await Promise.allSettled(refreshTasks);
|
|
8540
8818
|
if (cliRefresh.status === 'rejected') throw cliRefresh.reason;
|
|
8541
8819
|
const openAIModelDiscovery = openAIRefresh.status === 'fulfilled'
|
|
8542
8820
|
? openAIRefresh.value
|
|
@@ -8573,6 +8851,13 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8573
8851
|
models: Array.isArray(deepseekSettings?.models) ? deepseekSettings.models : [],
|
|
8574
8852
|
error: deepseekRefresh.reason?.message || 'DeepSeek model discovery failed',
|
|
8575
8853
|
};
|
|
8854
|
+
const claudeApiModelDiscovery = claudeApiRefresh.status === 'fulfilled'
|
|
8855
|
+
? claudeApiRefresh.value
|
|
8856
|
+
: {
|
|
8857
|
+
ok: false,
|
|
8858
|
+
models: Array.isArray(claudeApiSettings?.models) ? claudeApiSettings.models : [],
|
|
8859
|
+
error: claudeApiRefresh.reason?.message || 'Claude API model discovery failed',
|
|
8860
|
+
};
|
|
8576
8861
|
const copilotModelDiscovery = copilotDiscoveryRefresh.status === 'fulfilled'
|
|
8577
8862
|
? copilotDiscoveryRefresh.value
|
|
8578
8863
|
: { ok: false, models: [], error: copilotDiscoveryRefresh.reason?.message || 'Copilot model discovery failed' };
|
|
@@ -8587,6 +8872,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8587
8872
|
cursorModelDiscovery,
|
|
8588
8873
|
grokModelDiscovery,
|
|
8589
8874
|
deepseekModelDiscovery,
|
|
8875
|
+
claudeApiModelDiscovery,
|
|
8590
8876
|
copilotModelDiscovery,
|
|
8591
8877
|
});
|
|
8592
8878
|
} catch (error) {
|
|
@@ -8634,6 +8920,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8634
8920
|
// stay in models[] (a selection must not vanish) but pickers grey them.
|
|
8635
8921
|
unavailableModels: Array.isArray(modelState.unavailableModels) ? modelState.unavailableModels : [],
|
|
8636
8922
|
claudeContextTiersByModel: modelState.claudeContextTiersByModel || {},
|
|
8923
|
+
claudeApiContextTiersByModel: modelState.claudeApiContextTiersByModel || {},
|
|
8637
8924
|
providersByModel: modelState.providersByModel || {},
|
|
8638
8925
|
stale: modelState.stale,
|
|
8639
8926
|
metadataValid: modelState.metadataValid === true,
|
|
@@ -8684,7 +8971,8 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8684
8971
|
cursorBilling: null,
|
|
8685
8972
|
deepseekBalance: null,
|
|
8686
8973
|
};
|
|
8687
|
-
|
|
8974
|
+
// `claude-api`'s card is read from the relay's own books on every answer.
|
|
8975
|
+
const USAGE_PROVIDER_IDS = ['github', 'claude', 'claude-cloud', 'cursor', 'grok', 'deepseek', 'claude-api'];
|
|
8688
8976
|
|
|
8689
8977
|
function parseUsageProviders(raw) {
|
|
8690
8978
|
const text = Array.isArray(raw) ? raw.join(',') : String(raw ?? '');
|
|
@@ -8803,6 +9091,14 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8803
9091
|
}
|
|
8804
9092
|
}
|
|
8805
9093
|
|
|
9094
|
+
// Built from the database on every answer, so it is always live.
|
|
9095
|
+
let claudeApiCard = null;
|
|
9096
|
+
try {
|
|
9097
|
+
claudeApiCard = claudeApiPlanUsage?.buildCard?.() || null;
|
|
9098
|
+
} catch {
|
|
9099
|
+
claudeApiCard = null;
|
|
9100
|
+
}
|
|
9101
|
+
|
|
8806
9102
|
const report = planUsageService.buildReport({
|
|
8807
9103
|
copilotSummary: copilot.summary,
|
|
8808
9104
|
copilotError: copilot.error,
|
|
@@ -8829,6 +9125,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8829
9125
|
? { amount: deepseekSettings.startingBalance, setAt: deepseekSettings.startingBalanceSetAt }
|
|
8830
9126
|
: null,
|
|
8831
9127
|
deepseekActuals: deepseekEnabled ? readDeepSeekActuals() : null,
|
|
9128
|
+
claudeApiCard,
|
|
8832
9129
|
});
|
|
8833
9130
|
|
|
8834
9131
|
// Legacy top-level fields stay alongside `providers` so an older cached
|
|
@@ -8844,6 +9141,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8844
9141
|
cursor: cursorEntry.at,
|
|
8845
9142
|
grok: grokEntry.at,
|
|
8846
9143
|
deepseek: deepseekEntry.value?.fetchedAt || deepseekEntry.at,
|
|
9144
|
+
'claude-api': nowIso(),
|
|
8847
9145
|
},
|
|
8848
9146
|
});
|
|
8849
9147
|
});
|