@oar-sh/oar 0.9.16 → 0.9.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/.github/extensions/web-relay/polling/polling-loop.mjs +14 -87
  2. package/CHANGELOG.md +220 -0
  3. package/README.md +12 -11
  4. package/package.json +2 -1
  5. package/server/claude-worker/claude-attachments.mjs +7 -4
  6. package/server/claude-worker/claude-sdk-adapter.mjs +167 -9
  7. package/server/claude-worker/claude-session-process.mjs +127 -29
  8. package/server/claude-worker/claude-session-worker-main.mjs +266 -0
  9. package/server/claude-worker/claude-session-worker.mjs +2 -253
  10. package/server/claude-worker/claude-turn-failure.mjs +8 -0
  11. package/server/claude-worker/claude-turn-publisher.mjs +282 -13
  12. package/server/claude-worker/claude-usage-totals.mjs +108 -0
  13. package/server/claude-worker/sdk-message-normalizer.mjs +55 -0
  14. package/server/copilot-worker/copilot-sdk-adapter.mjs +18 -4
  15. package/server/copilot-worker/copilot-sdk-session-process.mjs +71 -5
  16. package/server/db-schema.mjs +63 -0
  17. package/server/deepseek-worker/deepseek-claude-backend.mjs +433 -0
  18. package/server/deepseek-worker/deepseek-session-worker.mjs +37 -0
  19. package/server/migrations/0024-deepseek-usage-totals.mjs +151 -0
  20. package/server/migrations/0025-deepseek-usage-requests-daily.mjs +179 -0
  21. package/server/migrations/0026-deepseek-usage-served-models.mjs +222 -0
  22. package/server/migrations/0027-deepseek-usage-checkpoint-runs.mjs +101 -0
  23. package/server/migrations/0028-deepseek-usage-checkpoint-request-runs.mjs +99 -0
  24. package/server/migrations/0029-deepseek-balance-rises.mjs +99 -0
  25. package/server/migrations/0030-deepseek-usage-actuals.mjs +123 -0
  26. package/server/public/app/api-client.js +53 -0
  27. package/server/public/app/bootstrap.js +101 -13
  28. package/server/public/app/context-tier-options.mjs +4 -0
  29. package/server/public/app/conversation-provider-indicator.mjs +7 -0
  30. package/server/public/app/conversation-view.js +2 -0
  31. package/server/public/app/deepseek-settings-ui.js +342 -0
  32. package/server/public/app/inbox-ui.js +14 -3
  33. package/server/public/app/journal-view.js +31 -3
  34. package/server/public/app/model-selector-options.mjs +11 -0
  35. package/server/public/app/model-vision-support.mjs +2 -0
  36. package/server/public/app/no-conversation-state.mjs +1 -1
  37. package/server/public/app/plan-usage-view.mjs +165 -13
  38. package/server/public/app/provider-readiness.mjs +4 -1
  39. package/server/public/app/relay-error-ctas.mjs +8 -0
  40. package/server/public/app/settings-modal.js +11 -0
  41. package/server/public/app/settings-tabs.js +1 -1
  42. package/server/public/app/socket-handlers.js +10 -0
  43. package/server/public/index.html +408 -33
  44. package/server/repositories/conversation-group-repository.mjs +13 -6
  45. package/server/routes/conversation-group-routes.mjs +2 -1
  46. package/server/routes/deepseek-usage-import-routes.mjs +72 -0
  47. package/server/routes/messages-routes.mjs +192 -15
  48. package/server/routes/sessions-routes.mjs +364 -55
  49. package/server/server-runtime.mjs +123 -10
  50. package/server/services/about-service.mjs +1 -0
  51. package/server/services/claude-session-leftovers.mjs +226 -0
  52. package/server/services/deepseek-balance-service.mjs +102 -0
  53. package/server/services/deepseek-settings-service.mjs +380 -0
  54. package/server/services/deepseek-usage-import.mjs +471 -0
  55. package/server/services/model-variant-catalog-service.mjs +1 -0
  56. package/server/services/plan-usage-contract.mjs +100 -5
  57. package/server/services/plan-usage-deepseek.mjs +931 -0
  58. package/server/services/plan-usage-service.mjs +431 -0
  59. package/server/services/provider-model-selection.mjs +1 -0
  60. package/server/services/provider-reasoning-effort.mjs +15 -2
  61. package/server/services/remote-relay-dispatcher.mjs +2 -1
  62. package/server/services/session-worker-launch-service.mjs +233 -82
  63. package/server/services/worker-secret-redaction.mjs +318 -0
  64. package/shared/context-window-fallbacks.mjs +3 -0
  65. package/shared/deepseek-api.mjs +137 -0
  66. package/shared/deepseek-models.mjs +114 -0
  67. package/shared/deepseek-pricing.mjs +146 -0
  68. package/shared/host-claude-session-env.mjs +101 -0
  69. package/shared/model-id.mjs +7 -0
  70. package/shared/preview-tool-core.mjs +1 -1
  71. package/shared/provider-routing.mjs +2 -2
  72. package/shared/remote-relay-contract.mjs +1 -1
  73. package/shared/steer-settle-failure.mjs +1 -0
  74. package/shared/worker-runtime/direct-openai-image.mjs +128 -0
  75. package/shared/worker-runtime/worker-log-file.mjs +60 -0
