dsh-custom-api 1.0.1 → 1.0.3

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 +224 -41
  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({
@@ -87,10 +82,13 @@ const GEMINI_LEVELS = Object.freeze({
87
82
  });
88
83
 
89
84
  function geminiEfforts(id) {
90
- const norm = String(id).toLowerCase();
91
- const documented = GEMINI_LEVELS[norm] || ['low', 'medium', 'high'];
92
- const efforts = {};
85
+ const norm = normalizeModelId(id);
86
+ const tail = String(id).toLowerCase().split('/').pop() || '';
87
+ const documented = GEMINI_LEVELS[tail] || GEMINI_LEVELS[norm] || ['low', 'medium', 'high'];
88
+ const efforts = { off: null };
93
89
  for (const level of documented) efforts[level] = level;
90
+ if (!efforts.medium) efforts.medium = efforts.high || 'medium';
91
+ if (!efforts.low) efforts.low = efforts.medium || 'low';
94
92
  return efforts;
95
93
  }
96
94
 
@@ -149,6 +147,16 @@ function buildModelsDevIndex(data) {
149
147
  isLoaded = true;
150
148
  }
151
149
 
150
+ // Synchronously load cache on module import if exists
151
+ try {
152
+ if (fs.existsSync(CACHE_PATH)) {
153
+ const content = fs.readFileSync(CACHE_PATH, 'utf8');
154
+ buildModelsDevIndex(JSON.parse(content));
155
+ }
156
+ } catch {
157
+ // Ignore sync read error, will be retried in initModelsDevDatabase
158
+ }
159
+
152
160
  async function initModelsDevDatabase(logger) {
153
161
  try {
154
162
  if (fs.existsSync(CACHE_PATH)) {
@@ -332,11 +340,22 @@ function determineReasoningEfforts(id, model, dev, forceThinking) {
332
340
  let isThinking = forceThinking;
333
341
  if (!isThinking) {
334
342
  if (typeof model?.supports_thinking === 'boolean') isThinking = model.supports_thinking;
343
+ else if (model?.reasoningEfforts && typeof model.reasoningEfforts === 'object') isThinking = true;
335
344
  else if (dev?.reasoning === true) isThinking = true;
336
- else if (THINKING_MODEL_ID.test(String(id))) isThinking = true;
337
345
  else {
338
- const desc = typeof model?.description === 'string' ? model.description : '';
339
- isThinking = THINKING_DESCRIPTION.test(desc);
346
+ const s = String(id).toLowerCase();
347
+ isThinking = (
348
+ s.includes('r1') || s.includes('o1') || s.includes('o3') || s.includes('o4') ||
349
+ s.includes('reason') || s.includes('think') || s.includes('thought') ||
350
+ s.includes('qwq') || s.includes('qvq') ||
351
+ s.includes('gemini-2.5') || s.includes('gemini-3') || s.includes('gemini-4') ||
352
+ (s.includes('claude') && (s.includes('3-7') || s.includes('3.7') || s.includes('sonnet') || s.includes('opus') || s.includes('4') || s.includes('5'))) ||
353
+ s.includes('gpt-5') || s.includes('gpt-6') ||
354
+ s.includes('grok-4') || s.includes('grok-3') ||
355
+ s.includes('glm-5') || s.includes('deepseek-v4') ||
356
+ s.includes('qwen-3') || s.includes('qwen3') ||
357
+ THINKING_DESCRIPTION.test(typeof model?.description === 'string' ? model.description : '')
358
+ );
340
359
  }
341
360
  }
342
361
 
@@ -392,7 +411,7 @@ function isImageGenerationEndpoint(id, dev) {
392
411
  /**
393
412
  * Giải quyết và gán năng lực hoàn chỉnh cho một model
394
413
  */
395
- function resolveCapabilities(rawModel, opts = {}) {
414
+ export function resolveCapabilities(rawModel, opts = {}) {
396
415
  const id = typeof rawModel === 'string' ? rawModel : rawModel.id;
397
416
  const dev = lookupModelsDev(id);
398
417
  const apiMeta = extractFromApiMetadata(rawModel);
@@ -498,12 +517,174 @@ async function fetchModelsFromApi(baseURL, apiKey, apiProtocol = 'openai-complet
498
517
  throw new Error(`Không thể lấy danh sách model từ ${baseURL}: ${lastError?.message || 'Không tìm thấy models'}`);
499
518
  }
500
519
 
520
+ /**
521
+ * Patch một instance model đã được PiAi resolve để luôn hỗ trợ thinking effort và vision
522
+ */
523
+ function patchResolvedModel(resolved, modelId) {
524
+ if (!resolved || typeof resolved !== 'object') return;
525
+ const cap = resolveCapabilities(modelId || resolved.id);
526
+
527
+ if (cap.hasThinking) {
528
+ resolved.reasoning = true;
529
+ if (!resolved.thinkingLevelMap || typeof resolved.thinkingLevelMap !== 'object') {
530
+ resolved.thinkingLevelMap = {};
531
+ }
532
+ const efforts = cap.modelEntry.reasoningEfforts || {};
533
+ for (const [lvl, wire] of Object.entries(efforts)) {
534
+ if (wire !== null && wire !== undefined) {
535
+ resolved.thinkingLevelMap[lvl] = wire;
536
+ }
537
+ }
538
+
539
+ // Đảm bảo 'off' luôn được hỗ trợ (không bị null)
540
+ if (resolved.thinkingLevelMap.off === null) {
541
+ delete resolved.thinkingLevelMap.off;
542
+ }
543
+
544
+ // Đảm bảo các nấc tiêu chuẩn (low, medium, high) không bị null
545
+ if (resolved.thinkingLevelMap.high === null || resolved.thinkingLevelMap.high === undefined) {
546
+ resolved.thinkingLevelMap.high = 'high';
547
+ }
548
+ if (resolved.thinkingLevelMap.medium === null || resolved.thinkingLevelMap.medium === undefined) {
549
+ resolved.thinkingLevelMap.medium = resolved.thinkingLevelMap.high || 'medium';
550
+ }
551
+ if (resolved.thinkingLevelMap.low === null || resolved.thinkingLevelMap.low === undefined) {
552
+ resolved.thinkingLevelMap.low = resolved.thinkingLevelMap.medium || 'low';
553
+ }
554
+
555
+ if (!resolved.compat || typeof resolved.compat !== 'object') {
556
+ resolved.compat = {};
557
+ }
558
+ resolved.compat.supportsReasoningEffort = true;
559
+ }
560
+
561
+ if (cap.hasVision && Array.isArray(resolved.input) && !resolved.input.includes('image')) {
562
+ resolved.input.push('image');
563
+ }
564
+ }
565
+
566
+ /**
567
+ * Tự động quét và nâng cấp tất cả provider hiện có trong llm-pi-ai trên startup
568
+ */
569
+ async function autoEnhanceStoredProfiles(ctx) {
570
+ if (!ctx.configEditor) return;
571
+ try {
572
+ const entries = ctx.configEditor.entries();
573
+ const entry = entries.find(
574
+ (e) => e.options?.id === 'llm-pi-ai' || e.patchId === 'llm-pi-ai' || e.id === 'include:llm-pi-ai'
575
+ );
576
+ if (!entry) return;
577
+
578
+ await ctx.configEditor.edit(entry, (current = {}) => {
579
+ const next = structuredClone(current);
580
+ if (!next.providers) return next;
581
+
582
+ let hasChanges = false;
583
+ for (const [pid, p] of Object.entries(next.providers)) {
584
+ if (!p || !Array.isArray(p.models)) continue;
585
+ for (const m of p.models) {
586
+ if (!m || typeof m !== 'object') continue;
587
+ const cap = resolveCapabilities(m, { api: p.api });
588
+
589
+ if (cap.hasVision && (!m.input || !m.input.includes('image'))) {
590
+ m.input = ['text', 'image'];
591
+ hasChanges = true;
592
+ }
593
+
594
+ if (cap.hasThinking && (m.reasoningEfforts === undefined || m.reasoningEfforts === false)) {
595
+ m.reasoningEfforts = cap.modelEntry.reasoningEfforts;
596
+ if (p.api === 'openai-completions') {
597
+ const s = (m.id || '').toLowerCase();
598
+ const isDeepSeek = s.includes('deepseek') || s.includes('r1');
599
+ m.compat = {
600
+ ...m.compat,
601
+ supportsReasoningEffort: true,
602
+ ...(isDeepSeek ? { thinkingFormat: 'deepseek' } : {}),
603
+ };
604
+ }
605
+ hasChanges = true;
606
+ }
607
+ }
608
+ }
609
+ return hasChanges ? next : current;
610
+ });
611
+ } catch (err) {
612
+ ctx.logger?.warn(`autoEnhanceStoredProfiles error: ${err.message}`);
613
+ }
614
+ }
615
+
501
616
  export function apply(ctx) {
502
617
  initModelsDevDatabase(ctx.logger);
503
618
  ctx.logger?.info('Custom Model Discovery Plugin (API Metadata + models.dev + Vendor Reasoning Ladders) active');
504
619
 
505
620
  // =========================================================================
506
- // 1. Live Hook vào ctx.llm.discoverModels (Web UI Settings -> Models)
621
+ // 1. Hook trực tiếp PiAiAdapter.prototype.modelOf
622
+ // Đảm bảo ở cấp Engine Adapter, model luôn có reasoning=true và thinkingLevelMap
623
+ // đầy đủ các mức (off, low, medium, high), triệt tiêu lỗi "không hỗ trợ" khi chuyển mức
624
+ // =========================================================================
625
+ (async () => {
626
+ let PiAiAdapterClass = null;
627
+ try {
628
+ const mod = await import('/usr/local/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai/dsh-llm-pi-ai/lib/index.js');
629
+ PiAiAdapterClass = mod.PiAiAdapter;
630
+ } catch {
631
+ if (ctx.llm?.adapters) {
632
+ for (const reg of ctx.llm.adapters.values()) {
633
+ if (reg?.adapter?.constructor?.name === 'PiAiAdapter') {
634
+ PiAiAdapterClass = reg.adapter.constructor;
635
+ break;
636
+ }
637
+ }
638
+ }
639
+ }
640
+
641
+ if (PiAiAdapterClass && typeof PiAiAdapterClass.prototype?.modelOf === 'function') {
642
+ const originalModelOf = PiAiAdapterClass.prototype.modelOf;
643
+ if (!originalModelOf.__dshEnhanced) {
644
+ const patched = function (snapshot, provider, model) {
645
+ const resolved = originalModelOf.call(this, snapshot, provider, model);
646
+ if (resolved) {
647
+ patchResolvedModel(resolved, model);
648
+ }
649
+ return resolved;
650
+ };
651
+ patched.__dshEnhanced = true;
652
+ PiAiAdapterClass.prototype.modelOf = patched;
653
+ ctx.logger?.info('PiAiAdapter.prototype.modelOf successfully hooked with dynamic reasoning & vision enhancer');
654
+ }
655
+ }
656
+ })();
657
+
658
+ // =========================================================================
659
+ // 2. Live Hook vào ctx.llm.resolveCallConfig
660
+ // 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
661
+ // =========================================================================
662
+ if (ctx.llm && typeof ctx.llm.resolveCallConfig === 'function') {
663
+ const originalResolveCallConfig = ctx.llm.resolveCallConfig.bind(ctx.llm);
664
+ ctx.llm.resolveCallConfig = async function (config, signal) {
665
+ try {
666
+ return await originalResolveCallConfig(config, signal);
667
+ } catch (err) {
668
+ if (err && err.code === 'UNSUPPORTED_REASONING_EFFORT') {
669
+ const cap = resolveCapabilities(config.model);
670
+ if (cap.hasThinking) {
671
+ ctx.logger?.info(`resolveCallConfig: gracefully accepted effort "${config.reasoningEffort}" for ${config.provider}/${config.model}`);
672
+ return {
673
+ ...config,
674
+ reasoningEffort: config.reasoningEffort,
675
+ };
676
+ }
677
+ }
678
+ throw err;
679
+ }
680
+ };
681
+ ctx.effect(() => () => {
682
+ ctx.llm.resolveCallConfig = originalResolveCallConfig;
683
+ });
684
+ }
685
+
686
+ // =========================================================================
687
+ // 3. Live Hook vào ctx.llm.discoverModels (Web UI Settings -> Models)
507
688
  // =========================================================================
508
689
  if (ctx.llm && typeof ctx.llm.discoverModels === 'function') {
509
690
  const originalDiscoverModels = ctx.llm.discoverModels.bind(ctx.llm);
@@ -527,7 +708,7 @@ export function apply(ctx) {
527
708
  }
528
709
 
529
710
  // =========================================================================
530
- // 2. Live Hook vào ctx.llm.resolveModelInfo (Chat / Agent Execution)
711
+ // 4. Live Hook vào ctx.llm.resolveModelInfo
531
712
  // =========================================================================
532
713
  if (ctx.llm && typeof ctx.llm.resolveModelInfo === 'function') {
533
714
  const originalResolveModelInfo = ctx.llm.resolveModelInfo.bind(ctx.llm);
@@ -538,13 +719,12 @@ export function apply(ctx) {
538
719
  const cap = resolveCapabilities(model);
539
720
 
540
721
  if (cap.hasThinking && !info.reasoning) {
722
+ const efforts = cap.modelEntry.reasoningEfforts || {};
541
723
  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
- })),
724
+ efforts: Object.keys(efforts).map((lvl) => ({
725
+ id: lvl,
726
+ name: lvl.charAt(0).toUpperCase() + lvl.slice(1),
727
+ })),
548
728
  defaultEffort: 'medium',
549
729
  };
550
730
  }
@@ -562,8 +742,13 @@ export function apply(ctx) {
562
742
  });
563
743
  }
564
744
 
745
+ // Tự động nâng cấp cấu hình trong llm-pi-ai sau khi host khởi tạo
746
+ setTimeout(() => {
747
+ autoEnhanceStoredProfiles(ctx);
748
+ }, 1000);
749
+
565
750
  // =========================================================================
566
- // 3. Hàm lưu cấu hình vào DSH giữ nguyên compat tùy biến (preserveCompat)
751
+ // 5. Hàm lưu cấu hình vào DSH giữ nguyên compat tùy biến (preserveCompat)
567
752
  // =========================================================================
568
753
  async function updateDshProviderConfig(providerId, providerData) {
569
754
  if (!ctx.configEditor) {
@@ -581,7 +766,6 @@ export function apply(ctx) {
581
766
  if (!next.providers) next.providers = {};
582
767
  const existing = next.providers[providerId] || {};
583
768
 
584
- // Giữ lại compat tùy biến của từng model cũ nếu có
585
769
  const existingCompat = new Map();
586
770
  for (const m of existing.models || []) {
587
771
  if (m && m.id && m.compat) existingCompat.set(m.id, m.compat);
@@ -608,7 +792,7 @@ export function apply(ctx) {
608
792
  }
609
793
 
610
794
  // =========================================================================
611
- // 4. Tool: discover_custom_models
795
+ // 6. Tool: discover_custom_models
612
796
  // =========================================================================
613
797
  if (ctx.tools && typeof ctx.tools.register === 'function') {
614
798
  ctx.tools.register({
@@ -690,7 +874,6 @@ export function apply(ctx) {
690
874
  api,
691
875
  });
692
876
 
693
- // Lọc bỏ endpoint thuần sinh ảnh (Flux/Midjourney/DALL-E) khỏi chat catalog
694
877
  if (cap.isImageEndpoint) continue;
695
878
 
696
879
  if (cap.hasVision) visionCount++;
@@ -745,7 +928,7 @@ export function apply(ctx) {
745
928
  });
746
929
 
747
930
  // =========================================================================
748
- // 5. Tool: enhance_existing_providers
931
+ // 7. Tool: enhance_existing_providers
749
932
  // =========================================================================
750
933
  ctx.tools.register({
751
934
  name: 'enhance_existing_providers',
@@ -815,10 +998,10 @@ export function apply(ctx) {
815
998
  maxTokens: Math.min(m.maxTokens || cap.modelEntry.maxTokens, m.contextWindow || cap.modelEntry.contextWindow),
816
999
  };
817
1000
 
818
- if (cap.hasThinking && !merged.reasoningEfforts) {
1001
+ if (cap.hasThinking) {
819
1002
  merged.reasoningEfforts = cap.modelEntry.reasoningEfforts;
820
1003
  if (p.api === 'openai-completions') {
821
- const s = m.id.toLowerCase();
1004
+ const s = (m.id || '').toLowerCase();
822
1005
  const isDeepSeek = s.includes('deepseek') || s.includes('r1');
823
1006
  merged.compat = {
824
1007
  ...merged.compat,
@@ -854,7 +1037,7 @@ export function apply(ctx) {
854
1037
  }
855
1038
 
856
1039
  // =========================================================================
857
- // 6. Slash Commands: /discover-models và /enhance-models
1040
+ // 8. Slash Commands: /discover-models và /enhance-models
858
1041
  // =========================================================================
859
1042
  if (ctx.commands && typeof ctx.commands.register === 'function') {
860
1043
  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.3",
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",