@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.
Files changed (47) hide show
  1. package/CHANGELOG.md +527 -0
  2. package/README.i18n.yaml +2 -2
  3. package/README.md +27 -4
  4. package/README.zh.md +27 -4
  5. package/docs/requirements-traceability/README.i18n.yaml +2 -2
  6. package/docs/requirements-traceability/README.md +4 -4
  7. package/docs/requirements-traceability/README.zh.md +4 -4
  8. package/lib/client.js +20 -1
  9. package/lib/client.js.map +1 -1
  10. package/lib/config.js +147 -5
  11. package/lib/config.js.map +1 -1
  12. package/lib/evidence-cache.js +3 -0
  13. package/lib/evidence-cache.js.map +1 -1
  14. package/lib/image-input-variants.js +100 -6
  15. package/lib/image-input-variants.js.map +1 -1
  16. package/lib/index.js +4 -7
  17. package/lib/index.js.map +1 -1
  18. package/lib/runtime.js +40 -5
  19. package/lib/runtime.js.map +1 -1
  20. package/lib/settings-compat.js +38 -0
  21. package/lib/settings-compat.js.map +1 -0
  22. package/lib/types/client/index.d.ts +7 -1
  23. package/lib/types/client/index.d.ts.map +1 -1
  24. package/lib/types/config.d.ts +19 -4
  25. package/lib/types/config.d.ts.map +1 -1
  26. package/lib/types/evidence-cache.d.ts.map +1 -1
  27. package/lib/types/image-input-variants.d.ts +8 -0
  28. package/lib/types/image-input-variants.d.ts.map +1 -1
  29. package/lib/types/index.d.ts +2 -2
  30. package/lib/types/index.d.ts.map +1 -1
  31. package/lib/types/runtime.d.ts +4 -0
  32. package/lib/types/runtime.d.ts.map +1 -1
  33. package/lib/types/settings-compat.d.ts +12 -0
  34. package/lib/types/settings-compat.d.ts.map +1 -0
  35. package/lib/types/upstream.d.ts +4 -1
  36. package/lib/types/upstream.d.ts.map +1 -1
  37. package/lib/upstream.js +88 -29
  38. package/lib/upstream.js.map +1 -1
  39. package/package.json +24 -22
  40. package/src/client/index.tsx +29 -3
  41. package/src/config.ts +171 -9
  42. package/src/evidence-cache.ts +3 -0
  43. package/src/image-input-variants.ts +110 -6
  44. package/src/index.ts +4 -8
  45. package/src/runtime.ts +45 -5
  46. package/src/settings-compat.ts +56 -0
  47. 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.44",
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"
@@ -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 type { SettingsNamespace } from '@deepseek-ai/dsh-settings'
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 Config: Schema<VisionToolkitConfig> = z.object({
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: { baseUrl, credential, model, protocol, anthropicThinking, userAgent },
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,
@@ -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
- { signal: new AbortController().signal, workspace: materialized.workspace },
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
- if (message.role === 'user' && message.source.kind === 'user') {
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: the variant
609
- // is a wire-only facade, and the upstream route owns retry and replay.
610
- yield* this.llm.stream({ ...options, provider: this.upstream, messages })
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.settings.register(VISION_TOOLKIT_SETTINGS_NAMESPACE, Config, {
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()