@@ -79,6 +79,7 @@ import {
79
79
  updateClaudeSettings,
80
80
  updateCursorSettings,
81
81
  updateGrokSettings,
82
+ updateDeepSeekSettings,
82
83
  loadConversation,
83
84
  refreshConversationHistory,
84
85
  updateConversationTitle,
@@ -323,6 +324,12 @@ import {
323
324
  toggleGrokProvider,
324
325
  applyGrokSettingsState,
325
326
  refreshGrokSettingsState,
327
+ saveDeepSeekSettings,
328
+ removeDeepSeekSettings,
329
+ toggleDeepSeekProvider,
330
+ importDeepSeekUsageExport,
331
+ applyDeepSeekSettingsState,
332
+ refreshDeepSeekSettingsState,
326
333
  saveCursorSettings,
327
334
  removeCursorSettings,
328
335
  saveCursorAllowanceSettings,
@@ -445,6 +452,7 @@ const PROVIDER_LABELS = {
445
452
  'claude-cloud': 'Claude Cloud',
446
453
  cursor: 'Cursor SDK',
447
454
  grok: 'Grok',
455
+ deepseek: 'DeepSeek',
448
456
  'github-copilot': 'GitHub Copilot',
449
457
  // Vendor grouping for Copilot-served rows, distinct from the Claude SDK runtime.
450
458
  anthropic: 'Anthropic',
@@ -1505,6 +1513,7 @@ function normalizeModelSelectorProviderType(providerType = '') {
1505
1513
  if (normalized === CLAUDE_CLOUD_PROVIDER) return CLAUDE_CLOUD_PROVIDER;
1506
1514
  if (normalized === 'cursor') return 'cursor';
1507
1515
  if (normalized === 'grok') return 'grok';
1516
+ if (normalized === 'deepseek') return 'deepseek';
1508
1517
  return 'github';
1509
1518
  }
1510
1519
 
@@ -1532,6 +1541,7 @@ function modelVisibleForActiveProvider(modelId, activeProviderType, providersByM
1532
1541
  const hasClaude = providers.includes('claude');
1533
1542
  const hasCursor = providers.includes('cursor');
1534
1543
  const hasGrok = providers.includes('grok');
1544
+ const hasDeepSeek = providers.includes('deepseek');
1535
1545
  // Only present if a relay tags catalog rows for Claude Cloud; such a row is
1536
1546
  // then as exclusive as the other runtimes' rows.
1537
1547
  const hasClaudeCloud = providers.includes(CLAUDE_CLOUD_PROVIDER);
@@ -1540,6 +1550,7 @@ function modelVisibleForActiveProvider(modelId, activeProviderType, providersByM
1540
1550
  && provider !== 'claude'
1541
1551
  && provider !== 'cursor'
1542
1552
  && provider !== 'grok'
1553
+ && provider !== 'deepseek'
1543
1554
  && provider !== CLAUDE_CLOUD_PROVIDER
1544
1555
  ));
1545
1556
  const activeProvider = normalizeModelSelectorProviderType(activeProviderType);
@@ -1547,7 +1558,8 @@ function modelVisibleForActiveProvider(modelId, activeProviderType, providersByM
1547
1558
  if (activeProvider === 'claude') return hasClaude;
1548
1559
  if (activeProvider === 'cursor') return hasCursor;
1549
1560
  if (activeProvider === 'grok') return hasGrok;
1550
- return !((hasOpenAIByok || hasClaude || hasCursor || hasGrok || hasClaudeCloud) && !hasNonExclusiveProvider);
1561
+ if (activeProvider === 'deepseek') return hasDeepSeek;
1562
+ return !((hasOpenAIByok || hasClaude || hasCursor || hasGrok || hasDeepSeek || hasClaudeCloud) && !hasNonExclusiveProvider);
1551
1563
  }
1552
1564
 
1553
1565
  // A Claude Cloud conversation does not pick from the shared catalog: its
@@ -1990,6 +2002,7 @@ const RELAY_MODES_BY_PROVIDER = {
1990
2002
  claude: ['agent', 'ask', 'plan', 'autopilot'],
1991
2003
  cursor: ['agent', 'ask', 'plan', 'autopilot'],
1992
2004
  grok: ['agent', 'ask', 'plan', 'autopilot'],
2005
+ deepseek: ['agent', 'ask', 'plan', 'autopilot'],
1993
2006
  'claude-cloud': ['agent'],
1994
2007
  };
1995
2008
 
@@ -2336,6 +2349,13 @@ function reportOpenAIModelDiscoveryFailure(payload) {
2336
2349
  8000,
2337
2350
  );
2338
2351
  }
2352
+ const deepseekDiscovery = payload?.deepseekModelDiscovery;
2353
+ if (deepseekDiscovery && deepseekDiscovery.ok === false && !deepseekDiscovery.skipped) {
2354
+ showTransientRelayNotice(
2355
+ `DeepSeek model discovery failed. Cached DeepSeek models were kept: ${deepseekDiscovery.error || 'unknown error'}`,
2356
+ 8000,
2357
+ );
2358
+ }
2339
2359
  }
2340
2360
 
2341
2361
  async function retryModelMetadataRefresh() {
@@ -2473,6 +2493,17 @@ function applyModelVariantCatalogState(payload) {
2473
2493
  : [],
2474
2494
  }
2475
2495
  : null,
2496
+ deepseekModels: payload?.deepseekModels && typeof payload.deepseekModels === 'object'
2497
+ ? {
2498
+ defaultModel: String(payload.deepseekModels.defaultModel || '').trim(),
2499
+ availableModels: Array.isArray(payload.deepseekModels.availableModels)
2500
+ ? payload.deepseekModels.availableModels.map((value) => String(value || '').trim()).filter(Boolean)
2501
+ : [],
2502
+ enabledModels: Array.isArray(payload.deepseekModels.enabledModels)
2503
+ ? payload.deepseekModels.enabledModels.map((value) => String(value || '').trim()).filter(Boolean)
2504
+ : [],
2505
+ }
2506
+ : null,
2476
2507
  };
2477
2508
  }
