@oar-sh/oar 0.9.16 → 0.9.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/.github/extensions/web-relay/polling/polling-loop.mjs +14 -87
  2. package/.github/extensions/web-relay/skills/tool-activity.mjs +5 -4
  3. package/CHANGELOG.md +412 -0
  4. package/README.md +15 -13
  5. package/package.json +2 -1
  6. package/server/claude-api-worker/claude-api-backend.mjs +345 -0
  7. package/server/claude-api-worker/claude-api-session-worker.mjs +29 -0
  8. package/server/claude-worker/claude-attachments.mjs +7 -4
  9. package/server/claude-worker/claude-cli-pinned-env.mjs +150 -0
  10. package/server/claude-worker/claude-remote-relay-tool.mjs +3 -3
  11. package/server/claude-worker/claude-sdk-adapter.mjs +199 -15
  12. package/server/claude-worker/claude-session-process.mjs +276 -33
  13. package/server/claude-worker/claude-session-worker-main.mjs +269 -0
  14. package/server/claude-worker/claude-session-worker.mjs +2 -253
  15. package/server/claude-worker/claude-subscription-key-check.mjs +64 -0
  16. package/server/claude-worker/claude-turn-failure.mjs +8 -0
  17. package/server/claude-worker/claude-turn-publisher.mjs +394 -13
  18. package/server/claude-worker/claude-usage-totals.mjs +161 -0
  19. package/server/claude-worker/sdk-message-normalizer.mjs +56 -1
  20. package/server/copilot-worker/copilot-remote-relay-tool.mjs +2 -2
  21. package/server/copilot-worker/copilot-sdk-adapter.mjs +18 -4
  22. package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +1 -1
  23. package/server/copilot-worker/copilot-sdk-session-process.mjs +80 -14
  24. package/server/cursor-worker/cursor-remote-relay-tool.mjs +3 -3
  25. package/server/cursor-worker/cursor-turn-runner.mjs +4 -4
  26. package/server/cursor-worker/sdk-message-normalizer.mjs +2 -2
  27. package/server/db-schema.mjs +96 -1
  28. package/server/deepseek-worker/deepseek-claude-backend.mjs +317 -0
  29. package/server/deepseek-worker/deepseek-session-worker.mjs +37 -0
  30. package/server/grok-worker/acp-client.mjs +1 -1
  31. package/server/grok-worker/grok-turn-runner.mjs +4 -4
  32. package/server/grok-worker/sdk-message-normalizer.mjs +1 -1
  33. package/server/mcp/oar-mcp-server.mjs +11 -5
  34. package/server/migrations/0024-deepseek-usage-totals.mjs +151 -0
  35. package/server/migrations/0025-deepseek-usage-requests-daily.mjs +179 -0
  36. package/server/migrations/0026-deepseek-usage-served-models.mjs +222 -0
  37. package/server/migrations/0027-deepseek-usage-checkpoint-runs.mjs +101 -0
  38. package/server/migrations/0028-deepseek-usage-checkpoint-request-runs.mjs +99 -0
  39. package/server/migrations/0029-deepseek-balance-rises.mjs +99 -0
  40. package/server/migrations/0030-deepseek-usage-actuals.mjs +123 -0
  41. package/server/migrations/0031-queue-provider.mjs +143 -0
  42. package/server/migrations/0032-runtime-session-billing-switch.mjs +94 -0
  43. package/server/migrations/0033-claude-api-usage-checkpoints.mjs +138 -0
  44. package/server/migrations/0034-claude-api-usage-totals.mjs +112 -0
  45. package/server/public/app/activity-replay-state.mjs +4 -3
  46. package/server/public/app/agent-sessions-settings-ui.js +1 -1
  47. package/server/public/app/agent-sessions-settings.mjs +1 -1
  48. package/server/public/app/api-client.js +105 -0
  49. package/server/public/app/bootstrap.js +243 -29
  50. package/server/public/app/claude-api-settings-ui.js +362 -0
  51. package/server/public/app/claude-billing-toggle-ui.js +224 -0
  52. package/server/public/app/claude-billing-toggle.mjs +112 -0
  53. package/server/public/app/context-tier-options.mjs +10 -2
  54. package/server/public/app/context-usage-view.mjs +1 -1
  55. package/server/public/app/conversation-provider-indicator.mjs +18 -0
  56. package/server/public/app/conversation-view.js +31 -3
  57. package/server/public/app/deepseek-settings-ui.js +342 -0
  58. package/server/public/app/inbox-ui.js +14 -3
  59. package/server/public/app/inbox-view.mjs +1 -1
  60. package/server/public/app/journal-view.js +156 -11
  61. package/server/public/app/model-selector-options.mjs +28 -1
  62. package/server/public/app/model-vision-support.mjs +2 -0
  63. package/server/public/app/new-conversation-model-choice.mjs +21 -0
  64. package/server/public/app/no-conversation-state.mjs +1 -1
  65. package/server/public/app/plan-usage-view.mjs +165 -13
  66. package/server/public/app/provider-readiness.mjs +8 -2
  67. package/server/public/app/relay-error-ctas.mjs +58 -0
  68. package/server/public/app/relay-schedules-ui.js +1 -1
  69. package/server/public/app/settings-modal.js +22 -0
  70. package/server/public/app/settings-tabs.js +1 -1
  71. package/server/public/app/socket-handlers.js +28 -1
  72. package/server/public/app/tool-activity-emoji.mjs +4 -3
  73. package/server/public/app/usage-limit-ui.js +136 -5
  74. package/server/public/index.html +596 -35
  75. package/server/relay-tools.md +4 -4
  76. package/server/repositories/conversation-group-repository.mjs +14 -7
  77. package/server/repositories/message-repository.mjs +7 -0
  78. package/server/repositories/session-repository.mjs +8 -1
  79. package/server/routes/ask-user-routes.mjs +1 -1
  80. package/server/routes/conversation-group-routes.mjs +3 -2
  81. package/server/routes/deepseek-usage-import-routes.mjs +72 -0
  82. package/server/routes/messages-routes.mjs +831 -36
  83. package/server/routes/relay-ask-routes.mjs +3 -3
  84. package/server/routes/relay-event-routes.mjs +2 -2
  85. package/server/routes/relay-schedule-routes.mjs +1 -1
  86. package/server/routes/remote-relay-routes.mjs +3 -3
  87. package/server/routes/sessions-routes.mjs +670 -63
  88. package/server/server-runtime.mjs +476 -26
  89. package/server/services/about-service.mjs +2 -0
  90. package/server/services/background-work-view.mjs +2 -1
  91. package/server/services/claude-api-settings-service.mjs +575 -0
  92. package/server/services/claude-api-usage-service.mjs +361 -0
  93. package/server/services/claude-billing-swap-service.mjs +274 -0
  94. package/server/services/claude-billing-switch-service.mjs +343 -0
  95. package/server/services/claude-session-leftovers.mjs +226 -0
  96. package/server/services/deepseek-balance-service.mjs +102 -0
  97. package/server/services/deepseek-settings-service.mjs +380 -0
  98. package/server/services/deepseek-usage-import.mjs +471 -0
  99. package/server/services/model-variant-catalog-service.mjs +1 -0
  100. package/server/services/plan-usage-claude-api.mjs +445 -0
  101. package/server/services/plan-usage-contract.mjs +100 -5
  102. package/server/services/plan-usage-deepseek.mjs +931 -0
  103. package/server/services/plan-usage-service.mjs +436 -0
  104. package/server/services/provider-model-selection.mjs +4 -2
  105. package/server/services/provider-reasoning-effort.mjs +24 -2
  106. package/server/services/relay-ask-service.mjs +1 -1
  107. package/server/services/relay-event-service.mjs +3 -2
  108. package/server/services/relay-schedule-service.mjs +1 -1
  109. package/server/services/remote-relay-caller-context.mjs +7 -0
  110. package/server/services/remote-relay-dispatcher.mjs +84 -14
  111. package/server/services/remote-relay-registry-service.mjs +1 -1
  112. package/server/services/session-worker-launch-service.mjs +294 -84
  113. package/server/services/session-worker-registry-service.mjs +28 -1
  114. package/server/services/session-worker-websocket-service.mjs +17 -0
  115. package/server/services/usage-limit-pause-service.mjs +55 -4
  116. package/server/services/worker-secret-redaction.mjs +320 -0
  117. package/shared/anthropic-api.mjs +202 -0
  118. package/shared/context-window-fallbacks.mjs +3 -0
  119. package/shared/deepseek-api.mjs +137 -0
  120. package/shared/deepseek-models.mjs +114 -0
  121. package/shared/deepseek-pricing.mjs +146 -0
  122. package/shared/host-claude-session-env.mjs +101 -0
  123. package/shared/model-id.mjs +7 -0
  124. package/shared/preview-tool-core.mjs +1 -1
  125. package/shared/provider-routing.mjs +5 -3
  126. package/shared/relay-asks.mjs +11 -5
  127. package/shared/relay-events.mjs +2 -2
  128. package/shared/relay-schedules.mjs +2 -2
  129. package/shared/remote-relay-contract.mjs +29 -9
  130. package/shared/remote-relay-tool-core.mjs +23 -6
  131. package/shared/steer-settle-failure.mjs +2 -0
  132. package/shared/worker-runtime/api-client.mjs +2 -2
  133. package/shared/worker-runtime/direct-openai-image.mjs +128 -0
  134. package/shared/worker-runtime/heartbeat.mjs +5 -0
  135. package/shared/worker-runtime/worker-log-file.mjs +60 -0
  136. package/shared/worker-runtime/worker-websocket-link.mjs +6 -0
