dsh-tap 0.20.0

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 (121) hide show
  1. package/AGENTS.md +99 -0
  2. package/CHANGELOG.md +751 -0
  3. package/LICENSE +21 -0
  4. package/README.en.md +59 -0
  5. package/README.md +256 -0
  6. package/cordis.patch.yml +385 -0
  7. package/core/bridge.js +698 -0
  8. package/core/json-store.js +91 -0
  9. package/core/rotation.js +108 -0
  10. package/core/usage-meter.js +176 -0
  11. package/docs/diagnosis-cache-quota.md +181 -0
  12. package/docs/diagnosis-qoder-flash.md +67 -0
  13. package/docs/diagnosis-trae-3003.md +302 -0
  14. package/docs/goals/bridge-port-host-split.md +91 -0
  15. package/docs/goals/desktop-adaptation.md +106 -0
  16. package/docs/goals/qoder-cn-provider-design.md +308 -0
  17. package/docs/goals/settings-card-ux-redesign-plan.md +1680 -0
  18. package/docs/goals/settings-card-ux-redesign.md +162 -0
  19. package/docs/goals/trae-agent-v3.md +41 -0
  20. package/docs/goals/trae-work-cn-repair.md +44 -0
  21. package/docs/goals/v0.8-/351/242/235/345/272/246/345/217/257/350/247/201-/346/250/241/345/236/213/345/212/250/346/200/201/345/214/226-/345/244/232/346/234/215/345/212/241/345/225/206.md +85 -0
  22. package/docs/pitfalls.md +105 -0
  23. package/docs/reverse/trae-cloud-api.md +218 -0
  24. package/docs/reverse/trae-model-catalog.md +129 -0
  25. package/docs/reverse/traework-cn.md +520 -0
  26. package/docs/rules/STATE.md +198 -0
  27. package/docs/rules/content-moderation.md +54 -0
  28. package/docs/rules/dev-role-boundary.md +94 -0
  29. package/docs/rules/extra-providers.md +42 -0
  30. package/docs/rules/gateway-facts.md +91 -0
  31. package/docs/rules/oauth-handshake.md +76 -0
  32. package/docs/rules/prompt-cache.md +93 -0
  33. package/docs/rules/quota-signals.md +125 -0
  34. package/docs/rules/routing.md +84 -0
  35. package/docs/rules/templates/oauth-reverse-checklist.md +42 -0
  36. package/docs/rules/trae-surface.md +242 -0
  37. package/docs/rules/ua-validation.md +81 -0
  38. package/host-config.js +282 -0
  39. package/index.js +2306 -0
  40. package/lib/client.js +2893 -0
  41. package/local-scan.js +104 -0
  42. package/package.json +82 -0
  43. package/providers/ark/index.js +11 -0
  44. package/providers/bailian/index.js +10 -0
  45. package/providers/bigmodel/index.js +11 -0
  46. package/providers/codebuddy/agenttool.js +122 -0
  47. package/providers/codebuddy/catalog.js +227 -0
  48. package/providers/codebuddy/errors.js +44 -0
  49. package/providers/codebuddy/headers.js +36 -0
  50. package/providers/codebuddy/images.js +125 -0
  51. package/providers/codebuddy/index.js +123 -0
  52. package/providers/codebuddy/oauth.js +279 -0
  53. package/providers/deepseek/index.js +11 -0
  54. package/providers/moonshot/index.js +11 -0
  55. package/providers/openai-compat.js +177 -0
  56. package/providers/openrouter/index.js +27 -0
  57. package/providers/qoder/catalog.js +145 -0
  58. package/providers/qoder/cosy.js +419 -0
  59. package/providers/qoder/gateway.js +563 -0
  60. package/providers/qoder/index.js +116 -0
  61. package/providers/qoder/oauth.js +364 -0
  62. package/providers/qoder/qoder_auth.wasm +0 -0
  63. package/providers/qoder/quota.js +56 -0
  64. package/providers/qwen/index.js +15 -0
  65. package/providers/tool-pairing.js +129 -0
  66. package/providers/trae/catalog.js +103 -0
  67. package/providers/trae/errors.js +85 -0
  68. package/providers/trae/gateway.js +853 -0
  69. package/providers/trae/index.js +126 -0
  70. package/providers/trae/oauth.js +443 -0
  71. package/providers/trae/quota.js +75 -0
  72. package/providers/trae/remote.js +365 -0
  73. package/scripts/capture-cache.mjs +65 -0
  74. package/scripts/capture-traffic.mjs +83 -0
  75. package/scripts/hermes-probe-dev-role.mjs +263 -0
  76. package/scripts/measure-latency.mjs +253 -0
  77. package/scripts/probe-ark-thinking.mjs +298 -0
  78. package/scripts/probe-cache-decline.mjs +292 -0
  79. package/scripts/probe-cache-ttl.mjs +221 -0
  80. package/scripts/probe-cache.mjs +156 -0
  81. package/scripts/probe-codebuddy-efforts.mjs +355 -0
  82. package/scripts/probe-codebuddy-tier-wiring.mjs +244 -0
  83. package/scripts/probe-effort-gaps.mjs +133 -0
  84. package/scripts/probe-media.mjs +115 -0
  85. package/scripts/probe-moderation.mjs +159 -0
  86. package/scripts/probe-oauth.mjs +617 -0
  87. package/scripts/probe-qoder-attribution-arm8.mjs +92 -0
  88. package/scripts/probe-qoder-attribution-arm9.mjs +102 -0
  89. package/scripts/probe-qoder-attribution.mjs +266 -0
  90. package/scripts/probe-qoder-flash-confirm.mjs +94 -0
  91. package/scripts/probe-qoder-live.mjs +344 -0
  92. package/scripts/probe-qoder-matrix.mjs +274 -0
  93. package/scripts/probe-qoder-null-content.mjs +153 -0
  94. package/scripts/probe-qoder-pairing.mjs +308 -0
  95. package/scripts/probe-qoder-quota.mjs +162 -0
  96. package/scripts/probe-qoder-thinking-config.mjs +52 -0
  97. package/scripts/probe-qoder-thinking-efforts.mjs +269 -0
  98. package/scripts/probe-quota.mjs +136 -0
  99. package/scripts/probe-quota2.mjs +144 -0
  100. package/scripts/probe-routing.mjs +226 -0
  101. package/scripts/probe-trae-3003-diagnosis.mjs +139 -0
  102. package/scripts/probe-trae-agent-v3.mjs +399 -0
  103. package/scripts/probe-trae-efforts.mjs +149 -0
  104. package/scripts/probe-trae-live.mjs +147 -0
  105. package/scripts/probe-trae-max-effort.mjs +179 -0
  106. package/scripts/probe-trae-model-routing.mjs +381 -0
  107. package/scripts/probe-trae-thinking-scene.mjs +238 -0
  108. package/scripts/probe-trae-transport-outage.mjs +176 -0
  109. package/scripts/probe-ua.mjs +508 -0
  110. package/scripts/trae-model-catalog.mjs +632 -0
  111. package/scripts/verify-agents-md.mjs +45 -0
  112. package/scripts/verify-bridge.mjs +1041 -0
  113. package/scripts/verify-core-generic.mjs +302 -0
  114. package/scripts/verify-desktop-acceptance.mjs +184 -0
  115. package/scripts/verify-host-config.mjs +265 -0
  116. package/scripts/verify-models.mjs +390 -0
  117. package/scripts/verify-providers.mjs +255 -0
  118. package/scripts/verify-qoder-provider.mjs +1020 -0
  119. package/scripts/verify-rotation.mjs +308 -0
  120. package/scripts/verify-trae-model-catalog.mjs +284 -0
  121. package/scripts/verify-trae-provider.mjs +1451 -0