2478
2509
 
@@ -2549,6 +2580,10 @@ function renderModelVariantCatalogBody() {
2549
2580
  return entryProvider === 'grok';
2550
2581
  }
2551
2582
  if (entryProvider === 'grok') return false;
2583
+ if (activeTab === 'deepseek') {
2584
+ return entryProvider === 'deepseek';
2585
+ }
2586
+ if (entryProvider === 'deepseek') return false;
2552
2587
  if (activeTab === 'openai') {
2553
2588
  return hasOpenAIByok || entryProvider === 'openai-byok';
2554
2589
  }
@@ -2560,6 +2595,7 @@ function renderModelVariantCatalogBody() {
2560
2595
  && provider !== 'claude'
2561
2596
  && provider !== 'cursor'
2562
2597
  && provider !== 'grok'
2598
+ && provider !== 'deepseek'
2563
2599
  && provider !== CLAUDE_CLOUD_PROVIDER
2564
2600
  ));
2565
2601
  }
@@ -2670,6 +2706,29 @@ function renderModelVariantCatalogBody() {
2670
2706
  }
2671
2707
  grouped.set('grok', providerRows);
2672
2708
  }
2709
+ const deepseekCatalog = modelVariantCatalogState.deepseekModels;
2710
+ if (deepseekCatalog?.availableModels?.length) {
2711
+ const deepseekEnabledSet = new Set(deepseekCatalog.enabledModels || []);
2712
+ const deepseekDefaultModel = String(deepseekCatalog.defaultModel || '').trim();
2713
+ const providerRows = [];
2714
+ for (const [index, deepseekModelId] of deepseekCatalog.availableModels.entries()) {
2715
+ const isDefaultModel = deepseekModelId === deepseekDefaultModel;
2716
+ providerRows.push({
2717
+ variantId: `${deepseekModelId}--provider-deepseek`,
2718
+ baseModelId: deepseekModelId,
2719
+ provider: 'deepseek',
2720
+ label: humanizeModelLabel(deepseekModelId),
2721
+ releaseStatus: null,
2722
+ reasoningEffort: null,
2723
+ // The default model always stays enabled, as for the other providers.
2724
+ selectable: !isDefaultModel,
2725
+ enabled: deepseekEnabledSet.has(deepseekModelId) || isDefaultModel,
2726
+ deepseekModelId,
2727
+ sortOrder: index,
2728
+ });
2729
+ }
2730
+ grouped.set('deepseek', providerRows);
2731
+ }
2673
2732
  const selected = new Set(modelVariantCatalogState.enabledVariantIds);