@@ -79,6 +79,8 @@ import {
79
79
  updateClaudeSettings,
80
80
  updateCursorSettings,
81
81
  updateGrokSettings,
82
+ updateDeepSeekSettings,
83
+ updateClaudeApiSettings,
82
84
  loadConversation,
83
85
  refreshConversationHistory,
84
86
  updateConversationTitle,
@@ -180,6 +182,7 @@ import {
180
182
  composerPlaceholderFor,
181
183
  contextWindowSuffix,
182
184
  humanizeModelLabel,
185
+ distinctModelLabels,
183
186
  modelMetadataFor,
184
187
  modelSelectorOptionsEqual,
185
188
  splitModelVariantId,
@@ -323,6 +326,19 @@ import {
323
326
  toggleGrokProvider,
324
327
  applyGrokSettingsState,
325
328
  refreshGrokSettingsState,
329
+ saveDeepSeekSettings,
330
+ removeDeepSeekSettings,
331
+ toggleDeepSeekProvider,
332
+ importDeepSeekUsageExport,
333
+ applyDeepSeekSettingsState,
334
+ refreshDeepSeekSettingsState,
335
+ saveClaudeApiSettings,
336
+ removeClaudeApiSettings,
337
+ toggleClaudeApiProvider,
338
+ refreshClaudeApiModels,
339
+ openClaudeTabFromClaudeApi,
340
+ applyClaudeApiSettingsState,
341
+ refreshClaudeApiSettingsState,
326
342
  saveCursorSettings,
327
343
  removeCursorSettings,
328
344
  saveCursorAllowanceSettings,
@@ -366,6 +382,7 @@ import {
366
382
  } from './action-confirmations.js';
367
383
  import { applyPendingActionsFromStatus, initHostSuspendUi } from './host-suspend-ui.js';
368
384
  import { applyUsageLimitFromStatus, initUsageLimitUi } from './usage-limit-ui.js';
385
+ import { initClaudeBillingToggle, renderClaudeBillingToggle } from './claude-billing-toggle-ui.js';
369
386
  import {
370
387
  startClaudeRelogin,
371
388
  submitClaudeLoginCodeFromInput,
@@ -442,9 +459,11 @@ const PROVIDER_LABELS = {
442
459
  openai: 'OpenAI',
443
460
  'openai-byok': 'OpenAI (BYOK)',
444
461
  claude: 'Claude SDK',
462
+ 'claude-api': 'Claude API',
445
463
  'claude-cloud': 'Claude Cloud',
446
464
  cursor: 'Cursor SDK',
447
465
  grok: 'Grok',
466
+ deepseek: 'DeepSeek',
448
467
  'github-copilot': 'GitHub Copilot',
449
468
  // Vendor grouping for Copilot-served rows, distinct from the Claude SDK runtime.
450
469
  anthropic: 'Anthropic',
@@ -514,6 +533,7 @@ let modelCatalogState = {
514
533
  reasoningEfforts: [],
515
534
  modelMetadataByModel: {},
516
535
  claudeContextTiersByModel: {},
536
+ claudeApiContextTiersByModel: {},
517
537
  stale: true,
518
538
  metadataValid: false,
519
539
  reasoningMetadataValid: false,
@@ -1436,6 +1456,8 @@ function syncSessionLockNote({ pinnedModel = '' } = {}) {
1436
1456
  } else {
1437
1457
  delete note.dataset.provider;
1438
1458
  }
1459
+ // The billing toggle sits under the note and follows the same chat.
1460
+ renderClaudeBillingToggle();
1439
1461
  }
1440
1462
 
1441
1463
  function applyModelMetadataHardFail(message = '') {
@@ -1502,9 +1524,11 @@ function normalizeModelSelectorProviderType(providerType = '') {
1502
1524
  const normalized = String(providerType || '').trim().toLowerCase();
1503
1525
  if (normalized === 'openai' || normalized === 'openai-byok') return 'openai';
1504
1526
  if (normalized === 'claude') return 'claude';
1527
+ if (normalized === 'claude-api') return 'claude-api';
1505
1528
  if (normalized === CLAUDE_CLOUD_PROVIDER) return CLAUDE_CLOUD_PROVIDER;
1506
1529
  if (normalized === 'cursor') return 'cursor';
1507
1530
  if (normalized === 'grok') return 'grok';
1531
+ if (normalized === 'deepseek') return 'deepseek';
1508
1532
  return 'github';
1509
1533
  }
1510
1534
 
@@ -1532,6 +1556,10 @@ function modelVisibleForActiveProvider(modelId, activeProviderType, providersByM
1532
1556
  const hasClaude = providers.includes('claude');
1533
1557
  const hasCursor = providers.includes('cursor');
1534
1558
  const hasGrok = providers.includes('grok');
1559
+ const hasDeepSeek = providers.includes('deepseek');
1560
+ // Claude API rows are the Claude provider's ids on an API key: as
1561
+ // exclusive as the other runtimes' rows.
1562
+ const hasClaudeApi = providers.includes('claude-api');
1535
1563
  // Only present if a relay tags catalog rows for Claude Cloud; such a row is
1536
1564
  // then as exclusive as the other runtimes' rows.
1537
1565
  const hasClaudeCloud = providers.includes(CLAUDE_CLOUD_PROVIDER);
@@ -1540,14 +1568,18 @@ function modelVisibleForActiveProvider(modelId, activeProviderType, providersByM
1540
1568
  && provider !== 'claude'
1541
1569
  && provider !== 'cursor'
1542
1570
  && provider !== 'grok'
1571
+ && provider !== 'deepseek'
1572
+ && provider !== 'claude-api'
1543
1573
  && provider !== CLAUDE_CLOUD_PROVIDER
1544
1574
  ));
1545
1575
  const activeProvider = normalizeModelSelectorProviderType(activeProviderType);
1546
1576
  if (activeProvider === 'openai') return hasOpenAIByok;
1547
1577
  if (activeProvider === 'claude') return hasClaude;
1578
+ if (activeProvider === 'claude-api') return hasClaudeApi;
1548
1579
  if (activeProvider === 'cursor') return hasCursor;
1549
1580
  if (activeProvider === 'grok') return hasGrok;
1550
- return !((hasOpenAIByok || hasClaude || hasCursor || hasGrok || hasClaudeCloud) && !hasNonExclusiveProvider);
1581
+ if (activeProvider === 'deepseek') return hasDeepSeek;
1582
+ return !((hasOpenAIByok || hasClaude || hasCursor || hasGrok || hasDeepSeek || hasClaudeApi || hasClaudeCloud) && !hasNonExclusiveProvider);
1551
1583
  }
1552
1584
 
1553
1585
  // A Claude Cloud conversation does not pick from the shared catalog: its
@@ -1583,10 +1615,16 @@ function buildModelSelectorOptions(
1583
1615
  if (normalizeModelSelectorProviderType(activeProviderType) === CLAUDE_CLOUD_PROVIDER) {
1584
1616
  return buildClaudeCloudModelSelectorOptions(models, providersByModel, metadataByModel);
1585
1617
  }
1586
- const normalizedOptions = buildCatalogModelOptions(models.length ? models : [FALLBACK_MODEL], {
1618
+ // Only this provider's models are labelled: two ids that read the same
1619
+ // fall back to their raw ids, and an id another provider serves
1620
+ // ("claude-haiku-4-5" beside the key's "claude-haiku-4-5-20251001") must
1621
+ // not do that to a list it is not in.
1622
+ const visibleModels = (models.length ? models : [FALLBACK_MODEL])
1623
+ .filter((modelId) => modelVisibleForActiveProvider(modelId, activeProviderType, providersByModel));
1624
+ const normalizedOptions = buildCatalogModelOptions(visibleModels, {
1587
1625
  autoValue: AUTO_MODEL_OPTION,
1588
1626
  metadataByModel,
1589
- annotateContextWindow: normalizeModelSelectorProviderType(activeProviderType) !== 'claude',
1627
+ annotateContextWindow: !['claude', 'claude-api'].includes(normalizeModelSelectorProviderType(activeProviderType)),
1590
1628
  });
1591
1629
  return normalizedOptions.filter((option) => modelVisibleForActiveProvider(
1592
1630
  option.value,
@@ -1792,6 +1830,12 @@ function updateModelCatalogState(payload) {
1792
1830
  Array.isArray(tiers) ? tiers : [],
1793
1831
  ]))
1794
1832
  : {},
1833
+ claudeApiContextTiersByModel: payload?.claudeApiContextTiersByModel && typeof payload.claudeApiContextTiersByModel === 'object'
1834
+ ? Object.fromEntries(Object.entries(payload.claudeApiContextTiersByModel).map(([modelId, tiers]) => [
1835
+ String(modelId || '').trim().toLowerCase(),
1836
+ Array.isArray(tiers) ? tiers : [],
1837
+ ]))
1838
+ : {},
1795
1839
  stale: !!payload?.stale,
1796
1840
  metadataValid: payload?.metadataValid === true,
1797
1841
  reasoningMetadataValid: payload?.reasoningMetadataValid === true,
@@ -1923,7 +1967,9 @@ function updateContextTierSelector(modelId) {
1923
1967
  modelId,
1924
1968
  providerType: activeComposerProviderType(),
1925
1969
  metadata: modelMetadataFor(modelId, modelCatalogState.modelMetadataByModel) || {},
1926
- claudeTiers: modelCatalogState.claudeContextTiersByModel,
1970
+ claudeTiers: activeComposerProviderType() === 'claude-api'
1971
+ ? modelCatalogState.claudeApiContextTiersByModel
1972
+ : modelCatalogState.claudeContextTiersByModel,
1927
1973
  });
1928
1974
  const current = select.value;
1929
1975
  select.innerHTML = '';
@@ -1945,8 +1991,9 @@ function updateModelPricingDetails(modelId, tier) {
1945
1991
  if (!details || !summary || !grid) return;
1946
1992
  const metadata = modelCatalogState.modelMetadataByModel?.[modelId] || {};
1947
1993
  // The catalog's prices are Copilot credits; a cloud session is billed to the
1948
- // Claude account, so they would be the wrong numbers for it.
1949
- const pricing = activeComposerProviderType() === CLAUDE_CLOUD_PROVIDER
1994
+ // Claude account and a Claude API chat to the Console in dollars, so they
1995
+ // would be the wrong numbers for them.
1996
+ const pricing = activeComposerProviderType() === CLAUDE_CLOUD_PROVIDER || activeComposerProviderType() === 'claude-api'
1950
1997
  ? null
1951
1998
  : metadata?.pricing?.[tier === 'long_context' ? 'longContext' : 'default'];
1952
1999
  if (!pricing || typeof pricing !== 'object') {
@@ -1988,8 +2035,10 @@ const RELAY_MODES_BY_PROVIDER = {
1988
2035
  github: ['agent', 'ask', 'plan', 'autopilot'],
1989
2036
  openai: ['agent', 'ask', 'plan', 'autopilot'],
1990
2037
  claude: ['agent', 'ask', 'plan', 'autopilot'],
2038
+ 'claude-api': ['agent', 'ask', 'plan', 'autopilot'],
1991
2039
  cursor: ['agent', 'ask', 'plan', 'autopilot'],
1992
2040
  grok: ['agent', 'ask', 'plan', 'autopilot'],
2041
+ deepseek: ['agent', 'ask', 'plan', 'autopilot'],
1993
2042
  'claude-cloud': ['agent'],
1994
2043
  };
1995
2044
 
@@ -2053,7 +2102,7 @@ async function persistCurrentConversationPreferences() {
2053
2102
  const contextTier = String(document.getElementById('context-tier-select')?.value || 'default').trim().toLowerCase();
2054
2103
  const preferredModelWithTier = model
2055
2104
  && contextTier === 'long_context'
2056
- && activeComposerProviderType() === 'claude'
2105
+ && (activeComposerProviderType() === 'claude' || activeComposerProviderType() === 'claude-api')
2057
2106
  && !CLAUDE_LONG_CONTEXT_PATTERN.test(model)
2058
2107
  ? `${model}[1m]`
2059
2108
  : model;
@@ -2336,6 +2385,20 @@ function reportOpenAIModelDiscoveryFailure(payload) {
2336
2385
  8000,
2337
2386
  );
2338
2387
  }
2388
+ const deepseekDiscovery = payload?.deepseekModelDiscovery;
2389
+ if (deepseekDiscovery && deepseekDiscovery.ok === false && !deepseekDiscovery.skipped) {
2390
+ showTransientRelayNotice(
2391
+ `DeepSeek model discovery failed. Cached DeepSeek models were kept: ${deepseekDiscovery.error || 'unknown error'}`,
2392
+ 8000,
2393
+ );
2394
+ }
2395
+ const claudeApiDiscovery = payload?.claudeApiModelDiscovery;
2396
+ if (claudeApiDiscovery && claudeApiDiscovery.ok === false && !claudeApiDiscovery.skipped) {
2397
+ showTransientRelayNotice(
2398
+ `Claude API model discovery failed. Cached Claude API models were kept: ${claudeApiDiscovery.error || 'unknown error'}`,
2399
+ 8000,
2400
+ );
2401
+ }
2339
2402
  }
2340
2403
 
2341
2404
  async function retryModelMetadataRefresh() {
@@ -2473,6 +2536,28 @@ function applyModelVariantCatalogState(payload) {
2473
2536
  : [],
2474
2537
  }
2475
2538
  : null,
2539
+ deepseekModels: payload?.deepseekModels && typeof payload.deepseekModels === 'object'
2540
+ ? {
2541
+ defaultModel: String(payload.deepseekModels.defaultModel || '').trim(),
2542
+ availableModels: Array.isArray(payload.deepseekModels.availableModels)
2543
+ ? payload.deepseekModels.availableModels.map((value) => String(value || '').trim()).filter(Boolean)
2544
+ : [],
2545
+ enabledModels: Array.isArray(payload.deepseekModels.enabledModels)
2546
+ ? payload.deepseekModels.enabledModels.map((value) => String(value || '').trim()).filter(Boolean)
2547
+ : [],
2548
+ }
2549
+ : null,
2550
+ claudeApiModels: payload?.claudeApiModels && typeof payload.claudeApiModels === 'object'
2551
+ ? {
2552
+ defaultModel: String(payload.claudeApiModels.defaultModel || '').trim(),
2553
+ availableModels: Array.isArray(payload.claudeApiModels.availableModels)
2554
+ ? payload.claudeApiModels.availableModels.map((value) => String(value || '').trim()).filter(Boolean)
2555
+ : [],
2556
+ enabledModels: Array.isArray(payload.claudeApiModels.enabledModels)
2557
+ ? payload.claudeApiModels.enabledModels.map((value) => String(value || '').trim()).filter(Boolean)
2558
+ : [],
2559
+ }
2560
+ : null,
2476
2561
  };
2477
2562
  }
2478
2563
 
@@ -2541,6 +2626,11 @@ function renderModelVariantCatalogBody() {
2541
2626
  return entryProvider === 'claude';
2542
2627
  }
2543
2628
  if (entryProvider === 'claude') return false;
2629
+ // Claude API lists the same Claude ids as the Claude SDK tab, its own rows.
2630
+ if (activeTab === 'claude-api') {
2631
+ return entryProvider === 'claude-api';
2632
+ }
2633
+ if (entryProvider === 'claude-api') return false;
2544
2634
  if (activeTab === 'cursor') {
2545
2635
  return entryProvider === 'cursor';
2546
2636
  }
@@ -2549,6 +2639,10 @@ function renderModelVariantCatalogBody() {
2549
2639
  return entryProvider === 'grok';
2550
2640
  }
2551
2641
  if (entryProvider === 'grok') return false;
2642
+ if (activeTab === 'deepseek') {
2643
+ return entryProvider === 'deepseek';
2644
+ }
2645
+ if (entryProvider === 'deepseek') return false;
2552
2646
  if (activeTab === 'openai') {
2553
2647
  return hasOpenAIByok || entryProvider === 'openai-byok';
2554
2648
  }
@@ -2560,6 +2654,8 @@ function renderModelVariantCatalogBody() {
2560
2654
  && provider !== 'claude'
2561
2655
  && provider !== 'cursor'
2562
2656
  && provider !== 'grok'
2657
+ && provider !== 'deepseek'
2658
+ && provider !== 'claude-api'
2563
2659
  && provider !== CLAUDE_CLOUD_PROVIDER
2564
2660
  ));
2565
2661
  }
@@ -2670,6 +2766,54 @@ function renderModelVariantCatalogBody() {
2670
2766
  }
2671
2767
  grouped.set('grok', providerRows);
2672
2768
  }
2769
+ const deepseekCatalog = modelVariantCatalogState.deepseekModels;
2770
+ if (deepseekCatalog?.availableModels?.length) {
2771
+ const deepseekEnabledSet = new Set(deepseekCatalog.enabledModels || []);
2772
+ const deepseekDefaultModel = String(deepseekCatalog.defaultModel || '').trim();
2773
+ const providerRows = [];
2774
+ for (const [index, deepseekModelId] of deepseekCatalog.availableModels.entries()) {
2775
+ const isDefaultModel = deepseekModelId === deepseekDefaultModel;
2776
+ providerRows.push({
2777
+ variantId: `${deepseekModelId}--provider-deepseek`,
2778
+ baseModelId: deepseekModelId,
2779
+ provider: 'deepseek',
2780
+ label: humanizeModelLabel(deepseekModelId),
2781
+ releaseStatus: null,
2782
+ reasoningEffort: null,
2783
+ // The default model always stays enabled, as for the other providers.
2784
+ selectable: !isDefaultModel,
2785
+ enabled: deepseekEnabledSet.has(deepseekModelId) || isDefaultModel,
2786
+ deepseekModelId,
2787
+ sortOrder: index,
2788
+ });
2789
+ }
2790
+ grouped.set('deepseek', providerRows);
2791
+ }
2792
+ const claudeApiCatalog = modelVariantCatalogState.claudeApiModels;
2793
+ if (claudeApiCatalog?.availableModels?.length) {
2794
+ const claudeApiEnabledSet = new Set(claudeApiCatalog.enabledModels || []);
2795
+ const claudeApiDefaultModel = String(claudeApiCatalog.defaultModel || '').trim();
2796
+ // Named by model; two snapshots that would read the same keep their ids.
2797
+ const claudeApiLabels = distinctModelLabels(claudeApiCatalog.availableModels);
2798
+ const providerRows = [];
2799
+ for (const [index, claudeApiModelId] of claudeApiCatalog.availableModels.entries()) {
2800
+ const isDefaultModel = claudeApiModelId === claudeApiDefaultModel;
2801
+ providerRows.push({
2802
+ variantId: `${claudeApiModelId}--provider-claude-api`,
2803
+ baseModelId: claudeApiModelId,
2804
+ provider: 'claude-api',
2805
+ label: claudeApiLabels.get(claudeApiModelId) || claudeApiModelId,
2806
+ releaseStatus: null,
2807
+ reasoningEffort: null,
2808
+ // The default model always stays enabled, as for the other providers.
2809
+ selectable: !isDefaultModel,
2810
+ enabled: claudeApiEnabledSet.has(claudeApiModelId) || isDefaultModel,
2811
+ claudeApiModelId,
2812
+ sortOrder: index,
2813
+ });
2814
+ }
2815
+ grouped.set('claude-api', providerRows);
2816
+ }
2673
2817
  const selected = new Set(modelVariantCatalogState.enabledVariantIds);
2674
2818
  const selectedOrder = new Map(
2675
2819
  modelVariantCatalogState.enabledVariantIds.map((variantId, index) => [variantId, index]),
@@ -2705,24 +2849,32 @@ function renderModelVariantCatalogBody() {
2705
2849
  const hasAnthropicTab = providerOrder.some((providerKey) => providerBelongsToTab(providerKey, 'anthropic'));
2706
2850
  const hasCursorTab = providerOrder.some((providerKey) => providerBelongsToTab(providerKey, 'cursor'));
2707
2851
  const hasGrokTab = providerOrder.some((providerKey) => providerBelongsToTab(providerKey, 'grok'));
2852
+ const hasDeepSeekTab = providerOrder.some((providerKey) => providerBelongsToTab(providerKey, 'deepseek'));
2853
+ const hasClaudeApiTab = providerOrder.some((providerKey) => providerBelongsToTab(providerKey, 'claude-api'));
2708
2854
  if (modelVariantCatalogProviderTab === 'openai' && !hasOpenAITab) modelVariantCatalogProviderTab = 'copilot';
2709
2855
  if (modelVariantCatalogProviderTab === 'anthropic' && !hasAnthropicTab) modelVariantCatalogProviderTab = 'copilot';
2710
2856
  if (modelVariantCatalogProviderTab === 'cursor' && !hasCursorTab) modelVariantCatalogProviderTab = 'copilot';
2711
2857
  if (modelVariantCatalogProviderTab === 'grok' && !hasGrokTab) modelVariantCatalogProviderTab = 'copilot';
2858
+ if (modelVariantCatalogProviderTab === 'deepseek' && !hasDeepSeekTab) modelVariantCatalogProviderTab = 'copilot';
2859
+ if (modelVariantCatalogProviderTab === 'claude-api' && !hasClaudeApiTab) modelVariantCatalogProviderTab = 'copilot';
2712
2860
  if (modelVariantCatalogProviderTab === 'copilot' && !hasCopilotTab) {
2713
2861
  if (hasOpenAITab) modelVariantCatalogProviderTab = 'openai';
2714
2862
  else if (hasAnthropicTab) modelVariantCatalogProviderTab = 'anthropic';
2715
2863
  else if (hasCursorTab) modelVariantCatalogProviderTab = 'cursor';
2716
2864
  else if (hasGrokTab) modelVariantCatalogProviderTab = 'grok';
2865
+ else if (hasDeepSeekTab) modelVariantCatalogProviderTab = 'deepseek';
2866
+ else if (hasClaudeApiTab) modelVariantCatalogProviderTab = 'claude-api';
2717
2867
  }
2718
2868
  const visibleProviderOrder = providerOrder.filter((providerKey) => providerBelongsToTab(providerKey, modelVariantCatalogProviderTab));
2719
2869
  const providerTabButtons = `
2720
- <div class="summary-tab-strip" style="display:flex;gap:8px;align-items:center;">
2870
+ <div class="summary-tab-strip" style="display:flex;flex-wrap:wrap;gap:8px;align-items:center;">
2721
2871
  <button type="button" class="summary-btn model-provider-tab${modelVariantCatalogProviderTab === 'copilot' ? ' active' : ''}" data-model-provider-tab="copilot">Copilot</button>
2722
2872
  <button type="button" class="summary-btn model-provider-tab${modelVariantCatalogProviderTab === 'openai' ? ' active' : ''}" data-model-provider-tab="openai" ${hasOpenAITab ? '' : 'disabled'}>OpenAI</button>
2723
2873
  <button type="button" class="summary-btn model-provider-tab${modelVariantCatalogProviderTab === 'anthropic' ? ' active' : ''}" data-model-provider-tab="anthropic" ${hasAnthropicTab ? '' : 'disabled'}>Claude SDK</button>
2874
+ <button type="button" class="summary-btn model-provider-tab${modelVariantCatalogProviderTab === 'claude-api' ? ' active' : ''}" data-model-provider-tab="claude-api" ${hasClaudeApiTab ? '' : 'disabled'}>Claude API</button>
2724
2875
  <button type="button" class="summary-btn model-provider-tab${modelVariantCatalogProviderTab === 'cursor' ? ' active' : ''}" data-model-provider-tab="cursor" ${hasCursorTab ? '' : 'disabled'}>Cursor SDK</button>
2725
2876
  <button type="button" class="summary-btn model-provider-tab${modelVariantCatalogProviderTab === 'grok' ? ' active' : ''}" data-model-provider-tab="grok" ${hasGrokTab ? '' : 'disabled'}>Grok</button>
2877
+ <button type="button" class="summary-btn model-provider-tab${modelVariantCatalogProviderTab === 'deepseek' ? ' active' : ''}" data-model-provider-tab="deepseek" ${hasDeepSeekTab ? '' : 'disabled'}>DeepSeek</button>
2726
2878
  </div>
2727
2879
  `;
2728
2880
  const warnings = [
@@ -2775,9 +2927,10 @@ function renderModelVariantCatalogBody() {
2775
2927
  : '';
2776
2928
  const variantRowsHtml = variantRows.map((row) => {
2777
2929
  const selectable = row.selectable !== false;
2778
- // SDK-provider rows (Claude/Cursor/Grok) carry their own enabled flag; only
2779
- // Copilot variant rows read the enabledVariantIds selection.
2780
- const sdkProviderModelId = row.claudeModelId || row.cursorModelId || row.grokModelId || '';
2930
+ // SDK-provider rows (Claude/Cursor/Grok/DeepSeek) carry their own enabled
2931
+ // flag; only Copilot variant rows read the enabledVariantIds selection.
2932
+ const sdkProviderModelId = row.claudeModelId || row.cursorModelId || row.grokModelId || row.deepseekModelId
2933
+ || row.claudeApiModelId || '';
2781
2934
  const checked = sdkProviderModelId
2782
2935
  ? row.enabled === true
2783
2936
  : (selectable && selected.has(row.variantId));
@@ -2797,12 +2950,16 @@ function renderModelVariantCatalogBody() {
2797
2950
  ? ` data-claude-model="${escHtml(row.claudeModelId)}"`
2798
2951
  : (row.cursorModelId
2799
2952
  ? ` data-cursor-model="${escHtml(row.cursorModelId)}"`
2800
- : (row.grokModelId ? ` data-grok-model="${escHtml(row.grokModelId)}"` : ''));
2953
+ : (row.grokModelId
2954
+ ? ` data-grok-model="${escHtml(row.grokModelId)}"`
2955
+ : (row.deepseekModelId
2956
+ ? ` data-deepseek-model="${escHtml(row.deepseekModelId)}"`
2957
+ : (row.claudeApiModelId ? ` data-claude-api-model="${escHtml(row.claudeApiModelId)}"` : ''))));
2801
2958
  return `
2802
2959
  <label class="model-variant-row">
2803
2960
  <input class="model-variant-checkbox" type="checkbox" data-selectable="${selectable ? '1' : '0'}" data-variant-id="${escHtml(row.variantId)}"${sdkModelAttr} ${checked ? 'checked' : ''} ${selectable ? '' : 'disabled'}>
2804
2961
  <span class="model-variant-row-copy">
2805
- <span class="model-variant-row-title">${escHtml(row.variantId)}${effortChip}${statusChip}${fixedChip}</span>
2962
+ <span class="model-variant-row-title"${row.claudeApiModelId ? ` title="${escHtml(row.claudeApiModelId)}"` : ''}>${escHtml(row.claudeApiModelId ? row.label : row.variantId)}${effortChip}${statusChip}${fixedChip}</span>
2806
2963
  </span>
2807
2964
  </label>
2808
2965
  `;
@@ -2923,12 +3080,22 @@ async function saveSelectedModelsFromModal() {
2923
3080
  .filter((input) => input.checked)
2924
3081
  .map((input) => String(input.getAttribute('data-grok-model') || '').trim())
2925
3082
  .filter(Boolean);
3083
+ const deepseekCheckboxes = Array.from(body.querySelectorAll('.model-variant-checkbox[data-deepseek-model]'));
3084
+ const selectedDeepSeekModels = deepseekCheckboxes
3085
+ .filter((input) => input.checked)
3086
+ .map((input) => String(input.getAttribute('data-deepseek-model') || '').trim())
3087
+ .filter(Boolean);
3088
+ const claudeApiCheckboxes = Array.from(body.querySelectorAll('.model-variant-checkbox[data-claude-api-model]'));
3089
+ const selectedClaudeApiModels = claudeApiCheckboxes
3090
+ .filter((input) => input.checked)
3091
+ .map((input) => String(input.getAttribute('data-claude-api-model') || '').trim())
3092
+ .filter(Boolean);
2926
3093
  // Only the active provider tab's rows are in the DOM, but the PATCH
2927
3094
  // replaces the WHOLE enabled set — so the save must only change rows the
2928
3095
  // user could actually see. Off-screen variants keep their stored state,
2929
3096
  // otherwise saving from e.g. the OpenAI tab silently disables every
2930
3097
  // Copilot-only vendor group (Anthropic, Google, …).
2931
- const renderedVariantCheckboxes = Array.from(body.querySelectorAll('.model-variant-checkbox[data-selectable="1"]:not([data-claude-model]):not([data-cursor-model]):not([data-grok-model])'));
3098
+ const renderedVariantCheckboxes = Array.from(body.querySelectorAll('.model-variant-checkbox[data-selectable="1"]:not([data-claude-model]):not([data-cursor-model]):not([data-grok-model]):not([data-deepseek-model]):not([data-claude-api-model])'));
2932
3099
  const renderedVariantIds = new Set(renderedVariantCheckboxes
2933
3100
  .map((input) => String(input.getAttribute('data-variant-id') || '').trim())
2934
3101
  .filter(Boolean));
@@ -2969,7 +3136,18 @@ async function saveSelectedModelsFromModal() {
2969
3136
  if (!savedGrok) throw new Error('Failed to save Grok model selection');
2970
3137
  applyGrokSettingsState(savedGrok);
2971
3138
  }
2972
- if (claudeCheckboxes.length || cursorCheckboxes.length || grokCheckboxes.length) {
3139
+ if (deepseekCheckboxes.length) {
3140
+ const savedDeepSeek = await updateDeepSeekSettings({ enabledModels: selectedDeepSeekModels });
3141
+ if (!savedDeepSeek) throw new Error('Failed to save DeepSeek model selection');
3142
+ applyDeepSeekSettingsState(savedDeepSeek);
3143
+ }
3144
+ if (claudeApiCheckboxes.length) {
3145
+ const savedClaudeApi = await updateClaudeApiSettings({ enabledModels: selectedClaudeApiModels });
3146
+ if (!savedClaudeApi) throw new Error('Failed to save Claude API model selection');
3147
+ applyClaudeApiSettingsState(savedClaudeApi);
3148
+ }
3149
+ if (claudeCheckboxes.length || cursorCheckboxes.length || grokCheckboxes.length || deepseekCheckboxes.length
3150
+ || claudeApiCheckboxes.length) {
2973
3151
  const refreshedCatalog = await loadModelVariantCatalog();
2974
3152
  if (refreshedCatalog) applyModelVariantCatalogState(refreshedCatalog);
2975
3153
  }
@@ -3476,11 +3654,15 @@ async function loadContextSummaryAndRender(convId) {
3476
3654
  const refreshLookupId = sessionId || trimmedConvId || null;
3477
3655
  const providerLabel = payload.providerType === 'claude'
3478
3656
  ? 'Claude'
3479
- : (payload.providerType === CLAUDE_CLOUD_PROVIDER
3480
- ? 'Claude Cloud'
3481
- : (payload.providerType === 'cursor'
3482
- ? 'Cursor'
3483
- : (payload.providerType === 'grok' ? 'Grok' : 'Copilot')));
3657
+ : (payload.providerType === 'claude-api'
3658
+ ? 'Claude API'
3659
+ : (payload.providerType === CLAUDE_CLOUD_PROVIDER
3660
+ ? 'Claude Cloud'
3661
+ : (payload.providerType === 'cursor'
3662
+ ? 'Cursor'
3663
+ : (payload.providerType === 'grok'
3664
+ ? 'Grok'
3665
+ : (payload.providerType === 'deepseek' ? 'DeepSeek' : 'Copilot')))));
3484
3666
  const subtitle = sessionId
3485
3667
  ? `${providerLabel} session ${sessionId.slice(0, 8)}`
3486
3668
  : (trimmedConvId ? `Conversation ${trimmedConvId.slice(0, 8)}` : 'No conversation selected');
@@ -3489,14 +3671,18 @@ async function loadContextSummaryAndRender(convId) {
3489
3671
  // (older relays send neither, and the notes fall back to draft vs stored).
3490
3672
  const sessionField = (key) => payload[key] ?? payload.contextUsage?.[key] ?? null;
3491
3673
  const modelContextLimit = sessionField('modelContextLimit');
3674
+ // A DeepSeek or Claude API chat runs the same Claude Code runner, so it has
3675
+ // the same compaction window to show and to steer.
3676
+ const runsClaudeCode = payload.providerType === 'claude' || payload.providerType === 'deepseek'
3677
+ || payload.providerType === 'claude-api';
3492
3678
  const usageHtml = renderContextUsageHtml(payload.contextUsage, {
3493
- compactionWindow: payload.providerType === 'claude',
3679
+ compactionWindow: runsClaudeCode,
3494
3680
  modelContextLimit,
3495
3681
  });
3496
3682
  // The slider's writes need the conversation id even when the modal was
3497
3683
  // opened through an sdk_session_id.
3498
3684
  const boundConversationId = String(
3499
- payload.resolvedConversationId || (payload.providerType === 'claude' ? trimmedConvId : '') || '',
3685
+ payload.resolvedConversationId || (runsClaudeCode ? trimmedConvId : '') || '',
3500
3686
  ).trim();
3501
3687
  // A draft for another conversation is saved before this one takes over.
3502
3688
  if (autoCompactDraft.conversationId && autoCompactDraft.conversationId !== boundConversationId) {
@@ -3512,9 +3698,9 @@ async function loadContextSummaryAndRender(convId) {
3512
3698
  usedTokens: payload.contextUsage?.totalTokens ?? null,
3513
3699
  };
3514
3700
  const draftWindow = autoCompactDraft.conversationId === boundConversationId ? autoCompactDraft.window : undefined;
3515
- // Claude-only: no other provider exposes a compaction window (or any
3516
- // session-settings surface) to steer.
3517
- const autoCompactHtml = payload.providerType === 'claude'
3701
+ // Claude Code only (Claude and DeepSeek chats): no other provider exposes a
3702
+ // compaction window (or any session-settings surface) to steer.
3703
+ const autoCompactHtml = runsClaudeCode
3518
3704
  ? renderAutoCompactControlHtml({
3519
3705
  autoCompactWindow: payload.autoCompactWindow ?? null,
3520
3706
  autoCompactThreshold: payload.contextUsage?.autoCompactThreshold ?? null,
@@ -3534,15 +3720,17 @@ async function loadContextSummaryAndRender(convId) {
3534
3720
  usedTokens: autoCompactControlSession.usedTokens,
3535
3721
  })
3536
3722
  : '';
3537
- const thinkingHtml = payload.providerType === 'claude'
3723
+ const thinkingHtml = payload.providerType === 'claude' || payload.providerType === 'claude-api'
3538
3724
  ? renderThinkingControlHtml({
3539
3725
  thinkingEnabled: payload.thinkingEnabled !== false,
3540
3726
  thinkingDisplay: payload.thinkingDisplay ?? null,
3541
3727
  })
3542
3728
  : '';
3543
- // What this folder's commits say, with the folder override: Claude, and a
3544
- // Claude Cloud chat that was started from a folder (no path, no row).
3545
- const attributionHtml = payload.providerType === 'claude' || payload.providerType === 'claude-cloud'
3729
+ // What this folder's commits say, with the folder override: Claude (on
3730
+ // either billing), and a Claude Cloud chat that was started from a folder
3731
+ // (no path, no row).
3732
+ const attributionHtml = payload.providerType === 'claude' || payload.providerType === 'claude-api'
3733
+ || payload.providerType === 'claude-cloud'
3546
3734
  ? renderAttributionControlHtml(payload.attribution || null)
3547
3735
  : '';
3548
3736
  thinkingControlStored = {
@@ -4571,6 +4759,8 @@ initSocketHandlers({
4571
4759
  applyCopilotSettingsState,
4572
4760
  applyGrokSettingsState,
4573
4761
  applyCursorSettingsState,
4762
+ applyDeepSeekSettingsState,
4763
+ applyClaudeApiSettingsState,
4574
4764
  });
4575
4765
 
4576
4766
  initCwdPicker({
@@ -4655,6 +4845,15 @@ async function initApp() {
4655
4845
  syncWindowRetentionSlider();
4656
4846
  initHostSuspendUi();
4657
4847
  initUsageLimitUi();
4848
+ // After a switch the chat's provider changed: its model list, lock note
4849
+ // and list pill follow.
4850
+ initClaudeBillingToggle({
4851
+ isSharedReaderMode,
4852
+ onConversationProviderChanged: (conversationId) => {
4853
+ if (conversationId === currentConvId) syncAutoModelAvailability();
4854
+ renderConvList();
4855
+ },
4856
+ });
4658
4857
  }
4659
4858
  setupViewportTracking();
4660
4859
  bindChatSelectionGuard();
@@ -4953,11 +5152,17 @@ async function initApp() {
4953
5152
  // Loaded here for provider readiness: whether any provider can run a chat
4954
5153
  // decides what the empty screen, New Chat and the composer offer.
4955
5154
  const claudeSettings = await refreshClaudeSettingsState();
5155
+ // null on a relay without DeepSeek: not offered, as for Claude Cloud.
5156
+ const deepseekSettings = await refreshDeepSeekSettingsState();
5157
+ // The same for Claude API; its state also decides the composer's billing toggle.
5158
+ const claudeApiSettings = await refreshClaudeApiSettingsState();
4956
5159
  updateProviderReadinessInputs({
4957
5160
  openai: openAISettings || null,
4958
5161
  cursor: cursorSettings || null,
4959
5162
  grok: grokSettings || null,
4960
5163
  claude: claudeSettings || null,
5164
+ deepseek: deepseekSettings || null,
5165
+ claudeApi: claudeApiSettings || null,
4961
5166
  });
4962
5167
  // Not awaited: only a Claude Cloud conversation needs it (its model list),
4963
5168
  // and the subscription below rebuilds that list when the answer arrives. A
@@ -5133,6 +5338,15 @@ window.cancelRunningUpdate = cancelRunningUpdate;
5133
5338
  window.dismissAvailableUpdate = dismissAvailableUpdate;
5134
5339
  window.dismissUpdateOutcome = dismissUpdateOutcome;
5135
5340
  window.saveGrokSettings = saveGrokSettings;
5341
+ window.saveDeepSeekSettings = saveDeepSeekSettings;
5342
+ window.removeDeepSeekSettings = removeDeepSeekSettings;
5343
+ window.toggleDeepSeekProvider = toggleDeepSeekProvider;
5344
+ window.importDeepSeekUsageExport = importDeepSeekUsageExport;
5345
+ window.saveClaudeApiSettings = saveClaudeApiSettings;
5346
+ window.removeClaudeApiSettings = removeClaudeApiSettings;
5347
+ window.toggleClaudeApiProvider = toggleClaudeApiProvider;
5348
+ window.refreshClaudeApiModels = refreshClaudeApiModels;
5349
+ window.openClaudeTabFromClaudeApi = openClaudeTabFromClaudeApi;
5136
5350
  window.toggleGrokProvider = toggleGrokProvider;
5137
5351
  window.saveClaudeCloudSettings = saveClaudeCloudSettings;
5138
5352
  window.toggleClaudeCloudProvider = toggleClaudeCloudProvider;