@@ -0,0 +1,177 @@
1
+ /**
2
+ * providers/openai-compat.js — key 型 OpenAI 兼容上游的共享骨架。
3
+ *
4
+ * 一个"适配器实例"= preset(id/displayName/baseURL/keyRef)+ 本模块的
5
+ * 通用行为:GET {baseURL}/models 拉模型清单、组装 llm-pi-ai provider 块。
6
+ * 上游特化(baseURL 方言、模型尺寸表等)放进 providers/<name>/index.js,
7
+ * 本模块与 core/ 一样不得出现具体上游名(静态扫描在 verify-core-generic)。
8
+ */
9
+
10
+ /** 官方 CustomProviderCard 同款路由 id 规则(dsh-client-ui-settings-models)。 */
11
+ export const PROVIDER_ID_RE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/
12
+
13
+ /** 凭据命名惯例:<ROUTE>_API_KEY(大写、非字母数字折 _)。 */
14
+ export function keyRefFor(id) {
15
+ return id.toUpperCase().replace(/[^A-Z0-9]/g, '_') + '_API_KEY'
16
+ }
17
+
18
+ /**
19
+ * GET {baseURL}/models(OpenAI 标准目录端点),Bearer key 直调。
20
+ * 返回 [{id}];非 2xx 带上游摘要抛错(添加前先用它验 key)。
21
+ * HTTP 404 单独标记 err.code = 'MODELS_ENDPOINT_404'——部分真实上游
22
+ * (iFlow、portal.qwen.ai 等)根本没有 /models 路由,调用方可回落
23
+ * probeChatKey 验证(preset 声明 fallbackModels 时)。
24
+ */
25
+ export async function fetchOpenAIModels(baseURL, apiKey, { timeoutMs = 15000 } = {}) {
26
+ const ctrl = new AbortController()
27
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs)
28
+ try {
29
+ const res = await fetch(baseURL.replace(/\/+$/, '') + '/models', {
30
+ headers: { authorization: `Bearer ${apiKey}`, accept: 'application/json' },
31
+ signal: ctrl.signal,
32
+ })
33
+ const text = await res.text()
34
+ if (!res.ok) {
35
+ const err = new Error(`GET /models ${res.status}: ${text.slice(0, 200)}`)
36
+ if (res.status === 404) err.code = 'MODELS_ENDPOINT_404'
37
+ throw err
38
+ }
39
+ let data
40
+ try {
41
+ data = JSON.parse(text)
42
+ } catch {
43
+ throw new Error(`GET /models 返回非 JSON: ${text.slice(0, 120)}`)
44
+ }
45
+ const rows = Array.isArray(data?.data) ? data.data : []
46
+ const models = rows
47
+ .filter((m) => typeof m?.id === 'string' && m.id)
48
+ .map((m) => ({ id: m.id }))
49
+ if (!models.length) throw new Error('GET /models 返回空清单(key 可能无权限)')
50
+ return models
51
+ } finally {
52
+ clearTimeout(timer)
53
+ }
54
+ }
55
+
56
+ /**
57
+ * 组装 llm-pi-ai provider 块(写 settings.yaml 的形状)。
58
+ * apiKeyEnv 指向 ~/.dsh/.credentials.yaml 里的 keyRef;sizes 不给就吃
59
+ * dsh 默认值(defaultContextWindow 262144 / defaultMaxTokens 32768)。
60
+ *
61
+ * **思考档位(0.19.0)**:preset 可用 `modelEfforts`(`{ '<模型 id>': { <档位>: <线值> } }`)
62
+ * 声明逐模型档位表——写入时按模型 id 合并进条目的 `reasoningEfforts`,宿主
63
+ * 「推理等级」据此出档;`provider-refresh`(GET /models 重建清单)走同一函数,
64
+ * **声明不丢**。两条纪律:
65
+ * a) **必须逐条有实测/厂商文档依据**(踩坑 #42:能力声明 ≠ 线值,不批量臆造;
66
+ * 上游没验证过的 provider 就保持不声明 = 不出档,而不是摆假档位);
67
+ * b) 档位拼写逐上游不同(OpenAI 官方 `low/medium/high`、OpenRouter 走
68
+ * `reasoning:{effort}` 另一方言…)——本函数只透传声明,不做拼写映射;
69
+ * OpenRouter 这类需要换 `thinkingFormat` 的上游要单独给 preset 加 compat。
70
+ * `reasoningCompat: true` 时块带 `compat.supportsReasoningEffort`:**缺它 pi-ai
71
+ * 出站不会把选中档位写成 `reasoning_effort`**(选择器出档但请求里没这个键)。
72
+ */
73
+ export function providerBlock(preset, models) {
74
+ const efforts = preset.modelEfforts && typeof preset.modelEfforts === 'object' ? preset.modelEfforts : {}
75
+ const entries = models.map((m) => {
76
+ const declared = efforts[m.id]
77
+ return {
78
+ ...(m.name ? { id: m.id, name: m.name } : { id: m.id }),
79
+ ...(declared && typeof declared === 'object' && Object.keys(declared).length ? { reasoningEfforts: declared } : {}),
80
+ }
81
+ })
82
+ return {
83
+ displayName: preset.displayName,
84
+ api: 'openai-completions',
85
+ baseURL: preset.baseURL,
86
+ apiKeyEnv: keyRefFor(preset.id),
87
+ ...(preset.reasoningCompat === true
88
+ ? { compat: { thinkingFormat: 'openai', supportsReasoningEffort: true } }
89
+ : {}),
90
+ models: entries,
91
+ }
92
+ }
93
+
94
+ /**
95
+ * 认证失败的身体特征(各上游方言汇总,2026-08-19 实测):
96
+ * - 标准:HTTP 401/403,或 body error.code = invalid_api_key 类
97
+ * - iFlow 方言:HTTP 200 + {"status":"434","msg":"Invalid apiKey…"}
98
+ */
99
+ const AUTH_FAIL_RE = /invalid[_ ]?(api[_ ]?key|access token)|unauthorized|"status"\s*:\s*"?434/i
100
+
101
+ /**
102
+ * 无 /models 端点上游的 key 验证:POST /chat/completions 最小探针
103
+ * (max_tokens 1,耗量可忽略;仅导入时用户确认后运行一次)。
104
+ * 认证失败抛错;其余一切响应(含模型错误等 4xx)视为 key 有效——
105
+ * 服务器拒绝的是请求内容而不是凭据。网络错误原样抛出(无法判定)。
106
+ */
107
+ export async function probeChatKey(baseURL, apiKey, model, { timeoutMs = 20000 } = {}) {
108
+ const ctrl = new AbortController()
109
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs)
110
+ try {
111
+ const res = await fetch(baseURL.replace(/\/+$/, '') + '/chat/completions', {
112
+ method: 'POST',
113
+ headers: { authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' },
114
+ body: JSON.stringify({ model, messages: [{ role: 'user', content: 'ping' }], max_tokens: 1, stream: false }),
115
+ signal: ctrl.signal,
116
+ })
117
+ const text = await res.text()
118
+ if (res.status === 401 || res.status === 403 || AUTH_FAIL_RE.test(text.slice(0, 400))) {
119
+ throw new Error(`key 验证失败(上游认证拒绝 ${res.status}): ${text.slice(0, 160)}`)
120
+ }
121
+ } finally {
122
+ clearTimeout(timer)
123
+ }
124
+ }
125
+
126
+ /** 注册表入口:preset + 凭据引用 + 通用行为。 */
127
+ export function createOpenAICompatProvider(preset) {
128
+ const fallback = Array.isArray(preset.fallbackModels) ? preset.fallbackModels.filter((m) => typeof m === 'string' && m) : []
129
+ const staticCatalog = !!preset.staticCatalog
130
+ return {
131
+ id: preset.id,
132
+ displayName: preset.displayName,
133
+ baseURL: preset.baseURL,
134
+ keyRef: keyRefFor(preset.id),
135
+ fallbackModels: fallback,
136
+ staticCatalog,
137
+ async fetchModels(apiKey) {
138
+ // 静态目录模式(公开目录型上游:/models 任意 key 都 200,
139
+ // 验不了 key,且全量 400+ 条进选择器不可用):chat 探针验 key,
140
+ // 清单恒吃 fallbackModels 内置表(裁判 docs/rules/extra-providers.md E-P7)。
141
+ if (staticCatalog && fallback.length) {
142
+ await probeChatKey(preset.baseURL, apiKey, fallback[0])
143
+ return fallback.map((id) => ({ id }))
144
+ }
145
+ try {
146
+ return await fetchOpenAIModels(preset.baseURL, apiKey)
147
+ } catch (err) {
148
+ // 上游没有 /models 且 preset 给了兜底清单:chat 探针验 key,清单吃兜底。
149
+ if (err?.code === 'MODELS_ENDPOINT_404' && fallback.length) {
150
+ await probeChatKey(preset.baseURL, apiKey, fallback[0])
151
+ return fallback.map((id) => ({ id }))
152
+ }
153
+ throw err
154
+ }
155
+ },
156
+ modelBlock: (models) => providerBlock({ ...preset }, models),
157
+ }
158
+ }
159
+
160
+ /**
161
+ * 登记册条目重建 adapter(index.js 的 test/refresh 两路共用接缝):registry
162
+ * entry 打底(displayName/baseURL 用添加时快照),preset 命中时合并先验
163
+ * 清单(fallbackModels/staticCatalog)与思考档位声明(modelEfforts/
164
+ * reasoningCompat)。refresh 拿重建的 adapter 重铺 provider 块——档位声明
165
+ * 必须随行,漏带 = 每次刷新把宿主「推理等级」的档位表洗掉(0.19.0 修:
166
+ * 此前 refresh 裸调 createOpenAICompatProvider 漏传两个声明字段;
167
+ * 回归 verify-providers 第 10 节)。
168
+ */
169
+ export function rebuildAdapterForEntry(entry, preset) {
170
+ return createOpenAICompatProvider({
171
+ ...entry,
172
+ fallbackModels: preset?.fallbackModels,
173
+ staticCatalog: preset?.staticCatalog,
174
+ modelEfforts: preset?.modelEfforts,
175
+ reasoningCompat: preset?.reasoningCompat,
176
+ })
177
+ }
@@ -0,0 +1,27 @@
1
+ import { createOpenAICompatProvider } from '../openai-compat.js'
2
+
3
+ /**
4
+ * OpenRouter preset。2026-09-11 实测:GET /models 是公开目录——任意 key
5
+ * (甚至无 key)都 200 返回全量 437 条,验不了 key 也不宜全量进选择器
6
+ * → staticCatalog:chat 探针验 key(标准 401 方言),清单吃内置精选表。
7
+ * 清单 = 当日目录实况里各厂商旗舰;探针用首项。刷新模型重跑同一逻辑,
8
+ * 目录漂移靠版本更新内置表(错误项聊天时显性暴露,裁判 E-P7)。
9
+ */
10
+ export default createOpenAICompatProvider({
11
+ id: 'openrouter',
12
+ displayName: 'OpenRouter',
13
+ baseURL: 'https://openrouter.ai/api/v1',
14
+ staticCatalog: true,
15
+ fallbackModels: [
16
+ 'openai/gpt-5.2',
17
+ 'anthropic/claude-opus-4.8',
18
+ 'anthropic/claude-sonnet-4.6',
19
+ 'google/gemini-3.1-pro-preview',
20
+ 'deepseek/deepseek-v3.2',
21
+ 'moonshotai/kimi-k2.6',
22
+ 'z-ai/glm-5.3',
23
+ 'qwen/qwen3-max',
24
+ 'minimax/minimax-m2.7',
25
+ 'x-ai/grok-4.6',
26
+ ],
27
+ })
@@ -0,0 +1,145 @@
1
+ /**
2
+ * providers/qoder/catalog.js — Qoder CN 模型目录(/algo/api/v2/model/list)。
3
+ *
4
+ * 2026-09-20 实测:签名 GET 返回明文 JSON(即便带 Encode=1——服务端对该
5
+ * 端点不加密;密文时走 cosy.decrypt 兜底)。条目取 `.chat` 数组:
6
+ * { key, display_name, format, source, enable, is_vl, is_reasoning,
7
+ * max_input_tokens, context_config: { "<档>": { token_count, is_default } },
8
+ * thinking_config: { disabled: {...}, enabled: { efforts: { "<档>": {...} } } } }
9
+ * 只收 format==='openai' 且 enable!==false 的条目(实测 14 个全满足)。
10
+ * contextWindow 取 context_config 默认档(无则 max_input_tokens);
11
+ * 目录不发布输出上限——maxTokens 取 32768 保守默认(可在 settings 镜像后手调)。
12
+ * 思考强度取 thinking_config 的逐模型能力声明(见 qoderReasoningEfforts)——
13
+ * 投影进 profile.reasoningEfforts,宿主 Model/Effort 选择器据此出档(同
14
+ * CodeBuddy 侧 catalogReasoningEfforts 的分层:目录声明 → 镜像 → 宿主选择器)。
15
+ */
16
+
17
+ /**
18
+ * thinking_config → dsh `reasoningEfforts` 档位表(键 = 档位名,值 = 出站线值)。
19
+ *
20
+ * 2026-10-04 实测(14 模型全量目录,证据 docs/probes/qoder-thinking-config-*.json):
21
+ * { disabled: {...}, enabled: { efforts: { "<档>": { is_default? } }, is_default? } }
22
+ * - `disabled` 在场 = 该模型**声明**支持关思考 → `off: null`。null = 省略参数
23
+ * (与网关 prefs 的 off 语义、codebuddy 侧 off 档同一条线),不是发 "off"
24
+ * 拼写——档位拼写的接受面逐模型不一致,不臆造(踩坑 #42)。
25
+ * - `enabled.efforts` 的键 = 可选档位,线值照抄档位名(与设置卡 prefs 的既有
26
+ * 拼写同源;网关只做"未带时补默认",不翻译)。
27
+ * - 无 thinking_config、或 enabled 里没有命名档位(如 qmodel/qmodel_latest 只有
28
+ * 一个开关)→ 返回 null:llm-pi-ai 拒绝"只有 off"的档位表,且没有强度可选时
29
+ * 不该在宿主选择器里摆一个空档位。
30
+ * @param {object} entry 目录原始条目
31
+ * @returns {Record<string, string|null>|null} 档位表或 null
32
+ */
33
+ export function qoderReasoningEfforts(entry) {
34
+ const tc = entry?.thinking_config
35
+ if (!tc || typeof tc !== 'object') return null
36
+ const enabled = tc.enabled && typeof tc.enabled === 'object' ? tc.enabled : null
37
+ const efforts = enabled?.efforts && typeof enabled.efforts === 'object' ? enabled.efforts : null
38
+ const levels = efforts
39
+ ? Object.keys(efforts).filter((k) => typeof k === 'string' && k)
40
+ : []
41
+ if (!levels.length) return null
42
+ const table = {}
43
+ if (tc.disabled && typeof tc.disabled === 'object') table.off = null
44
+ for (const level of levels) table[level] = level
45
+ return table
46
+ }
47
+
48
+ /**
49
+ * 该模型**展示用**档位清单——设置卡 select 与宿主「推理等级」的共同真源。
50
+ * = 线值表的键(顺序随目录声明);含 `off` 仅当目录声明了 `disabled`。
51
+ * 目录没声明命名档位(如 auto/qmodel)→ `[]`:卡里不出该 select,也不接受
52
+ * effort 写入(宁可不给控件,也不摆假档位,踩坑 #42 同纪律)。
53
+ * @param {object} entry 目录原始条目
54
+ * @returns {string[]} 档位名数组(可能为空)
55
+ */
56
+ export function qoderEffortTiers(entry) {
57
+ const table = qoderReasoningEfforts(entry)
58
+ return table ? Object.keys(table) : []
59
+ }
60
+
61
+ /** 目录条目 → dsh profile。 */
62
+ export function projectQoderModel(entry) {
63
+ if (!entry || typeof entry.key !== 'string' || !entry.key) return null
64
+ const ctxVariants = entry.context_config && typeof entry.context_config === 'object'
65
+ ? Object.values(entry.context_config)
66
+ : []
67
+ const defaultVariant = ctxVariants.find((v) => v?.is_default) ?? ctxVariants[0] ?? null
68
+ const contextWindow = Number.isFinite(defaultVariant?.token_count)
69
+ ? defaultVariant.token_count
70
+ : (Number.isFinite(entry.max_input_tokens) ? entry.max_input_tokens : 128000)
71
+ const reasoningEfforts = qoderReasoningEfforts(entry)
72
+ return {
73
+ id: entry.key,
74
+ name: typeof entry.display_name === 'string' && entry.display_name ? entry.display_name : entry.key,
75
+ contextWindow,
76
+ maxTokens: 32768,
77
+ input: entry.is_vl === true ? ['text', 'image'] : ['text'],
78
+ ...(reasoningEfforts ? { reasoningEfforts } : {}),
79
+ }
80
+ }
81
+
82
+ /**
83
+ * 目录条目 → 上下文变体清单 [{name, tokenCount, isDefault}](context_config
84
+ * 变体表,实测 Qwen3.8-Max:200K 默认/400K/1M)。无变体/全非法 → []——
85
+ * 设置卡据此隐藏该模型的上下文选择。
86
+ */
87
+ export function projectQoderVariants(entry) {
88
+ const ctx = entry?.context_config
89
+ if (!ctx || typeof ctx !== 'object') return []
90
+ return Object.entries(ctx)
91
+ .filter(([, v]) => v && Number.isFinite(v.token_count))
92
+ .map(([name, v]) => ({ name, tokenCount: v.token_count, isDefault: v.is_default === true }))
93
+ }
94
+
95
+ /**
96
+ * 选中变体 → profile.contextWindow 覆盖(未选/变体名已不在目录 → 原样)。
97
+ * 纯函数单点:镜像(syncQoderModelsToDshSettings)与回归断言共用。
98
+ */
99
+ export function applyQoderContextVariant(profile, variantName, variants) {
100
+ if (!variantName || !Array.isArray(variants)) return profile
101
+ const hit = variants.find((v) => v && v.name === variantName && Number.isFinite(v.tokenCount))
102
+ if (!hit) return profile
103
+ return { ...profile, contextWindow: hit.tokenCount }
104
+ }
105
+
106
+ /**
107
+ * 拉取目录 → { profiles, sources, variants, raw }。明文/密文两态自适应。
108
+ * @param {object} cosy createCosyRuntime 实例
109
+ * @param {object} cred { accessToken, machineId, uid }
110
+ * @param {string} inferBaseURL 例 https://gateway.qoder.com.cn
111
+ */
112
+ export async function fetchQoderCatalog(cosy, cred, inferBaseURL) {
113
+ const base = String(inferBaseURL ?? '').replace(/\/+$/, '')
114
+ const req = await cosy.prepareGet(cred, { endpoint: base, path: '/api/v2/model/list?Encode=1' })
115
+ const res = await fetch(req.url, { headers: req.headers })
116
+ const text = await res.text()
117
+ if (!res.ok) {
118
+ throw new Error(`qoder 目录 HTTP ${res.status}:${text.slice(0, 120)}`)
119
+ }
120
+ let body
121
+ try {
122
+ body = JSON.parse(text)
123
+ } catch {
124
+ body = JSON.parse(cosy.decrypt(text))
125
+ }
126
+ const chat = Array.isArray(body?.chat) ? body.chat : []
127
+ const entries = chat.filter((e) => e && e.format === 'openai' && e.enable !== false)
128
+ const profiles = entries.map(projectQoderModel).filter(Boolean)
129
+ if (!profiles.length) throw new Error('qoder 目录为空(chat 数组无可用 openai 条目)')
130
+ // X-Model-Source 头取数(全部实测条目为 "system",但保留逐条目映射以防分化)
131
+ const sources = {}
132
+ for (const e of entries) sources[e.key] = typeof e.source === 'string' && e.source ? e.source : 'system'
133
+ // 上下文变体清单逐模型保留(不进 profiles——镜像块形状不变,见 index.js 镜像纪律)
134
+ const variants = {}
135
+ for (const e of entries) variants[e.key] = projectQoderVariants(e)
136
+ // 展示用档位清单逐模型保留(同 variants:不进 profiles——镜像里的档位表由
137
+ // projectQoderModel 的 reasoningEfforts 决定;这里只服务设置卡 select)。
138
+ const efforts = {}
139
+ for (const e of entries) efforts[e.key] = qoderEffortTiers(e)
140
+ // 原始条目按键索引(网关出站信封的 model_config 需要 is_vl/is_reasoning/
141
+ // max_input_tokens 等未投影字段,2026-09-22 用量归因信封对齐官方客户端)
142
+ const byKey = {}
143
+ for (const e of entries) byKey[e.key] = e
144
+ return { profiles, sources, variants, efforts, entries: byKey, raw: body }
145
+ }