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.
- package/lib/index.js +196 -35
- 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:
|
|
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
|
-
*
|
|
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
|
-
*
|
|
56
|
-
*
|
|
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.
|
|
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
|
-
//
|
|
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(
|
|
543
|
-
|
|
544
|
-
.
|
|
545
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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