dsh-custom-api 1.0.1 → 1.0.2

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 (2) hide show
  1. package/lib/index.js +196 -35
  2. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -2,10 +2,10 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
4
  /**
5
- * Plugin: @local/dsh-model-discovery
5
+ * Plugin: dsh-custom-api
6
6
  * Tự động phát hiện model từ Custom API (OpenAI/Anthropic compatible),
7
- * kết hợp API Endpoint Metadata, models.dev và bộ quy chuẩn Reasoning Ladder theo từng hãng (vendor)
8
- * lấy cảm hứng từ dsh-dc-ai-api.
7
+ * kết hợp API Endpoint Metadata, models.dev và bộ quy chuẩn Reasoning Ladder theo từng hãng (vendor).
8
+ * Giải quyết triệt để lỗi "không hỗ trợ reasoning effort" khi chuyển mức độ suy nghĩ.
9
9
  */
10
10
 
11
11
  export const name = 'dsh-custom-api';
@@ -52,23 +52,18 @@ const THINKING_DESCRIPTION =
52
52
 
53
53
  /**
54
54
  * Reasoning Ladder chuẩn hóa theo từng hãng từ tài liệu API chính thức:
55
- * - DeepSeek: none / low / high / max (api-docs.deepseek.com)
56
- * - OpenAI: low / medium / high / xhigh / max (platform.openai.com)
57
- * - Gemini: per-model (low/medium/high, Flash dòng 3.x có thêm minimal)
58
- * - Claude: low / medium / high / xhigh / max (platform.claude.com)
59
- * - Grok: low / medium / high / xhigh (docs.x.ai)
60
- * - GLM: none / high / max (docs.z.ai)
61
- * - Qwen: toggle only (off: null, high: high)
55
+ * Bao gồm đầy đủ 'off' và các nấc 'low', 'medium', 'high', 'max' để người dùng
56
+ * chuyển bất kỳ nấc nào trên giao diện cũng KHÔNG bao giờ bị báo lỗi "không hỗ trợ".
62
57
  */
63
58
  const FAMILY_EFFORTS = Object.freeze({
64
- deepseek: { off: 'none', low: 'low', high: 'high', max: 'max' },
65
- openai: { low: 'low', medium: 'medium', high: 'high', xhigh: 'xhigh', max: 'max' },
66
- gemini: { low: 'low', medium: 'medium', high: 'high' },
67
- claude: { low: 'low', medium: 'medium', high: 'high', xhigh: 'xhigh', max: 'max' },
68
- grok: { low: 'low', medium: 'medium', high: 'high', xhigh: 'xhigh' },
69
- glm: { off: 'none', high: 'high', max: 'max' },
70
- qwen: { off: null, high: 'high' },
71
- other: { off: null, high: 'high' },
59
+ deepseek: { off: 'none', low: 'low', medium: 'high', high: 'high', max: 'max' },
60
+ openai: { off: null, low: 'low', medium: 'medium', high: 'high', xhigh: 'xhigh', max: 'max' },
61
+ gemini: { off: null, minimal: 'minimal', low: 'low', medium: 'medium', high: 'high' },
62
+ claude: { off: null, low: 'low', medium: 'medium', high: 'high', xhigh: 'xhigh', max: 'max' },
63
+ grok: { off: null, low: 'low', medium: 'medium', high: 'high', xhigh: 'xhigh' },
64
+ glm: { off: 'none', low: 'high', medium: 'high', high: 'high', max: 'max' },
65
+ qwen: { off: null, low: 'high', medium: 'high', high: 'high' },
66
+ other: { off: null, low: 'low', medium: 'medium', high: 'high', max: 'max' },
72
67
  });
73
68
 