2674
2733
  const selectedOrder = new Map(
2675
2734
  modelVariantCatalogState.enabledVariantIds.map((variantId, index) => [variantId, index]),
@@ -2705,15 +2764,18 @@ function renderModelVariantCatalogBody() {
2705
2764
  const hasAnthropicTab = providerOrder.some((providerKey) => providerBelongsToTab(providerKey, 'anthropic'));
2706
2765
  const hasCursorTab = providerOrder.some((providerKey) => providerBelongsToTab(providerKey, 'cursor'));
2707
2766
  const hasGrokTab = providerOrder.some((providerKey) => providerBelongsToTab(providerKey, 'grok'));
2767
+ const hasDeepSeekTab = providerOrder.some((providerKey) => providerBelongsToTab(providerKey, 'deepseek'));
2708
2768
  if (modelVariantCatalogProviderTab === 'openai' && !hasOpenAITab) modelVariantCatalogProviderTab = 'copilot';
2709
2769
  if (modelVariantCatalogProviderTab === 'anthropic' && !hasAnthropicTab) modelVariantCatalogProviderTab = 'copilot';
2710
2770
  if (modelVariantCatalogProviderTab === 'cursor' && !hasCursorTab) modelVariantCatalogProviderTab = 'copilot';
2711
2771
  if (modelVariantCatalogProviderTab === 'grok' && !hasGrokTab) modelVariantCatalogProviderTab = 'copilot';
2772
+ if (modelVariantCatalogProviderTab === 'deepseek' && !hasDeepSeekTab) modelVariantCatalogProviderTab = 'copilot';
2712
2773
  if (modelVariantCatalogProviderTab === 'copilot' && !hasCopilotTab) {
2713
2774
  if (hasOpenAITab) modelVariantCatalogProviderTab = 'openai';
2714
2775
  else if (hasAnthropicTab) modelVariantCatalogProviderTab = 'anthropic';
2715
2776
  else if (hasCursorTab) modelVariantCatalogProviderTab = 'cursor';
2716
2777
  else if (hasGrokTab) modelVariantCatalogProviderTab = 'grok';
2778
+ else if (hasDeepSeekTab) modelVariantCatalogProviderTab = 'deepseek';
2717
2779
  }
2718
2780
  const visibleProviderOrder = providerOrder.filter((providerKey) => providerBelongsToTab(providerKey, modelVariantCatalogProviderTab));
2719
2781
  const providerTabButtons = `
@@ -2723,6 +2785,7 @@ function renderModelVariantCatalogBody() {
2723
2785
  <button type="button" class="summary-btn model-provider-tab${modelVariantCatalogProviderTab === 'anthropic' ? ' active' : ''}" data-model-provider-tab="anthropic" ${hasAnthropicTab ? '' : 'disabled'}>Claude SDK</button>
2724
2786
  <button type="button" class="summary-btn model-provider-tab${modelVariantCatalogProviderTab === 'cursor' ? ' active' : ''}" data-model-provider-tab="cursor" ${hasCursorTab ? '' : 'disabled'}>Cursor SDK</button>
2725
2787
  <button type="button" class="summary-btn model-provider-tab${modelVariantCatalogProviderTab === 'grok' ? ' active' : ''}" data-model-provider-tab="grok" ${hasGrokTab ? '' : 'disabled'}>Grok</button>
2788
+ <button type="button" class="summary-btn model-provider-tab${modelVariantCatalogProviderTab === 'deepseek' ? ' active' : ''}" data-model-provider-tab="deepseek" ${hasDeepSeekTab ? '' : 'disabled'}>DeepSeek</button>
2726
2789
  </div>
2727
2790
  `;
2728
2791
  const warnings = [
@@ -2775,9 +2838,9 @@ function renderModelVariantCatalogBody() {
2775
2838
  : '';
2776
2839
  const variantRowsHtml = variantRows.map((row) => {
2777
2840
  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 || '';
2841
+ // SDK-provider rows (Claude/Cursor/Grok/DeepSeek) carry their own enabled
2842
+ // flag; only Copilot variant rows read the enabledVariantIds selection.
2843
+ const sdkProviderModelId = row.claudeModelId || row.cursorModelId || row.grokModelId || row.deepseekModelId || '';
2781
2844
  const checked = sdkProviderModelId
2782
2845
  ? row.enabled === true
2783
2846
  : (selectable && selected.has(row.variantId));
@@ -2797,7 +2860,9 @@ function renderModelVariantCatalogBody() {
2797
2860
  ? ` data-claude-model="${escHtml(row.claudeModelId)}"`
2798
2861
  : (row.cursorModelId
2799
2862
  ? ` data-cursor-model="${escHtml(row.cursorModelId)}"`
2800
- : (row.grokModelId ? ` data-grok-model="${escHtml(row.grokModelId)}"` : ''));
2863
+ : (row.grokModelId
2864
+ ? ` data-grok-model="${escHtml(row.grokModelId)}"`
2865
+ : (row.deepseekModelId ? ` data-deepseek-model="${escHtml(row.deepseekModelId)}"` : '')));
2801
2866
  return `
2802
2867
  <label class="model-variant-row">
2803
2868
  <input class="model-variant-checkbox" type="checkbox" data-selectable="${selectable ? '1' : '0'}" data-variant-id="${escHtml(row.variantId)}"${sdkModelAttr} ${checked ? 'checked' : ''} ${selectable ? '' : 'disabled'}>
@@ -2923,12 +2988,17 @@ async function saveSelectedModelsFromModal() {
2923
2988
  .filter((input) => input.checked)
2924
2989
  .map((input) => String(input.getAttribute('data-grok-model') || '').trim())
2925
2990
  .filter(Boolean);
2991
+ const deepseekCheckboxes = Array.from(body.querySelectorAll('.model-variant-checkbox[data-deepseek-model]'));
2992
+ const selectedDeepSeekModels = deepseekCheckboxes
2993
+ .filter((input) => input.checked)
2994
+ .map((input) => String(input.getAttribute('data-deepseek-model') || '').trim())
2995
+ .filter(Boolean);
2926
2996
  // Only the active provider tab's rows are in the DOM, but the PATCH
2927
2997
  // replaces the WHOLE enabled set — so the save must only change rows the
2928
2998
  // user could actually see. Off-screen variants keep their stored state,
2929
2999
  // otherwise saving from e.g. the OpenAI tab silently disables every
2930
3000
  // 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])'));
3001
+ 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])'));
2932
3002
  const renderedVariantIds = new Set(renderedVariantCheckboxes
2933
3003
  .map((input) => String(input.getAttribute('data-variant-id') || '').trim())
2934
3004
  .filter(Boolean));
@@ -2969,7 +3039,12 @@ async function saveSelectedModelsFromModal() {
2969
3039
  if (!savedGrok) throw new Error('Failed to save Grok model selection');
2970
3040
  applyGrokSettingsState(savedGrok);
2971
3041
  }
2972
- if (claudeCheckboxes.length || cursorCheckboxes.length || grokCheckboxes.length) {
3042
+ if (deepseekCheckboxes.length) {
3043
+ const savedDeepSeek = await updateDeepSeekSettings({ enabledModels: selectedDeepSeekModels });
3044
+ if (!savedDeepSeek) throw new Error('Failed to save DeepSeek model selection');
3045
+ applyDeepSeekSettingsState(savedDeepSeek);
3046
+ }
3047
+ if (claudeCheckboxes.length || cursorCheckboxes.length || grokCheckboxes.length || deepseekCheckboxes.length) {
2973
3048
  const refreshedCatalog = await loadModelVariantCatalog();
2974
3049
  if (refreshedCatalog) applyModelVariantCatalogState(refreshedCatalog);
2975
3050
  }
@@ -3480,7 +3555,9 @@ async function loadContextSummaryAndRender(convId) {
3480
3555
  ? 'Claude Cloud'
3481
3556
  : (payload.providerType === 'cursor'
3482
3557
  ? 'Cursor'
3483
- : (payload.providerType === 'grok' ? 'Grok' : 'Copilot')));
3558
+ : (payload.providerType === 'grok'
3559
+ ? 'Grok'
3560
+ : (payload.providerType === 'deepseek' ? 'DeepSeek' : 'Copilot'))));
3484
3561
  const subtitle = sessionId
3485
3562
  ? `${providerLabel} session ${sessionId.slice(0, 8)}`
3486
3563
  : (trimmedConvId ? `Conversation ${trimmedConvId.slice(0, 8)}` : 'No conversation selected');
@@ -3489,14 +3566,17 @@ async function loadContextSummaryAndRender(convId) {
3489
3566
  // (older relays send neither, and the notes fall back to draft vs stored).
3490
3567
  const sessionField = (key) => payload[key] ?? payload.contextUsage?.[key] ?? null;
3491
3568
  const modelContextLimit = sessionField('modelContextLimit');
3569
+ // A DeepSeek chat runs the same Claude Code runner, so it has the same
3570
+ // compaction window to show and to steer.
3571
+ const runsClaudeCode = payload.providerType === 'claude' || payload.providerType === 'deepseek';
3492
3572
  const usageHtml = renderContextUsageHtml(payload.contextUsage, {
3493
- compactionWindow: payload.providerType === 'claude',
3573
+ compactionWindow: runsClaudeCode,
3494
3574
  modelContextLimit,
3495
3575
  });
3496
3576
  // The slider's writes need the conversation id even when the modal was
3497
3577
  // opened through an sdk_session_id.
3498
3578
  const boundConversationId = String(
3499
- payload.resolvedConversationId || (payload.providerType === 'claude' ? trimmedConvId : '') || '',
3579
+ payload.resolvedConversationId || (runsClaudeCode ? trimmedConvId : '') || '',
3500
3580
  ).trim();
3501
3581
  // A draft for another conversation is saved before this one takes over.
3502
3582
  if (autoCompactDraft.conversationId && autoCompactDraft.conversationId !== boundConversationId) {
@@ -3512,9 +3592,9 @@ async function loadContextSummaryAndRender(convId) {
3512
3592
  usedTokens: payload.contextUsage?.totalTokens ?? null,
3513
3593
  };
3514
3594
  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'
3595
+ // Claude Code only (Claude and DeepSeek chats): no other provider exposes a
3596
+ // compaction window (or any session-settings surface) to steer.
3597
+ const autoCompactHtml = runsClaudeCode
3518
3598
  ? renderAutoCompactControlHtml({
3519
3599
  autoCompactWindow: payload.autoCompactWindow ?? null,
3520
3600
  autoCompactThreshold: payload.contextUsage?.autoCompactThreshold ?? null,
@@ -4571,6 +4651,7 @@ initSocketHandlers({
4571
4651
  applyCopilotSettingsState,
4572
4652
  applyGrokSettingsState,
4573
4653
  applyCursorSettingsState,
4654
+ applyDeepSeekSettingsState,
4574
4655
  });
4575
4656
 
4576
4657
  initCwdPicker({
@@ -4953,11 +5034,14 @@ async function initApp() {
4953
5034
  // Loaded here for provider readiness: whether any provider can run a chat
4954
5035
  // decides what the empty screen, New Chat and the composer offer.
4955
5036
  const claudeSettings = await refreshClaudeSettingsState();
5037
+ // null on a relay without DeepSeek: not offered, as for Claude Cloud.
5038
+ const deepseekSettings = await refreshDeepSeekSettingsState();
4956
5039
  updateProviderReadinessInputs({
4957
5040
  openai: openAISettings || null,
4958
5041
  cursor: cursorSettings || null,
4959
5042
  grok: grokSettings || null,
4960
5043
  claude: claudeSettings || null,
5044
+ deepseek: deepseekSettings || null,
4961
5045
  });
4962
5046
  // Not awaited: only a Claude Cloud conversation needs it (its model list),
4963
5047
  // and the subscription below rebuilds that list when the answer arrives. A
@@ -5133,6 +5217,10 @@ window.cancelRunningUpdate = cancelRunningUpdate;
5133
5217
  window.dismissAvailableUpdate = dismissAvailableUpdate;
5134
5218
  window.dismissUpdateOutcome = dismissUpdateOutcome;
5135
5219
  window.saveGrokSettings = saveGrokSettings;
5220
+ window.saveDeepSeekSettings = saveDeepSeekSettings;
5221
+ window.removeDeepSeekSettings = removeDeepSeekSettings;
5222
+ window.toggleDeepSeekProvider = toggleDeepSeekProvider;
5223
+ window.importDeepSeekUsageExport = importDeepSeekUsageExport;
5136
5224
  window.toggleGrokProvider = toggleGrokProvider;
5137
5225
  window.saveClaudeCloudSettings = saveClaudeCloudSettings;
5138
5226
  window.toggleClaudeCloudProvider = toggleClaudeCloudProvider;
@@ -1,6 +1,8 @@
1
1
  // Mirrors shared/model-id.mjs, which the browser cannot import (only
2
2
  // server/public is served).
3
3
  const CLAUDE_LONG_CONTEXT_LIMIT_TOKENS = 1000000;
4
+ // Mirrors shared/deepseek-models.mjs for the same reason.
5
+ const DEEPSEEK_CONTEXT_WINDOW_TOKENS = 1048576;
4
6
  export const UNKNOWN_WINDOW_LABEL = '—';
5
7
 
6
8
  // Shared by the context chip and both model pickers so a window reads the same
@@ -59,6 +61,8 @@ export function buildContextTierOptions({
59
61
  // metadata either (that number is Copilot's): one blank default, as for a
60
62
  // Claude model whose window is unknown.
61
63
  if (provider === 'claude-cloud') return [{ value: 'default', label: UNKNOWN_WINDOW_LABEL }];
64
+ // DeepSeek's models have one window, 1M: no tier to choose.
65
+ if (provider === 'deepseek') return [{ value: 'default', label: tokenLabel(DEEPSEEK_CONTEXT_WINDOW_TOKENS) }];
62
66
  const claudeTierList = provider === 'claude' ? claudeTiersForModel(modelId, claudeTiers) : null;
63
67
  if (claudeTierList) {
64
68
  return claudeTierList
@@ -23,6 +23,10 @@ export function isConversationUsingGrokProvider(conversation = null) {
23
23
  return resolveConversationProviderType(conversation) === 'grok';
24
24
  }
25
25
 
26
+ export function isConversationUsingDeepSeekProvider(conversation = null) {
27
+ return resolveConversationProviderType(conversation) === 'deepseek';
28
+ }
29
+
26
30
  // Its own provider, never a flavour of `claude`: the session runs in a sandbox
27
31
  // at Anthropic, so nothing that is true of a local Claude worker (modes,
28
32
  // effort, steering, a working directory) may be assumed for it.
@@ -39,6 +43,7 @@ const PROVIDER_NAMES = Object.freeze({
39
43
  'claude-cloud': 'Claude Cloud',
40
44
  cursor: 'Cursor',
41
45
  grok: 'Grok',
46
+ deepseek: 'DeepSeek',
42
47
  github: 'Copilot',
43
48
  'github-copilot': 'Copilot',
44
49
  });
@@ -76,6 +81,7 @@ const SESSION_LOCK_LABELS = {
76
81
  claude: 'Claude SDK',
77
82
  cursor: 'Cursor SDK',
78
83
  grok: 'Grok',
84
+ deepseek: 'DeepSeek',
79
85
  'claude-cloud': `${CLAUDE_CLOUD_MARKER} Claude Cloud`,
80
86
  };
81
87
 
@@ -96,6 +102,7 @@ export function sessionLockProviderKey({ providerType = '', model = '' } = {}) {
96
102
  if (normalized === 'claude-cloud') return 'claude-cloud';
97
103
  if (normalized === 'cursor') return 'cursor';
98
104
  if (normalized === 'grok' || normalized === 'xai' || normalized === 'xai-grok') return 'grok';
105
+ if (normalized === 'deepseek') return 'deepseek';
99
106
  if (normalized === 'github' || normalized === 'github-copilot') return 'github';
100
107
  return '';
101
108
  }
@@ -3523,6 +3523,8 @@ function runRelayErrorCta(cta) {
3523
3523
  }
3524
3524
  if (cta === 'open-grok-settings') openSettingsModal('providers', 'grok');
3525
3525
  if (cta === 'open-claude-settings') openSettingsModal('providers', 'claude');
3526
+ if (cta === 'open-deepseek-settings') openSettingsModal('providers', 'deepseek');
3527
+ if (cta === 'open-deepseek-top-up') window.open('https://platform.deepseek.com/top_up', '_blank', 'noopener');
3526
3528
  }
3527
3529
 
3528
3530
  function handleBubbleActionClick(event) {