@anionex/dsh-vision-toolkit 0.1.44 → 0.1.46
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/CHANGELOG.md +527 -0
- package/README.i18n.yaml +2 -2
- package/README.md +27 -4
- package/README.zh.md +27 -4
- package/docs/requirements-traceability/README.i18n.yaml +2 -2
- package/docs/requirements-traceability/README.md +4 -4
- package/docs/requirements-traceability/README.zh.md +4 -4
- package/lib/client.js +20 -1
- package/lib/client.js.map +1 -1
- package/lib/config.js +147 -5
- package/lib/config.js.map +1 -1
- package/lib/evidence-cache.js +3 -0
- package/lib/evidence-cache.js.map +1 -1
- package/lib/image-input-variants.js +100 -6
- package/lib/image-input-variants.js.map +1 -1
- package/lib/index.js +4 -7
- package/lib/index.js.map +1 -1
- package/lib/runtime.js +40 -5
- package/lib/runtime.js.map +1 -1
- package/lib/settings-compat.js +38 -0
- package/lib/settings-compat.js.map +1 -0
- package/lib/types/client/index.d.ts +7 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/config.d.ts +19 -4
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/evidence-cache.d.ts.map +1 -1
- package/lib/types/image-input-variants.d.ts +8 -0
- package/lib/types/image-input-variants.d.ts.map +1 -1
- package/lib/types/index.d.ts +2 -2
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/runtime.d.ts +4 -0
- package/lib/types/runtime.d.ts.map +1 -1
- package/lib/types/settings-compat.d.ts +12 -0
- package/lib/types/settings-compat.d.ts.map +1 -0
- package/lib/types/upstream.d.ts +4 -1
- package/lib/types/upstream.d.ts.map +1 -1
- package/lib/upstream.js +88 -29
- package/lib/upstream.js.map +1 -1
- package/package.json +24 -22
- package/src/client/index.tsx +29 -3
- package/src/config.ts +171 -9
- package/src/evidence-cache.ts +3 -0
- package/src/image-input-variants.ts +110 -6
- package/src/index.ts +4 -8
- package/src/runtime.ts +45 -5
- package/src/settings-compat.ts +56 -0
- package/src/upstream.ts +88 -25
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anionex/dsh-vision-toolkit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.46",
|
|
4
4
|
"description": "DeepSeek Harness-native integration for agent-vision-toolkit: image Q&A, OCR, grounding, UI restoration, pixel diff, Artifacts, and Web UI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"runtime",
|
|
56
56
|
"vendor",
|
|
57
57
|
"cordis.patch.yml",
|
|
58
|
+
"CHANGELOG.md",
|
|
58
59
|
"README.md",
|
|
59
60
|
"README.zh.md",
|
|
60
61
|
"LICENSE"
|
|
@@ -96,7 +97,7 @@
|
|
|
96
97
|
"upstreamSkillCommit": "77c24ad5b5d7a123119862893129f939307f1d3f"
|
|
97
98
|
},
|
|
98
99
|
"compatibility": {
|
|
99
|
-
"dsh": ">=0.1.0-rc.8 <0.2.0",
|
|
100
|
+
"dsh": ">=0.1.0-rc.8 <0.2.0 || 0.2.0-rc.2",
|
|
100
101
|
"dshReleases": {
|
|
101
102
|
"0.1.0-rc.8": "compatible",
|
|
102
103
|
"0.1.1-rc.1": "compatible",
|
|
@@ -109,7 +110,8 @@
|
|
|
109
110
|
"0.1.3-alpha.2": "compatible",
|
|
110
111
|
"0.1.5-alpha.1": "compatible",
|
|
111
112
|
"0.1.5-alpha.2": "compatible",
|
|
112
|
-
"0.1.5-rc.1": "compatible"
|
|
113
|
+
"0.1.5-rc.1": "compatible",
|
|
114
|
+
"0.2.0-rc.2": "compatible"
|
|
113
115
|
},
|
|
114
116
|
"profiles": [
|
|
115
117
|
"web",
|
|
@@ -124,25 +126,25 @@
|
|
|
124
126
|
"zod": "4.4.3"
|
|
125
127
|
},
|
|
126
128
|
"peerDependencies": {
|
|
127
|
-
"@deepseek-ai/dsh-agent": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
128
|
-
"@deepseek-ai/dsh-api-remotes": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
129
|
-
"@deepseek-ai/dsh-attachment": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
130
|
-
"@deepseek-ai/dsh-client-locale": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
131
|
-
"@deepseek-ai/dsh-client-ui-conversation": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
132
|
-
"@deepseek-ai/dsh-client-ui-input-trigger": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
133
|
-
"@deepseek-ai/dsh-client-ui-primitives": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
134
|
-
"@deepseek-ai/dsh-client-ui-settings": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
135
|
-
"@deepseek-ai/dsh-client-ui-slots": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
136
|
-
"@deepseek-ai/dsh-client-ui-tool": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
137
|
-
"@deepseek-ai/dsh-credentials": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
138
|
-
"@deepseek-ai/dsh-host-webserver": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
139
|
-
"@deepseek-ai/dsh-llm": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
140
|
-
"@deepseek-ai/dsh-session": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
141
|
-
"@deepseek-ai/dsh-settings": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
142
|
-
"@deepseek-ai/dsh-skill": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
143
|
-
"@deepseek-ai/dsh-storage-domain": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
144
|
-
"@deepseek-ai/dsh-subprocess": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
145
|
-
"@deepseek-ai/dsh-tools": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1",
|
|
129
|
+
"@deepseek-ai/dsh-agent": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1 || 0.2.0-rc.2",
|
|
130
|
+
"@deepseek-ai/dsh-api-remotes": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1 || 0.2.0-rc.2",
|
|
131
|
+
"@deepseek-ai/dsh-attachment": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1 || 0.2.0-rc.2",
|
|
132
|
+
"@deepseek-ai/dsh-client-locale": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1 || 0.2.0-rc.2",
|
|
133
|
+
"@deepseek-ai/dsh-client-ui-conversation": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1 || 0.2.0-rc.2",
|
|
134
|
+
"@deepseek-ai/dsh-client-ui-input-trigger": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1 || 0.2.0-rc.2",
|
|
135
|
+
"@deepseek-ai/dsh-client-ui-primitives": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1 || 0.2.0-rc.2",
|
|
136
|
+
"@deepseek-ai/dsh-client-ui-settings": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1 || 0.2.0-rc.2",
|
|
137
|
+
"@deepseek-ai/dsh-client-ui-slots": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1 || 0.2.0-rc.2",
|
|
138
|
+
"@deepseek-ai/dsh-client-ui-tool": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1 || 0.2.0-rc.2",
|
|
139
|
+
"@deepseek-ai/dsh-credentials": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1 || 0.2.0-rc.2",
|
|
140
|
+
"@deepseek-ai/dsh-host-webserver": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1 || 0.2.0-rc.2",
|
|
141
|
+
"@deepseek-ai/dsh-llm": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1 || 0.2.0-rc.2",
|
|
142
|
+
"@deepseek-ai/dsh-session": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1 || 0.2.0-rc.2",
|
|
143
|
+
"@deepseek-ai/dsh-settings": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1 || 0.2.0-rc.2",
|
|
144
|
+
"@deepseek-ai/dsh-skill": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1 || 0.2.0-rc.2",
|
|
145
|
+
"@deepseek-ai/dsh-storage-domain": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1 || 0.2.0-rc.2",
|
|
146
|
+
"@deepseek-ai/dsh-subprocess": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1 || 0.2.0-rc.2",
|
|
147
|
+
"@deepseek-ai/dsh-tools": ">=0.1.0-rc.8 <0.2.0 || ^0.1.1-rc.1 || ^0.1.2-alpha.1 || ^0.1.5-rc.1 || 0.2.0-rc.2",
|
|
146
148
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
147
149
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
148
150
|
"react": "^18.2.0"
|
package/src/client/index.tsx
CHANGED
|
@@ -43,6 +43,8 @@ const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKi
|
|
|
43
43
|
const BUILT_IN_FREE_VISION_BASE_URL = 'https://vision.anionex.me/v1'
|
|
44
44
|
const BUILT_IN_FREE_VISION_CREDENTIAL = 'ANIONEX_FREE_VISION'
|
|
45
45
|
const BUILT_IN_FREE_VISION_MODEL = 'gemini-3.7-flash'
|
|
46
|
+
const MAX_REASONING_EFFORT_LENGTH = 64
|
|
47
|
+
const REASONING_EFFORT_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/u
|
|
46
48
|
const AIHUBMIX_TUTORIAL_URL_EN = 'https://github.com/Anionex/dsh-vision-toolkit/blob/main/docs/aihubmix-gemini-vision.md'
|
|
47
49
|
const AIHUBMIX_TUTORIAL_URL_ZH = 'https://github.com/Anionex/dsh-vision-toolkit/blob/main/docs/aihubmix-gemini-vision.zh.md'
|
|
48
50
|
|
|
@@ -66,6 +68,9 @@ const en = {
|
|
|
66
68
|
credentialHint: 'The built-in free provider needs no user key. For a custom provider, this is the DSH credential reference used to store its key.',
|
|
67
69
|
model: 'Model',
|
|
68
70
|
protocol: 'API protocol',
|
|
71
|
+
reasoningEffort: 'Vision service reasoning effort',
|
|
72
|
+
reasoningEffortHint: 'Optional. Common values: none, minimal, low, medium, high, xhigh. Supported values and billing depend on the model or proxy; higher effort may increase tokens, latency, and cost. Requests send store:false, but you should still verify the provider\'s data-retention policy.',
|
|
73
|
+
reasoningEffortInvalid: 'Vision service reasoning effort must be at most 64 ASCII letters, digits, dots, underscores, or hyphens.',
|
|
69
74
|
anthropicThinking: 'Anthropic thinking',
|
|
70
75
|
anthropicThinkingHint: 'omit has the broadest compatibility. Use disabled or adaptive only when the selected model documents that mode; restore omit first after HTTP 400.',
|
|
71
76
|
userAgent: 'User-Agent',
|
|
@@ -257,6 +262,9 @@ const zh: Record<LocaleKey, string> = {
|
|
|
257
262
|
credentialHint: '内置免费视觉服务无需用户密钥;切换到自定义服务时,此处是保存其密钥的 DSH 凭据名称。',
|
|
258
263
|
model: '模型名称',
|
|
259
264
|
protocol: 'API 协议',
|
|
265
|
+
reasoningEffort: '视觉服务推理强度',
|
|
266
|
+
reasoningEffortHint: '可选。常见值:none、minimal、low、medium、high、xhigh。实际支持值和计费由模型或代理决定;较高强度可能增加 token、延迟和费用。请求会发送 store:false,但仍应核查服务商的数据保留政策。',
|
|
267
|
+
reasoningEffortInvalid: '视觉服务推理强度最多 64 位,只能包含 ASCII 字母、数字、点、下划线或连字符。',
|
|
260
268
|
anthropicThinking: 'Anthropic thinking',
|
|
261
269
|
anthropicThinkingHint: 'omit 兼容性最好。仅当所选模型明确支持时使用 disabled 或 adaptive;遇到 HTTP 400 时先恢复 omit。',
|
|
262
270
|
userAgent: 'User-Agent',
|
|
@@ -486,9 +494,12 @@ interface SettingsValue {
|
|
|
486
494
|
baseUrl?: string
|
|
487
495
|
credential?: string
|
|
488
496
|
model?: string
|
|
489
|
-
protocol?: 'openai' | 'anthropic'
|
|
497
|
+
protocol?: 'openai' | 'responses' | 'anthropic'
|
|
498
|
+
reasoningEffort?: string
|
|
490
499
|
anthropicThinking?: 'omit' | 'disabled' | 'adaptive'
|
|
491
500
|
userAgent?: string
|
|
501
|
+
headers?: Record<string, string>
|
|
502
|
+
sessionHeaders?: string[]
|
|
492
503
|
}
|
|
493
504
|
language?: 'zh' | 'en'
|
|
494
505
|
timeoutMs?: number
|
|
@@ -1094,9 +1105,12 @@ interface Draft {
|
|
|
1094
1105
|
baseUrl: string
|
|
1095
1106
|
credential: string
|
|
1096
1107
|
model: string
|
|
1097
|
-
protocol: 'openai' | 'anthropic'
|
|
1108
|
+
protocol: 'openai' | 'responses' | 'anthropic'
|
|
1109
|
+
reasoningEffort: string
|
|
1098
1110
|
anthropicThinking: 'omit' | 'disabled' | 'adaptive'
|
|
1099
1111
|
userAgent: string
|
|
1112
|
+
providerHeaders: Record<string, string>
|
|
1113
|
+
providerSessionHeaders: string[]
|
|
1100
1114
|
language: 'zh' | 'en'
|
|
1101
1115
|
timeoutMs: string
|
|
1102
1116
|
maxImageBytes: string
|
|
@@ -1119,8 +1133,11 @@ function draftOf(value: SettingsValue): Draft {
|
|
|
1119
1133
|
credential: value.provider?.credential ?? BUILT_IN_FREE_VISION_CREDENTIAL,
|
|
1120
1134
|
model: value.provider?.model ?? BUILT_IN_FREE_VISION_MODEL,
|
|
1121
1135
|
protocol: value.provider?.protocol ?? 'openai',
|
|
1136
|
+
reasoningEffort: value.provider?.protocol === 'responses' ? value.provider.reasoningEffort ?? '' : '',
|
|
1122
1137
|
anthropicThinking: value.provider?.anthropicThinking ?? 'omit',
|
|
1123
1138
|
userAgent: value.provider?.userAgent ?? DEFAULT_USER_AGENT,
|
|
1139
|
+
providerHeaders: { ...(value.provider?.headers ?? {}) },
|
|
1140
|
+
providerSessionHeaders: [...(value.provider?.sessionHeaders ?? [])],
|
|
1124
1141
|
language: value.language ?? 'zh',
|
|
1125
1142
|
timeoutMs: String(value.timeoutMs ?? 30000),
|
|
1126
1143
|
maxImageBytes: String(value.maxImageBytes ?? 4194304),
|
|
@@ -1155,14 +1172,22 @@ function apiKeyFailure(value: string, t: Translate): string | undefined {
|
|
|
1155
1172
|
}
|
|
1156
1173
|
|
|
1157
1174
|
function valueOf(draft: Draft, t: Translate): SettingsValue {
|
|
1175
|
+
const reasoningEffort = draft.reasoningEffort.trim()
|
|
1176
|
+
if (draft.protocol === 'responses' && reasoningEffort.length > 0
|
|
1177
|
+
&& (reasoningEffort.length > MAX_REASONING_EFFORT_LENGTH || !REASONING_EFFORT_PATTERN.test(reasoningEffort))) {
|
|
1178
|
+
throw new Error(t('reasoningEffortInvalid'))
|
|
1179
|
+
}
|
|
1158
1180
|
return {
|
|
1159
1181
|
provider: {
|
|
1160
1182
|
baseUrl: draft.baseUrl.trim(),
|
|
1161
1183
|
credential: draft.credential.trim(),
|
|
1162
1184
|
model: draft.model.trim(),
|
|
1163
1185
|
protocol: draft.protocol,
|
|
1186
|
+
...(draft.protocol === 'responses' && reasoningEffort.length > 0 ? { reasoningEffort } : {}),
|
|
1164
1187
|
anthropicThinking: draft.anthropicThinking,
|
|
1165
1188
|
userAgent: draft.userAgent.trim(),
|
|
1189
|
+
...(Object.keys(draft.providerHeaders).length === 0 ? {} : { headers: { ...draft.providerHeaders } }),
|
|
1190
|
+
...(draft.providerSessionHeaders.length === 0 ? {} : { sessionHeaders: [...draft.providerSessionHeaders] }),
|
|
1166
1191
|
},
|
|
1167
1192
|
language: draft.language,
|
|
1168
1193
|
timeoutMs: positiveInteger(draft.timeoutMs, t('timeout'), t),
|
|
@@ -1426,7 +1451,8 @@ function LoadedSettings({ controller, t }: SettingsInjected) {
|
|
|
1426
1451
|
<section className="dvt-panel dvt-essential"><div className="dvt-panel-title"><div><h3>{t('provider')}</h3><p>{t('providerHint')}</p></div><span className={`dvt-badge ${snapshot.credential.configured ? 'ok' : 'error'}`}>{snapshot.credential.configured ? t('configured') : t('missing')}</span></div>
|
|
1427
1452
|
<p className="dvt-tutorial-link"><a href={aihubmixTutorialUrl} target="_blank" rel="noreferrer">{t('aihubmixTutorial')}</a></p>
|
|
1428
1453
|
<div className="dvt-form-grid">
|
|
1429
|
-
<Field label={t('protocol')}><select disabled={!snapshot.writable || busy} value={draft.protocol} onChange={(event) => { update('protocol', event.target.value as 'openai' | 'anthropic') }}><option value="openai">OpenAI Chat Completions</option><option value="anthropic">Anthropic Messages</option></select></Field>
|
|
1454
|
+
<Field label={t('protocol')}><select disabled={!snapshot.writable || busy} value={draft.protocol} onChange={(event) => { update('protocol', event.target.value as 'openai' | 'responses' | 'anthropic') }}><option value="openai">OpenAI Chat Completions</option><option value="responses">OpenAI Responses</option><option value="anthropic">Anthropic Messages</option></select></Field>
|
|
1455
|
+
{draft.protocol === 'responses' ? <Field label={t('reasoningEffort')} hint={t('reasoningEffortHint')}><Input aria-label={t('reasoningEffort')} disabled={!snapshot.writable || busy} placeholder="none / minimal / low / medium / high / xhigh" value={draft.reasoningEffort} onChange={(event) => { update('reasoningEffort', event.target.value); setDraftError(undefined) }} /></Field> : null}
|
|
1430
1456
|
<Field label={t('baseUrl')}><Input disabled={!snapshot.writable || busy} value={draft.baseUrl} onChange={(event) => { update('baseUrl', event.target.value) }} /></Field>
|
|
1431
1457
|
<Field label={t('model')}><Input disabled={!snapshot.writable || busy} value={draft.model} onChange={(event) => { update('model', event.target.value) }} /></Field>
|
|
1432
1458
|
<Field label={t('apiKey')} hint={keyLocked ? t('apiKeyLocked') : snapshot.credential.source === undefined ? t('apiKeyHint') : `${t('apiKeyHint')} ${t('sourceHint', { source: t('source'), value: credentialSource(snapshot.credential.source, t) })}`}><Input aria-label={t('apiKey')} type="password" autoComplete="new-password" disabled={busy || keyLocked} placeholder={snapshot.credential.configured ? t('apiKeyPlaceholderConfigured') : t('apiKeyPlaceholderMissing')} value={apiKey} onChange={(event) => { setApiKey(event.target.value); setDraftError(undefined) }} /></Field>
|
package/src/config.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import z from '@deepseek-ai/schemastery'
|
|
10
10
|
import type Schema from '@deepseek-ai/schemastery'
|
|
11
11
|
import { credentialRef, type CredentialRef } from '@deepseek-ai/dsh-credentials'
|
|
12
|
-
import
|
|
12
|
+
import SettingsService, { type SettingsNamespace } from '@deepseek-ai/dsh-settings'
|
|
13
13
|
import { VisionToolkitError } from './errors.ts'
|
|
14
14
|
import {
|
|
15
15
|
BUILT_IN_FREE_VISION_BASE_URL,
|
|
@@ -66,12 +66,18 @@ export interface VisionToolkitConfig {
|
|
|
66
66
|
credential?: string
|
|
67
67
|
/** Multimodal model name. */
|
|
68
68
|
model?: string
|
|
69
|
-
/** Vision request protocol: OpenAI Chat Completions or Anthropic Messages. */
|
|
70
|
-
protocol?: 'openai' | 'anthropic'
|
|
69
|
+
/** Vision request protocol: OpenAI Chat Completions, OpenAI Responses, or Anthropic Messages. */
|
|
70
|
+
protocol?: 'openai' | 'responses' | 'anthropic'
|
|
71
|
+
/** Optional provider-specific reasoning effort for OpenAI Responses requests. */
|
|
72
|
+
reasoningEffort?: string
|
|
71
73
|
/** Anthropic thinking field behavior; `omit` leaves model defaults untouched. */
|
|
72
74
|
anthropicThinking?: 'omit' | 'disabled' | 'adaptive'
|
|
73
75
|
/** Outbound User-Agent for provider requests and connection tests. */
|
|
74
76
|
userAgent?: string
|
|
77
|
+
/** Non-secret deployment metadata sent with provider requests. */
|
|
78
|
+
headers?: Record<string, string>
|
|
79
|
+
/** Header names whose values are derived from the current operation identity. */
|
|
80
|
+
sessionHeaders?: string[]
|
|
75
81
|
}
|
|
76
82
|
/** Vision output language (`zh` or `en`). */
|
|
77
83
|
language?: 'zh' | 'en'
|
|
@@ -134,14 +140,17 @@ export interface VisionToolkitConfig {
|
|
|
134
140
|
}
|
|
135
141
|
|
|
136
142
|
/** Configuration schema with the documented P0 defaults. */
|
|
137
|
-
export const
|
|
143
|
+
export const LegacyConfig: Schema<VisionToolkitConfig> = z.object({
|
|
138
144
|
provider: z.object({
|
|
139
145
|
baseUrl: z.string().default(BUILT_IN_FREE_VISION_BASE_URL),
|
|
140
146
|
credential: z.string().default(BUILT_IN_FREE_VISION_CREDENTIAL),
|
|
141
147
|
model: z.string().default(BUILT_IN_FREE_VISION_MODEL),
|
|
142
|
-
protocol: z.union(['openai', 'anthropic'] as const).default('openai'),
|
|
148
|
+
protocol: z.union(['openai', 'responses', 'anthropic'] as const).default('openai'),
|
|
149
|
+
reasoningEffort: z.string(),
|
|
143
150
|
anthropicThinking: z.union(['omit', 'disabled', 'adaptive'] as const).default('omit'),
|
|
144
151
|
userAgent: z.string().default(DEFAULT_VISION_USER_AGENT),
|
|
152
|
+
headers: z.dict(z.string()).default({}),
|
|
153
|
+
sessionHeaders: z.array(z.string()).default([]),
|
|
145
154
|
}),
|
|
146
155
|
language: z.union(['zh', 'en'] as const).default('zh'),
|
|
147
156
|
timeoutMs: z.number().default(30000),
|
|
@@ -164,15 +173,54 @@ export const Config: Schema<VisionToolkitConfig> = z.object({
|
|
|
164
173
|
}),
|
|
165
174
|
})
|
|
166
175
|
|
|
176
|
+
/** New Settings reads this metadata; older Schemastery has no .volatile() method. */
|
|
177
|
+
export const VolatileConfig: Schema<VisionToolkitConfig> = (() => {
|
|
178
|
+
const schema = new z(LegacyConfig.toJSON()) as Schema<VisionToolkitConfig>
|
|
179
|
+
const markFields = (node: Schema): void => {
|
|
180
|
+
if (node.type === 'object') {
|
|
181
|
+
for (const field of Object.values(node.dict ?? {})) markFields(field as Schema)
|
|
182
|
+
} else {
|
|
183
|
+
node.meta.volatile = true
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
markFields(schema)
|
|
187
|
+
return schema
|
|
188
|
+
})()
|
|
189
|
+
|
|
190
|
+
/** Cordis resolves this export before apply(); select the host's schema dialect here. */
|
|
191
|
+
export const Config: Schema<VisionToolkitConfig> = typeof (SettingsService.prototype as { register?: unknown }).register === 'function'
|
|
192
|
+
? LegacyConfig
|
|
193
|
+
: VolatileConfig
|
|
194
|
+
|
|
195
|
+
/** Resolve Schemastery's live field wrappers into ordinary config data. */
|
|
196
|
+
export function plainVisionConfig(value: VisionToolkitConfig): VisionToolkitConfig {
|
|
197
|
+
const visit = (current: unknown): unknown => {
|
|
198
|
+
if (current !== null && typeof current === 'object'
|
|
199
|
+
&& typeof (current as { get?: unknown }).get === 'function'
|
|
200
|
+
&& Symbol.for('cosmokit.volatile.write') in current) {
|
|
201
|
+
return visit((current as { get(): unknown }).get())
|
|
202
|
+
}
|
|
203
|
+
if (Array.isArray(current)) return current.map(visit)
|
|
204
|
+
if (current !== null && typeof current === 'object') {
|
|
205
|
+
return Object.fromEntries(Object.entries(current).map(([key, child]) => [key, visit(child)]))
|
|
206
|
+
}
|
|
207
|
+
return current
|
|
208
|
+
}
|
|
209
|
+
return visit(value) as VisionToolkitConfig
|
|
210
|
+
}
|
|
211
|
+
|
|
167
212
|
/** Configuration after static validation, with every default materialized. */
|
|
168
213
|
export interface ResolvedVisionToolkitConfig {
|
|
169
214
|
provider: {
|
|
170
215
|
baseUrl: string
|
|
171
216
|
credential: CredentialRef
|
|
172
217
|
model: string
|
|
173
|
-
protocol: 'openai' | 'anthropic'
|
|
218
|
+
protocol: 'openai' | 'responses' | 'anthropic'
|
|
219
|
+
reasoningEffort?: string
|
|
174
220
|
anthropicThinking: 'omit' | 'disabled' | 'adaptive'
|
|
175
221
|
userAgent: string
|
|
222
|
+
headers: Record<string, string>
|
|
223
|
+
sessionHeaders: string[]
|
|
176
224
|
}
|
|
177
225
|
language: 'zh' | 'en'
|
|
178
226
|
timeoutMs: number
|
|
@@ -199,6 +247,107 @@ const MAX_TIMEOUT_MS = 600000
|
|
|
199
247
|
const MAX_IMAGE_BYTES = 268435456
|
|
200
248
|
const MAX_IMAGE_PIXELS = 268435456
|
|
201
249
|
const MAX_CONCURRENCY = 16
|
|
250
|
+
const MAX_REASONING_EFFORT_LENGTH = 64
|
|
251
|
+
const REASONING_EFFORT_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/u
|
|
252
|
+
const MAX_PROVIDER_HEADER_COUNT = 32
|
|
253
|
+
const MAX_PROVIDER_HEADER_NAME_BYTES = 128
|
|
254
|
+
const MAX_PROVIDER_HEADER_VALUE_BYTES = 4096
|
|
255
|
+
const MAX_PROVIDER_HEADER_BYTES = 16 * 1024
|
|
256
|
+
|
|
257
|
+
/** Headers owned by the client or HTTP transport cannot be taken over by configuration. */
|
|
258
|
+
const RESERVED_PROVIDER_HEADER_NAMES = new Set([
|
|
259
|
+
'authorization',
|
|
260
|
+
'connection',
|
|
261
|
+
'content-length',
|
|
262
|
+
'content-type',
|
|
263
|
+
'cookie',
|
|
264
|
+
'expect',
|
|
265
|
+
'host',
|
|
266
|
+
'keep-alive',
|
|
267
|
+
'proxy-authenticate',
|
|
268
|
+
'proxy-authorization',
|
|
269
|
+
'proxy-connection',
|
|
270
|
+
'set-cookie',
|
|
271
|
+
'te',
|
|
272
|
+
'trailer',
|
|
273
|
+
'transfer-encoding',
|
|
274
|
+
'upgrade',
|
|
275
|
+
'user-agent',
|
|
276
|
+
'www-authenticate',
|
|
277
|
+
'x-api-key',
|
|
278
|
+
'anthropic-version',
|
|
279
|
+
])
|
|
280
|
+
|
|
281
|
+
function headerBytes(value: string): number {
|
|
282
|
+
return Buffer.byteLength(value, 'utf8')
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Validate one header without echoing its potentially sensitive value. */
|
|
286
|
+
function assertSendableHeader(field: string, name: string, value: string): void {
|
|
287
|
+
if (headerBytes(name) > MAX_PROVIDER_HEADER_NAME_BYTES) {
|
|
288
|
+
throw new VisionToolkitError('config', `${field} entry "${name}" exceeds the ${MAX_PROVIDER_HEADER_NAME_BYTES}-byte name limit`)
|
|
289
|
+
}
|
|
290
|
+
if (headerBytes(value) > MAX_PROVIDER_HEADER_VALUE_BYTES) {
|
|
291
|
+
throw new VisionToolkitError('config', `${field} entry "${name}" exceeds the ${MAX_PROVIDER_HEADER_VALUE_BYTES}-byte value limit`)
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
new Headers([[name, value]])
|
|
295
|
+
} catch {
|
|
296
|
+
throw new VisionToolkitError(
|
|
297
|
+
'config',
|
|
298
|
+
`${field} entry "${name}" is not a valid HTTP header; use a valid field name and a single-line value`,
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function normalizeProviderHeaders(provider: NonNullable<VisionToolkitConfig['provider']>): {
|
|
304
|
+
headers: Record<string, string>
|
|
305
|
+
sessionHeaders: string[]
|
|
306
|
+
} {
|
|
307
|
+
const normalized = new Map<string, string>()
|
|
308
|
+
for (const [rawName, value] of Object.entries(provider.headers ?? {})) {
|
|
309
|
+
const name = rawName.trim().toLowerCase()
|
|
310
|
+
if (name.length === 0) throw new VisionToolkitError('config', 'provider.headers has an entry with an empty name')
|
|
311
|
+
if (RESERVED_PROVIDER_HEADER_NAMES.has(name)) {
|
|
312
|
+
throw new VisionToolkitError('config', `provider.headers must not set "${name}"; the client or HTTP transport owns it`)
|
|
313
|
+
}
|
|
314
|
+
if (normalized.has(name)) {
|
|
315
|
+
throw new VisionToolkitError('config', `provider.headers contains the duplicate name "${name}"`)
|
|
316
|
+
}
|
|
317
|
+
assertSendableHeader('provider.headers', name, value)
|
|
318
|
+
normalized.set(name, value)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const sessions = new Set<string>()
|
|
322
|
+
for (const rawName of provider.sessionHeaders ?? []) {
|
|
323
|
+
const name = rawName.trim().toLowerCase()
|
|
324
|
+
if (name.length === 0) throw new VisionToolkitError('config', 'provider.sessionHeaders has an empty entry')
|
|
325
|
+
if (RESERVED_PROVIDER_HEADER_NAMES.has(name)) {
|
|
326
|
+
throw new VisionToolkitError('config', `provider.sessionHeaders must not name "${name}"; the client or HTTP transport owns it`)
|
|
327
|
+
}
|
|
328
|
+
if (sessions.has(name)) {
|
|
329
|
+
throw new VisionToolkitError('config', `provider.sessionHeaders contains the duplicate name "${name}"`)
|
|
330
|
+
}
|
|
331
|
+
if (normalized.has(name)) {
|
|
332
|
+
throw new VisionToolkitError('config', `provider.headers and provider.sessionHeaders both name "${name}"`)
|
|
333
|
+
}
|
|
334
|
+
assertSendableHeader('provider.sessionHeaders', name, '0'.repeat(32))
|
|
335
|
+
sessions.add(name)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (normalized.size + sessions.size > MAX_PROVIDER_HEADER_COUNT) {
|
|
339
|
+
throw new VisionToolkitError('config', `provider headers must contain at most ${MAX_PROVIDER_HEADER_COUNT} entries in total`)
|
|
340
|
+
}
|
|
341
|
+
const compareNames = (left: string, right: string): number => left < right ? -1 : left > right ? 1 : 0
|
|
342
|
+
const entries = [...normalized.entries()].sort(([left], [right]) => compareNames(left, right))
|
|
343
|
+
const sessionHeaders = [...sessions].sort(compareNames)
|
|
344
|
+
const totalBytes = entries.reduce((total, [name, value]) => total + headerBytes(name) + headerBytes(value) + 4, 0)
|
|
345
|
+
+ sessionHeaders.reduce((total, name) => total + headerBytes(name) + 32 + 4, 0)
|
|
346
|
+
if (totalBytes > MAX_PROVIDER_HEADER_BYTES) {
|
|
347
|
+
throw new VisionToolkitError('config', `provider headers exceed the ${MAX_PROVIDER_HEADER_BYTES}-byte total limit`)
|
|
348
|
+
}
|
|
349
|
+
return { headers: Object.fromEntries(entries), sessionHeaders }
|
|
350
|
+
}
|
|
202
351
|
|
|
203
352
|
/**
|
|
204
353
|
* Validate and normalize a config object (partial inputs receive the same
|
|
@@ -230,8 +379,16 @@ export function resolveConfig(config: VisionToolkitConfig = {}): ResolvedVisionT
|
|
|
230
379
|
throw new VisionToolkitError('config', 'provider.model must not be empty')
|
|
231
380
|
}
|
|
232
381
|
const protocol = provider.protocol ?? 'openai'
|
|
233
|
-
if (protocol !== 'openai' && protocol !== 'anthropic') {
|
|
234
|
-
throw new VisionToolkitError('config', 'provider.protocol must be "openai" or "anthropic"')
|
|
382
|
+
if (protocol !== 'openai' && protocol !== 'responses' && protocol !== 'anthropic') {
|
|
383
|
+
throw new VisionToolkitError('config', 'provider.protocol must be "openai", "responses", or "anthropic"')
|
|
384
|
+
}
|
|
385
|
+
const reasoningEffort = protocol === 'responses' ? provider.reasoningEffort?.trim() : undefined
|
|
386
|
+
if (reasoningEffort !== undefined && reasoningEffort.length > 0
|
|
387
|
+
&& (reasoningEffort.length > MAX_REASONING_EFFORT_LENGTH || !REASONING_EFFORT_PATTERN.test(reasoningEffort))) {
|
|
388
|
+
throw new VisionToolkitError(
|
|
389
|
+
'config',
|
|
390
|
+
`provider.reasoningEffort must be 1-${MAX_REASONING_EFFORT_LENGTH} ASCII letters, digits, dots, underscores, or hyphens`,
|
|
391
|
+
)
|
|
235
392
|
}
|
|
236
393
|
const anthropicThinking = provider.anthropicThinking ?? 'omit'
|
|
237
394
|
if (anthropicThinking !== 'omit' && anthropicThinking !== 'disabled' && anthropicThinking !== 'adaptive') {
|
|
@@ -241,6 +398,7 @@ export function resolveConfig(config: VisionToolkitConfig = {}): ResolvedVisionT
|
|
|
241
398
|
if (userAgent.length === 0) {
|
|
242
399
|
throw new VisionToolkitError('config', 'provider.userAgent must not be empty')
|
|
243
400
|
}
|
|
401
|
+
const { headers, sessionHeaders } = normalizeProviderHeaders(provider)
|
|
244
402
|
const language = config.language ?? 'zh'
|
|
245
403
|
if (language !== 'zh' && language !== 'en') {
|
|
246
404
|
throw new VisionToolkitError('config', 'language must be "zh" or "en"')
|
|
@@ -289,7 +447,11 @@ export function resolveConfig(config: VisionToolkitConfig = {}): ResolvedVisionT
|
|
|
289
447
|
.map(provider => provider.trim())
|
|
290
448
|
.filter(provider => provider.length > 0)
|
|
291
449
|
return {
|
|
292
|
-
provider: {
|
|
450
|
+
provider: {
|
|
451
|
+
baseUrl, credential, model, protocol,
|
|
452
|
+
...(reasoningEffort === undefined || reasoningEffort.length === 0 ? {} : { reasoningEffort }),
|
|
453
|
+
anthropicThinking, userAgent, headers, sessionHeaders,
|
|
454
|
+
},
|
|
293
455
|
language,
|
|
294
456
|
timeoutMs,
|
|
295
457
|
maxImageBytes,
|
package/src/evidence-cache.ts
CHANGED
|
@@ -89,7 +89,10 @@ export function evidenceRuntimeFingerprint(
|
|
|
89
89
|
},
|
|
90
90
|
model: config.provider.model,
|
|
91
91
|
protocol: config.provider.protocol,
|
|
92
|
+
reasoningEffort: config.provider.reasoningEffort ?? null,
|
|
92
93
|
anthropicThinking: config.provider.anthropicThinking,
|
|
94
|
+
headersSha256: hash(JSON.stringify(config.provider.headers)),
|
|
95
|
+
sessionHeaders: config.provider.sessionHeaders,
|
|
93
96
|
sslVerify: sslVerify ?? null,
|
|
94
97
|
userAgent: config.provider.userAgent,
|
|
95
98
|
},
|
|
@@ -14,7 +14,7 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
|
14
14
|
import { tmpdir } from 'node:os'
|
|
15
15
|
import { isAbsolute, join, resolve } from 'node:path'
|
|
16
16
|
import type { Context } from '@deepseek-ai/cordis'
|
|
17
|
-
import LlmService, { LlmAdapter, contentHasImage } from '@deepseek-ai/dsh-llm'
|
|
17
|
+
import LlmService, { LlmAdapter, contentHasImage, freezeMessage } from '@deepseek-ai/dsh-llm'
|
|
18
18
|
import type {
|
|
19
19
|
ContentBlock,
|
|
20
20
|
GenerateOptions,
|
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
LlmProviderInfo,
|
|
24
24
|
LlmResolvedModelInfo,
|
|
25
25
|
Message,
|
|
26
|
+
ModelMessageSource,
|
|
26
27
|
ResolvedRetryPolicy,
|
|
27
28
|
StreamChunk,
|
|
28
29
|
} from '@deepseek-ai/dsh-llm'
|
|
@@ -390,7 +391,11 @@ async function readImageBlock(
|
|
|
390
391
|
// deadline still bounds it.
|
|
391
392
|
const result = await current.glance(
|
|
392
393
|
{ images: [materialized.file], query },
|
|
393
|
-
{
|
|
394
|
+
{
|
|
395
|
+
signal: new AbortController().signal,
|
|
396
|
+
workspace: materialized.workspace,
|
|
397
|
+
...(sessionId === undefined ? {} : { sessionId }),
|
|
398
|
+
},
|
|
394
399
|
)
|
|
395
400
|
const answer = result.answer.trim()
|
|
396
401
|
if (answer.length === 0) throw new Error('the Vision Toolkit returned an empty description')
|
|
@@ -444,7 +449,10 @@ export async function convertImagesToEvidence(
|
|
|
444
449
|
for (const message of messages) {
|
|
445
450
|
let hint = ''
|
|
446
451
|
let hintSource: VisionHintSource = 'user'
|
|
447
|
-
|
|
452
|
+
// `source` is declared required but is optional at runtime: the official
|
|
453
|
+
// createUserMessage() never writes one. A user-role message that does not
|
|
454
|
+
// declare its producer is still the current user turn.
|
|
455
|
+
if (message.role === 'user' && (message.source?.kind ?? 'user') === 'user') {
|
|
448
456
|
const itemUserText = userMessageText(message)
|
|
449
457
|
if (itemUserText.length > 0) {
|
|
450
458
|
lastUserText = itemUserText
|
|
@@ -508,6 +516,82 @@ export async function convertImagesToEvidence(
|
|
|
508
516
|
return converted
|
|
509
517
|
}
|
|
510
518
|
|
|
519
|
+
/**
|
|
520
|
+
* Present one delegated request's assistant history under the upstream route,
|
|
521
|
+
* restoring the adapter-private replay state the host withholds from a facade.
|
|
522
|
+
*
|
|
523
|
+
* The host hands `replayState` to a target adapter only while that exact
|
|
524
|
+
* adapter instance owns both the historical provider and the target provider.
|
|
525
|
+
* A variant route is a different instance, so history produced under the
|
|
526
|
+
* upstream route arrives stripped, and history produced under the variant
|
|
527
|
+
* route is stripped again inside the delegation (the upstream adapter does not
|
|
528
|
+
* own the variant route either). Losing that state costs provider-native
|
|
529
|
+
* fidelity — thinking signatures, native effort binding, response ids — which
|
|
530
|
+
* makes an upstream that needs reasoning continuity answer without thinking
|
|
531
|
+
* while still being asked for it, and the host reports only a state it could
|
|
532
|
+
* not use, never one it withheld.
|
|
533
|
+
*
|
|
534
|
+
* The durable transcript is the authority for what was withheld: history
|
|
535
|
+
* messages of this wrapper's own routes are matched by id, and only the request
|
|
536
|
+
* copy is re-provenanced. The Session itself is never touched, so the durable
|
|
537
|
+
* log keeps the route the user actually selected.
|
|
538
|
+
* @param upstream - upstream route the delegated request is dispatched under.
|
|
539
|
+
* @param messages - messages about to be delegated to that route.
|
|
540
|
+
* @param transcript - reads the durable Session transcript on demand, if any.
|
|
541
|
+
* @returns the messages to delegate: the input array when nothing needed repair.
|
|
542
|
+
*/
|
|
543
|
+
function presentUnderUpstreamRoute(
|
|
544
|
+
upstream: string,
|
|
545
|
+
messages: Message[],
|
|
546
|
+
transcript: () => readonly Message[] | undefined,
|
|
547
|
+
): Message[] {
|
|
548
|
+
const variant = variantProviderId(upstream)
|
|
549
|
+
let durable: Map<string, Message> | undefined
|
|
550
|
+
let presented: Message[] | undefined
|
|
551
|
+
messages.forEach((message, index) => {
|
|
552
|
+
const source = message.source
|
|
553
|
+
// Without provenance there is no route to prove and nothing to restore;
|
|
554
|
+
// skip it exactly like history produced under another route.
|
|
555
|
+
if (message.role !== 'assistant' || source?.kind !== 'model') return
|
|
556
|
+
if (source.provider !== upstream && source.provider !== variant) return
|
|
557
|
+
let replayState = source.replayState
|
|
558
|
+
if (replayState === undefined) {
|
|
559
|
+
durable ??= new Map(
|
|
560
|
+
(transcript() ?? [])
|
|
561
|
+
.filter(candidate => candidate.role === 'assistant')
|
|
562
|
+
.map(candidate => [String(candidate.id), candidate] as const),
|
|
563
|
+
)
|
|
564
|
+
const withheld = durable.get(String(message.id))
|
|
565
|
+
// Provenance and shape must still describe this very message: a Session
|
|
566
|
+
// that was rewritten under us (a fold, a rewind) must not have one
|
|
567
|
+
// message's state reattached to another's content.
|
|
568
|
+
if (withheld !== undefined && withheld.source.kind === 'model'
|
|
569
|
+
&& withheld.source.provider === source.provider
|
|
570
|
+
&& withheld.source.replayState !== undefined
|
|
571
|
+
&& sameBlockShape(withheld.content, message.content)) {
|
|
572
|
+
replayState = withheld.source.replayState
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
// Nothing withheld and no re-provenancing to do: leave the message alone.
|
|
576
|
+
if (source.provider === upstream && replayState === undefined) return
|
|
577
|
+
const replayed: ModelMessageSource = {
|
|
578
|
+
kind: 'model',
|
|
579
|
+
provider: upstream,
|
|
580
|
+
model: source.model,
|
|
581
|
+
...(replayState === undefined ? {} : { replayState }),
|
|
582
|
+
}
|
|
583
|
+
presented ??= [...messages]
|
|
584
|
+
presented[index] = freezeMessage({ ...message, source: replayed })
|
|
585
|
+
})
|
|
586
|
+
return presented ?? messages
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/** Whether two block lists carry the same block types in the same order. */
|
|
590
|
+
function sameBlockShape(left: readonly ContentBlock[], right: readonly ContentBlock[]): boolean {
|
|
591
|
+
return left.length === right.length
|
|
592
|
+
&& left.every((block, index) => block.type === right[index]?.type)
|
|
593
|
+
}
|
|
594
|
+
|
|
511
595
|
/**
|
|
512
596
|
* The adapter behind one variant route: model metadata declares image input,
|
|
513
597
|
* and every stream rewrites image blocks before delegating to the upstream
|
|
@@ -539,6 +623,18 @@ export class ImageInputVariantAdapter extends LlmAdapter {
|
|
|
539
623
|
return this.llm.providerRetryPolicy(this.upstream)
|
|
540
624
|
}
|
|
541
625
|
|
|
626
|
+
/**
|
|
627
|
+
* Read the durable transcript of one Session, when the host still holds it.
|
|
628
|
+
* Only `presentUnderUpstreamRoute` consumes it, and only for messages whose
|
|
629
|
+
* replay state the host withheld from this facade route.
|
|
630
|
+
* @param sessionId - Session whose history is being delegated, if any.
|
|
631
|
+
* @returns the durable messages, or undefined without a live Session.
|
|
632
|
+
*/
|
|
633
|
+
private durableTranscript(sessionId: GenerateOptions['sessionId']): readonly Message[] | undefined {
|
|
634
|
+
if (sessionId === undefined) return undefined
|
|
635
|
+
return this.ctx.sessions.get(sessionId as never)?.deriveMessages()
|
|
636
|
+
}
|
|
637
|
+
|
|
542
638
|
override async listModels(provider: string): Promise<readonly LlmModelInfo[]> {
|
|
543
639
|
const models = await this.llm.listModels(this.upstream)
|
|
544
640
|
return models.filter(shouldWrapModel).map((model) => ({
|
|
@@ -605,9 +701,17 @@ export class ImageInputVariantAdapter extends LlmAdapter {
|
|
|
605
701
|
.digest('hex'),
|
|
606
702
|
storageDir,
|
|
607
703
|
)
|
|
608
|
-
// Delegate through the host service under the upstream route
|
|
609
|
-
//
|
|
610
|
-
|
|
704
|
+
// Delegate through the host service under the upstream route, so the
|
|
705
|
+
// upstream's own middleware, retry policy, and replay handling still apply.
|
|
706
|
+
// Replay state is host-owned and route-scoped, so the history this facade
|
|
707
|
+
// route cannot receive back is restored before that delegation: the durable
|
|
708
|
+
// log keeps the user's route, only the delegated copy is re-provenanced.
|
|
709
|
+
const delegated = presentUnderUpstreamRoute(
|
|
710
|
+
this.upstream,
|
|
711
|
+
messages,
|
|
712
|
+
() => this.durableTranscript(options.sessionId),
|
|
713
|
+
)
|
|
714
|
+
yield* this.llm.stream({ ...options, provider: this.upstream, messages: delegated })
|
|
611
715
|
}
|
|
612
716
|
}
|
|
613
717
|
|
package/src/index.ts
CHANGED
|
@@ -16,9 +16,8 @@ import type {} from '@deepseek-ai/dsh-session'
|
|
|
16
16
|
import type {} from '@deepseek-ai/dsh-settings'
|
|
17
17
|
import { ArtifactAccessController, prepareArtifactAccessKey } from './artifact-access.ts'
|
|
18
18
|
import {
|
|
19
|
-
Config,
|
|
20
|
-
VISION_TOOLKIT_SETTINGS_NAMESPACE,
|
|
21
19
|
prepareWatchedSettingsGeneration,
|
|
20
|
+
plainVisionConfig,
|
|
22
21
|
resolveConfig,
|
|
23
22
|
type ResolvedVisionToolkitConfig,
|
|
24
23
|
type VisionToolkitConfig,
|
|
@@ -28,6 +27,7 @@ import { createPasteTakeoverResolver, installImageInputVariants } from './image-
|
|
|
28
27
|
import { VisionToolkitRuntimeManager } from './runtime-manager.ts'
|
|
29
28
|
import { VISION_SKILLS_SKILL } from './skill.ts'
|
|
30
29
|
import { StorageHistoryStore } from './storage-history.ts'
|
|
30
|
+
import { bindVisionSettings } from './settings-compat.ts'
|
|
31
31
|
import { createVisionTools } from './tools.ts'
|
|
32
32
|
import { PLUGIN_VERSION } from './version.ts'
|
|
33
33
|
import { installVisionToolkitWeb, VisionToolkitWebBackend } from './web.ts'
|
|
@@ -35,7 +35,7 @@ import { MAX_PASTE_IMAGE_BYTES, PastedImageBackend } from './paste-images.ts'
|
|
|
35
35
|
|
|
36
36
|
export const name = '@anionex/dsh-vision-toolkit'
|
|
37
37
|
|
|
38
|
-
export { Config }
|
|
38
|
+
export { Config } from './config.ts'
|
|
39
39
|
|
|
40
40
|
export const inject = ['tools', 'credentials', 'skills', 'subprocess', 'settings', 'agents', 'sessions']
|
|
41
41
|
|
|
@@ -45,11 +45,7 @@ export async function apply(ctx: Context, config: VisionToolkitConfig = {}): Pro
|
|
|
45
45
|
// or Tool becomes visible. The custom Web editor preflights runtime changes
|
|
46
46
|
// before persistence; hand-edited settings still fail loud here or retain
|
|
47
47
|
// the last serving generation when changed live.
|
|
48
|
-
const settings = ctx
|
|
49
|
-
base: config,
|
|
50
|
-
applies: 'live',
|
|
51
|
-
validate: (value) => { resolveConfig(value) },
|
|
52
|
-
})
|
|
48
|
+
const settings = bindVisionSettings(ctx, plainVisionConfig(config))
|
|
53
49
|
const manager = new VisionToolkitRuntimeManager(ctx)
|
|
54
50
|
const artifacts = new ArtifactAccessController(await prepareArtifactAccessKey())
|
|
55
51
|
const lifecycle = new AbortController()
|