74
69
  const GEMINI_LEVELS = Object.freeze({
@@ -89,8 +84,10 @@ const GEMINI_LEVELS = Object.freeze({
89
84
  function geminiEfforts(id) {
90
85
  const norm = String(id).toLowerCase();
91
86
  const documented = GEMINI_LEVELS[norm] || ['low', 'medium', 'high'];
92
- const efforts = {};
87
+ const efforts = { off: null };
93
88
  for (const level of documented) efforts[level] = level;
89
+ if (!efforts.medium) efforts.medium = efforts.high || 'medium';
90
+ if (!efforts.low) efforts.low = efforts.medium || 'low';
94
91
  return efforts;
95
92
  }
96
93
 
@@ -498,12 +495,174 @@ async function fetchModelsFromApi(baseURL, apiKey, apiProtocol = 'openai-complet
498
495
  throw new Error(`Không thể lấy danh sách model từ ${baseURL}: ${lastError?.message || 'Không tìm thấy models'}`);
499
496
  }
500
497
 
498
+ /**
499
+ * Patch một instance model đã được PiAi resolve để luôn hỗ trợ thinking effort và vision
500
+ */
501
+ function patchResolvedModel(resolved, modelId) {
502
+ if (!resolved || typeof resolved !== 'object') return;
503
+ const cap = resolveCapabilities(modelId || resolved.id);
504
+
505
+ if (cap.hasThinking) {
506
+ resolved.reasoning = true;
507
+ if (!resolved.thinkingLevelMap || typeof resolved.thinkingLevelMap !== 'object') {
508
+ resolved.thinkingLevelMap = {};
509
+ }
510
+ const efforts = cap.modelEntry.reasoningEfforts || {};
511
+ for (const [lvl, wire] of Object.entries(efforts)) {
512
+ if (wire !== null && wire !== undefined) {
513
+ resolved.thinkingLevelMap[lvl] = wire;
514
+ }
515
+ }
516
+
517
+ // Đảm bảo 'off' luôn được hỗ trợ (không bị null)
518
+ if (resolved.thinkingLevelMap.off === null) {
519
+ delete resolved.thinkingLevelMap.off;
520
+ }
521
+
522
+ // Đảm bảo các nấc tiêu chuẩn (low, medium, high) không bị null
523
+ if (resolved.thinkingLevelMap.high === null || resolved.thinkingLevelMap.high === undefined) {
524
+ resolved.thinkingLevelMap.high = 'high';
525
+ }
526
+ if (resolved.thinkingLevelMap.medium === null || resolved.thinkingLevelMap.medium === undefined) {
527
+ resolved.thinkingLevelMap.medium = resolved.thinkingLevelMap.high || 'medium';
528
+ }
529
+ if (resolved.thinkingLevelMap.low === null || resolved.thinkingLevelMap.low === undefined) {
530
+ resolved.thinkingLevelMap.low = resolved.thinkingLevelMap.medium || 'low';
531
+ }
532
+
533
+ if (!resolved.compat || typeof resolved.compat !== 'object') {
534
+ resolved.compat = {};
535
+ }
536
+ resolved.compat.supportsReasoningEffort = true;
537
+ }
538
+
539
+ if (cap.hasVision && Array.isArray(resolved.input) && !resolved.input.includes('image')) {
540
+ resolved.input.push('image');
541
+ }
542
+ }
543
+
544
+ /**
545
+ * Tự động quét và nâng cấp tất cả provider hiện có trong llm-pi-ai trên startup
546
+ */
547
+ async function autoEnhanceStoredProfiles(ctx) {
548
+ if (!ctx.configEditor) return;
549
+ try {
550
+ const entries = ctx.configEditor.entries();
551
+ const entry = entries.find(
552
+ (e) => e.options?.id === 'llm-pi-ai' || e.patchId === 'llm-pi-ai' || e.id === 'include:llm-pi-ai'
553
+ );
554
+ if (!entry) return;
555
+
556
+ await ctx.configEditor.edit(entry, (current = {}) => {
557
+ const next = structuredClone(current);
558
+ if (!next.providers) return next;
559
+
560
+ let hasChanges = false;
561
+ for (const [pid, p] of Object.entries(next.providers)) {
562
+ if (!p || !Array.isArray(p.models)) continue;
563
+ for (const m of p.models) {
564
+ if (!m || typeof m !== 'object') continue;
565
+ const cap = resolveCapabilities(m, { api: p.api });
566
+
567
+ if (cap.hasVision && (!m.input || !m.input.includes('image'))) {
568
+ m.input = ['text', 'image'];
569
+ hasChanges = true;
570
+ }
571
+
572
+ if (cap.hasThinking && (m.reasoningEfforts === undefined || m.reasoningEfforts === false)) {
573
+ m.reasoningEfforts = cap.modelEntry.reasoningEfforts;
574
+ if (p.api === 'openai-completions') {
575
+ const s = (m.id || '').toLowerCase();
576
+ const isDeepSeek = s.includes('deepseek') || s.includes('r1');
577
+ m.compat = {
578
+ ...m.compat,
579
+ supportsReasoningEffort: true,
580
+ ...(isDeepSeek ? { thinkingFormat: 'deepseek' } : {}),
581
+ };
582
+ }
583
+ hasChanges = true;
584
+ }
585
+ }
586
+ }
587
+ return hasChanges ? next : current;
588
+ });
589
+ } catch (err) {
590
+ ctx.logger?.warn(`autoEnhanceStoredProfiles error: ${err.message}`);
591
+ }
592
+ }
593
+
501
594
  export function apply(ctx) {
502
595
  initModelsDevDatabase(ctx.logger);
503
596
  ctx.logger?.info('Custom Model Discovery Plugin (API Metadata + models.dev + Vendor Reasoning Ladders) active');
504
597
 
505
598
  // =========================================================================
506
- // 1. Live Hook vào ctx.llm.discoverModels (Web UI Settings -> Models)
599
+ // 1. Hook trực tiếp PiAiAdapter.prototype.modelOf
600
+ // Đảm bảo ở cấp Engine Adapter, model luôn có reasoning=true và thinkingLevelMap
601
+ // đầy đủ các mức (off, low, medium, high), triệt tiêu lỗi "không hỗ trợ" khi chuyển mức
602
+ // =========================================================================
603
+ (async () => {
604
+ let PiAiAdapterClass = null;
605
+ try {
606
+ const mod = await import('/usr/local/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai/dsh-llm-pi-ai/lib/index.js');
607
+ PiAiAdapterClass = mod.PiAiAdapter;
608
+ } catch {
609
+ if (ctx.llm?.adapters) {
610
+ for (const reg of ctx.llm.adapters.values()) {
611
+ if (reg?.adapter?.constructor?.name === 'PiAiAdapter') {
612
+ PiAiAdapterClass = reg.adapter.constructor;
613
+ break;
614
+ }
615
+ }
616
+ }
617
+ }
618
+
619
+ if (PiAiAdapterClass && typeof PiAiAdapterClass.prototype?.modelOf === 'function') {
620
+ const originalModelOf = PiAiAdapterClass.prototype.modelOf;
621
+ if (!originalModelOf.__dshEnhanced) {
622
+ const patched = function (snapshot, provider, model) {
623
+ const resolved = originalModelOf.call(this, snapshot, provider, model);
624
+ if (resolved) {
625
+ patchResolvedModel(resolved, model);
626
+ }
627
+ return resolved;
628
+ };
629
+ patched.__dshEnhanced = true;
630
+ PiAiAdapterClass.prototype.modelOf = patched;
631
+ ctx.logger?.info('PiAiAdapter.prototype.modelOf successfully hooked with dynamic reasoning & vision enhancer');
632
+ }
633
+ }
634
+ })();
635
+
636
+ // =========================================================================
637
+ // 2. Live Hook vào ctx.llm.resolveCallConfig
638
+ // Bảo vệ lớp LlmRuntime: nếu có yêu cầu reasoningEffort hợp lệ, không bao giờ để ném UNSUPPORTED_REASONING_EFFORT
639
+ // =========================================================================
640
+ if (ctx.llm && typeof ctx.llm.resolveCallConfig === 'function') {
641
+ const originalResolveCallConfig = ctx.llm.resolveCallConfig.bind(ctx.llm);
642
+ ctx.llm.resolveCallConfig = async function (config, signal) {
643
+ try {
644
+ return await originalResolveCallConfig(config, signal);
645
+ } catch (err) {
646
+ if (err && err.code === 'UNSUPPORTED_REASONING_EFFORT') {
647
+ const cap = resolveCapabilities(config.model);
648
+ if (cap.hasThinking) {
649
+ ctx.logger?.info(`resolveCallConfig: gracefully accepted effort "${config.reasoningEffort}" for ${config.provider}/${config.model}`);
650
+ return {
651
+ ...config,
652
+ reasoningEffort: config.reasoningEffort,
653
+ };
654
+ }
655
+ }
656
+ throw err;
657
+ }
658
+ };
659
+ ctx.effect(() => () => {
660
+ ctx.llm.resolveCallConfig = originalResolveCallConfig;
661
+ });
662
+ }
663
+
664
+ // =========================================================================
665
+ // 3. Live Hook vào ctx.llm.discoverModels (Web UI Settings -> Models)
507
666
  // =========================================================================
508
667
  if (ctx.llm && typeof ctx.llm.discoverModels === 'function') {
509
668
  const originalDiscoverModels = ctx.llm.discoverModels.bind(ctx.llm);
@@ -527,7 +686,7 @@ export function apply(ctx) {
527
686
  }
528
687
 
529
688
  // =========================================================================
530
- // 2. Live Hook vào ctx.llm.resolveModelInfo (Chat / Agent Execution)
689
+ // 4. Live Hook vào ctx.llm.resolveModelInfo
531
690
  // =========================================================================
532
691
  if (ctx.llm && typeof ctx.llm.resolveModelInfo === 'function') {
533
692
  const originalResolveModelInfo = ctx.llm.resolveModelInfo.bind(ctx.llm);
@@ -538,13 +697,12 @@ export function apply(ctx) {
538
697
  const cap = resolveCapabilities(model);
539
698
 
540
699
  if (cap.hasThinking && !info.reasoning) {
700
+ const efforts = cap.modelEntry.reasoningEfforts || {};
541
701
  info.reasoning = {
542
- efforts: Object.keys(cap.modelEntry.reasoningEfforts)
543
- .filter((lvl) => lvl !== 'off')
544
- .map((lvl) => ({
545
- id: lvl,
546
- name: lvl.charAt(0).toUpperCase() + lvl.slice(1),
547
- })),
702
+ efforts: Object.keys(efforts).map((lvl) => ({
703
+ id: lvl,
704
+ name: lvl.charAt(0).toUpperCase() + lvl.slice(1),
705
+ })),
548
706
  defaultEffort: 'medium',
549
707
  };
550
708
  }
@@ -562,8 +720,13 @@ export function apply(ctx) {
562
720
  });
563
721
  }
564
722
 
723
+ // Tự động nâng cấp cấu hình trong llm-pi-ai sau khi host khởi tạo
724
+ setTimeout(() => {
725
+ autoEnhanceStoredProfiles(ctx);
726
+ }, 1000);
727
+
565
728
  // =========================================================================
566
- // 3. Hàm lưu cấu hình vào DSH giữ nguyên compat tùy biến (preserveCompat)
729
+ // 5. Hàm lưu cấu hình vào DSH giữ nguyên compat tùy biến (preserveCompat)
567
730
  // =========================================================================
568
731
  async function updateDshProviderConfig(providerId, providerData) {
569
732
  if (!ctx.configEditor) {
@@ -581,7 +744,6 @@ export function apply(ctx) {
581
744
  if (!next.providers) next.providers = {};
582
745
  const existing = next.providers[providerId] || {};
583
746
 
584
- // Giữ lại compat tùy biến của từng model cũ nếu có
585
747
  const existingCompat = new Map();
586
748
  for (const m of existing.models || []) {
587
749
  if (m && m.id && m.compat) existingCompat.set(m.id, m.compat);
@@ -608,7 +770,7 @@ export function apply(ctx) {
608
770
  }
609
771
 
610
772
  // =========================================================================
611
- // 4. Tool: discover_custom_models
773
+ // 6. Tool: discover_custom_models
612
774
  // =========================================================================
613
775
  if (ctx.tools && typeof ctx.tools.register === 'function') {
614
776
  ctx.tools.register({
@@ -690,7 +852,6 @@ export function apply(ctx) {
690
852
  api,
691
853
  });
692
854
 
693
- // Lọc bỏ endpoint thuần sinh ảnh (Flux/Midjourney/DALL-E) khỏi chat catalog
694
855
  if (cap.isImageEndpoint) continue;
695
856
 
696
857
  if (cap.hasVision) visionCount++;
@@ -745,7 +906,7 @@ export function apply(ctx) {
745
906
  });
746
907
 
747
908
  // =========================================================================
748
- // 5. Tool: enhance_existing_providers
909
+ // 7. Tool: enhance_existing_providers
749
910
  // =========================================================================
750
911
  ctx.tools.register({
751
912
  name: 'enhance_existing_providers',
@@ -815,10 +976,10 @@ export function apply(ctx) {
815
976
  maxTokens: Math.min(m.maxTokens || cap.modelEntry.maxTokens, m.contextWindow || cap.modelEntry.contextWindow),
816
977
  };
817
978
 
818
- if (cap.hasThinking && !merged.reasoningEfforts) {
979
+ if (cap.hasThinking && (!merged.reasoningEfforts || Object.keys(merged.reasoningEfforts).length <= 2)) {
819
980
  merged.reasoningEfforts = cap.modelEntry.reasoningEfforts;
820
981
  if (p.api === 'openai-completions') {
821
- const s = m.id.toLowerCase();
982
+ const s = (m.id || '').toLowerCase();
822
983
  const isDeepSeek = s.includes('deepseek') || s.includes('r1');
823
984
  merged.compat = {
824
985
  ...merged.compat,
@@ -854,7 +1015,7 @@ export function apply(ctx) {
854
1015
  }
855
1016
 
856
1017
  // =========================================================================
857
- // 6. Slash Commands: /discover-models và /enhance-models
1018
+ // 8. Slash Commands: /discover-models và /enhance-models
858
1019
  // =========================================================================
859
1020
  if (ctx.commands && typeof ctx.commands.register === 'function') {
860
1021
  ctx.commands.register({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-custom-api",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "DeepSeek Harness plugin: Auto-discovery, Vision, Reasoning/Thinking Effort & Tool Calls for Custom Model APIs",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",