dsh-provider-qoder 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/dsh-provider-qoder.svg?color=blue)](https://www.npmjs.com/package/dsh-provider-qoder)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
5
+ [![DSH Plugin](https://img.shields.io/badge/DSH-Plugin-6f42c1.svg)](https://github.com/mo-n/dsh-provider-qoder)
5
6
  [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com)
6
7
 
7
8
  English | [简体中文](./README_CN.md)
@@ -15,17 +16,18 @@ This project is a community adapter plugin and is not affiliated with Qoder or D
15
16
 
16
17
  - Configure subscription access via Qoder Personal Access Token (PAT).
17
18
  - Supports both Global (`global`) and China (`china`) services.
18
- - Discovers available models for your account, allows selecting enabled models, and displays pricing multipliers and reasoning effort options when reported by the service.
19
- - Supports multimodality, search, and tool calls.
19
+ - Automatically fetches available models for your account, supports selecting enabled models, and displays pricing multipliers and reasoning effort options when reported by the service.
20
+ - Supports multimodality, web search, and tool calls.
20
21
  - View account information, quota, and reset dates in Settings.
21
22
 
22
23
  ## Installation
23
24
 
24
25
  ### Prerequisites
25
26
 
26
- You will need a running DSH Web environment, an active Qoder subscription, and a PAT matching the selected service region. The DSH profile running the plugin must provide a managed credentials service; when configuring via the settings page, the credential storage must also be writable.
27
+ - An active Qoder subscription and a PAT matching the selected service region.
28
+ - The DSH profile running the plugin must provide a managed credentials service; when configuring via the settings page, the credential storage must also be writable.
29
+ - Requirements: DSH `>=0.1.7-rc.2`.
27
30
 
28
- This package supports DSH `>=0.1.7-rc.2 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0`, Cordis `>=4.0.4 <5`, and React `^18.2.0`.
29
31
  ### Install from npm
30
32
 
31
33
  Run in your terminal:
@@ -47,27 +49,14 @@ Open DSH Web, navigate to **Settings → Models**, locate the **Qoder Credential
47
49
 
48
50
  Select the region matching your account, enter your Qoder PAT into the input box labeled **API Key**, and click **Save**.
49
51
 
50
- ### 2. Fetch and Select Models
52
+ Once saved, the plugin automatically fetches the list of supported models for your account, making them immediately available in the DSH model picker.
51
53
 
52
- 1. After saving the PAT and service region, click **Edit** again.
53
- 2. Expand **Custom Settings** and click **Fetch Available Models**.
54
- 3. Select the models you want to enable (keep at least one), then click **Save**.
55
- 4. Select a Qoder model in the DSH model picker to start chatting.
54
+ ### 2. View Account & Manage Models
56
55
 
57
- Fetching models uses the **saved PAT and service region**. If you switch accounts or regions, you need to fetch models again. The initial model catalog is only a candidate reference; actual available models are subject to account query results. Model multipliers, reasoning effort options, and image-input capability are provided by Qoder and may not be available for all models.
58
-
59
- The model picker refreshes Qoder metadata for enabled models when opened, with a five-minute cache after a successful fetch. When settings are writable, refreshed multipliers and capabilities are also synchronized to the saved catalog shown in **Custom Settings**, without enabling additional models. Failed refreshes retain the last known metadata.
56
+ Open **Settings → Qoder** to view account and quota details, configure the web search policy, and adjust enabled models:
60
57
 
61
58
  <p align="center">
62
- <img src="./assets/credentials-settings.png" alt="Qoder credentials and model configuration" width="680" />
63
- </p>
64
-
65
- ### 3. View Account & Quota
66
-
67
- Open **Settings → Qoder** to view account and quota details, and configure the web search policy as needed:
68
-
69
- <p align="center">
70
- <img src="./assets/account-quota.png" alt="Qoder account quota and search settings" width="650" />
59
+ <img src="./assets/account-quota.png" alt="Qoder account quota and model settings" width="650" />
71
60
  </p>
72
61
 
73
62
  ## Upcoming Features
@@ -83,4 +72,6 @@ Open **Settings → Qoder** to view account and quota details, and configure the
83
72
 
84
73
  Please report issues via [GitHub Issues](https://github.com/mo-n/dsh-provider-qoder/issues) with the plugin and DSH versions, selected service region, reproduction steps, and sanitized error messages. Do not submit PATs, short-lived tokens, or personal account information.
85
74
 
86
- This project is licensed under the MIT License.
75
+ ## License
76
+
77
+ This project is licensed under the [MIT License](LICENSE).
package/README_CN.md CHANGED
@@ -5,10 +5,9 @@
5
5
  [![DSH Plugin](https://img.shields.io/badge/DSH-Plugin-6f42c1.svg)](https://github.com/mo-n/dsh-provider-qoder)
6
6
  [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com)
7
7
 
8
-
9
8
  [English](./README.md) | 简体中文
10
9
 
11
- 在 DeepSeek Harness(DSH) 中使用 Qoder 订阅,支持多模态、联网搜索和工具调用,可选择国际版或中国区 Qoder 服务。
10
+ 在 DeepSeek Harness(DSH)中使用 Qoder 订阅,支持多模态、联网搜索和工具调用,可选择国际版或中国区 Qoder 服务。
12
11
 
13
12
  插件负责认证和模型通信,工具执行、工作区操作及权限管理由 DSH 负责。
14
13
  本项目是社区适配插件,与 Qoder、DeepSeek 官方均无关联。
@@ -17,7 +16,7 @@
17
16
 
18
17
  - 通过 Qoder Personal Access Token(PAT,个人访问令牌)配置订阅访问。
19
18
  - 支持国际版(`global`)和中国区(`china`)服务。
20
- - 获取账号可用模型,选择启用的模型,并在服务返回相关信息时显示价格倍率和推理强度选项。
19
+ - 自动拉取账号可用模型,支持选择启用的模型,并在服务支持时显示价格倍率和推理强度选项。
21
20
  - 支持多模态、搜索以及工具调用。
22
21
  - 在设置中查看账号信息、额度和重置时间。
23
22
 
@@ -25,9 +24,9 @@
25
24
 
26
25
  ### 使用前提
27
26
 
28
- 你需要可运行的 DSH Web 环境、可用的 Qoder 订阅,以及与所选服务区域匹配的 PAT。运行插件的 DSH profile 必须提供托管凭据服务;通过设置页面配置时,凭据存储还需可写。
29
-
30
- 本包支持 DSH `>=0.1.7-rc.2 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0`、Cordis `>=4.0.4 <5` 和 React `^18.2.0`。
27
+ - 拥有可用的 Qoder 订阅,以及与所选服务区域匹配的 PAT。
28
+ - 运行插件的 DSH profile 必须提供托管凭据服务;通过设置页面配置时,凭据存储还需可写。
29
+ - 环境要求:DSH `>=0.1.7-rc.2`。
31
30
 
32
31
  ### 从 npm 安装
33
32
 
@@ -50,31 +49,18 @@ dsh plugin --profile web add dsh-provider-qoder
50
49
 
51
50
  选择与账号一致的区域,将 Qoder PAT 填入界面标为 **API 密钥** 的输入框,然后点击 **保存**。
52
51
 
53
- ### 2. 获取并选择模型
54
-
55
- 1. 保存 PAT 和服务区域后,再次点击 **编辑**。
56
- 2. 展开 **自定义设置**,点击 **获取可用模型**。
57
- 3. 选择需要启用的模型,至少保留一个,然后点击 **保存**。
58
- 4. 在 DSH 的模型选择器中选择 Qoder 模型,开始对话。
52
+ 保存后,插件会拉取账号支持的模型列表,即可在 DSH 模型选择器中选用。
59
53
 
60
- 获取模型使用的是**已保存的 PAT 和服务区域**。更换账号或区域后,需要重新获取模型。初始模型目录仅供候选参考,实际可用模型以账号查询结果为准。模型倍率、推理强度选项及图片输入能力由 Qoder 提供,并非所有模型都有这些信息。
54
+ ### 2. 查看账号与设置模型
61
55
 
62
- 展开模型选择器时会自动刷新已启用 Qoder 模型的元数据,成功获取后缓存 5 分钟。设置可写时,更新后的倍率和能力信息也会同步到**自定义设置**显示的已保存目录,不会自动启用其他模型。刷新失败时保留最近一次成功获取的信息。
56
+ 打开 **设置 → Qoder** 查看账号额度,并可切换联网搜索策略与调整启用的模型:
63
57
 
64
58
  <p align="center">
65
- <img src="./assets/credentials-settings.png" alt="Qoder 凭据与模型配置" width="680" />
59
+ <img src="./assets/account-quota_CN.png" alt="Qoder 账号额度与模型设置" width="650" />
66
60
  </p>
67
61
 
68
- ### 3. 查看账号与额度
62
+ ## 后续计划
69
63
 
70
- 打开 **设置 → Qoder** 查看账号额度,并可按需切换联网搜索策略:
71
-
72
- <p align="center">
73
- <img src="./assets/account-quota.png" alt="Qoder 账号额度与搜索设置" width="650" />
74
- </p>
75
-
76
-
77
- ## 后续计划功能
78
64
  - [ ] 请求限流排队机制
79
65
  - [ ] 接入 Qoder 的 Protobuf 协议
80
66
 
@@ -86,4 +72,6 @@ dsh plugin --profile web add dsh-provider-qoder
86
72
 
87
73
  请通过 [GitHub Issues](https://github.com/mo-n/dsh-provider-qoder/issues) 提交问题,附上插件和 DSH 版本、所选服务区域、复现步骤及脱敏后的错误信息。不要提交 PAT、短期令牌或个人账号信息。
88
74
 
89
- 本项目采用 MIT 许可证。
75
+ ## 许可证
76
+
77
+ 本项目基于 [MIT License](LICENSE) 开源。
package/lib/client.js CHANGED
@@ -69,6 +69,29 @@ window.__ModuleLoader__.load({
69
69
  tokenCount
70
70
  };
71
71
  }
72
+ /** Clone a model catalog entry, detaching any contextOptions object. */
73
+ function cloneCatalogModel(model) {
74
+ return {
75
+ ...model,
76
+ ...model.contextOptions !== void 0 ? { contextOptions: { ...model.contextOptions } } : {}
77
+ };
78
+ }
79
+ /** Carry subscriber context choices only while the refreshed catalog supports them. */
80
+ function mergeAdvertisedContext(remembered, advertised, updated) {
81
+ const merged = { ...updated };
82
+ delete merged.contextOptions;
83
+ delete merged.contextTier;
84
+ if (advertised.contextOptions !== void 0) merged.contextOptions = { ...advertised.contextOptions };
85
+ const tier = selectedContextTier({
86
+ contextTier: remembered.contextTier,
87
+ contextOptions: merged.contextOptions
88
+ });
89
+ if (tier !== void 0) {
90
+ merged.contextTier = tier.key;
91
+ merged.contextWindow = tier.tokenCount;
92
+ } else if (advertised.contextWindow !== void 0) merged.contextWindow = remembered.contextTier !== void 0 ? advertised.contextWindow : Math.min(remembered.contextWindow ?? advertised.contextWindow, advertised.contextWindow);
93
+ return merged;
94
+ }
72
95
  const defaultMaxTokens = 32768;
73
96
  const defaultModels = [
74
97
  {
@@ -120,6 +143,31 @@ window.__ModuleLoader__.load({
120
143
  }
121
144
  ];
122
145
  //#endregion
146
+ //#region src/client/credential-operations.ts
147
+ function regionOf(value) {
148
+ if (typeof value !== "object" || value === null) return "global";
149
+ return value.region === "china" ? "china" : "global";
150
+ }
151
+ function modelsOf(section, region) {
152
+ const scoped = section?.modelsByRegion?.[region];
153
+ return Array.isArray(scoped) ? scoped : defaultModels.map((model) => ({ ...model }));
154
+ }
155
+ function reconcileQoderModels(known, discovered) {
156
+ const availableIds = new Set(discovered.map((model) => model.id));
157
+ const unavailable = known.filter((model) => !availableIds.has(model.id)).map(cloneCatalogModel);
158
+ const previous = new Map(known.map((model) => [model.id, model]));
159
+ const reconciled = discovered.map((model) => {
160
+ const remembered = previous.get(model.id);
161
+ if (remembered === void 0) return cloneCatalogModel(model);
162
+ return mergeAdvertisedContext(remembered, model, model);
163
+ });
164
+ return {
165
+ catalog: [...reconciled, ...unavailable],
166
+ selected: reconciled,
167
+ unavailableIds: new Set(unavailable.map((model) => model.id))
168
+ };
169
+ }
170
+ //#endregion
123
171
  //#region src/client/locales.ts
124
172
  const zh = {
125
173
  nav: "Qoder",
@@ -165,7 +213,6 @@ window.__ModuleLoader__.load({
165
213
  contextTierDefaultSuffix: "(默认)",
166
214
  contextTierHint: "选择该模型使用的上下文档位;更大的档位会同时提升 DSH 的上下文预算并改变 Qoder 请求使用的档位。",
167
215
  contextSelectFailed: "上下文切换失败,请重试。",
168
- contextDefaultFailed: "当前会话已切换,但新会话默认值保存失败。",
169
216
  contextSelectTitle: "选择上下文大小",
170
217
  contextSelectMenuHeader: "上下文容量",
171
218
  accountTitle: "Qoder",
@@ -237,7 +284,6 @@ window.__ModuleLoader__.load({
237
284
  contextTierDefaultSuffix: " (default)",
238
285
  contextTierHint: "Select the context tier for this model. A larger tier raises the DSH context budget and changes the tier the Qoder request uses.",
239
286
  contextSelectFailed: "Could not change the context window. Please retry.",
240
- contextDefaultFailed: "Session updated, but the default for new sessions could not be saved.",
241
287
  contextSelectTitle: "Select context window size",
242
288
  contextSelectMenuHeader: "Context Capacity",
243
289
  accountTitle: "Qoder",
@@ -316,49 +362,6 @@ window.__ModuleLoader__.load({
316
362
  }
317
363
  }
318
364
  //#endregion
319
- //#region src/client/credential-operations.ts
320
- function reconcileQoderModels(known, discovered) {
321
- const availableIds = new Set(discovered.map((model) => model.id));
322
- const unavailable = known.filter((model) => !availableIds.has(model.id)).map((model) => ({
323
- ...model,
324
- ...model.contextOptions !== void 0 ? { contextOptions: { ...model.contextOptions } } : {}
325
- }));
326
- const previous = new Map(known.map((model) => [model.id, model]));
327
- const reconciled = discovered.map((model) => {
328
- const remembered = previous.get(model.id);
329
- if (remembered === void 0) return {
330
- ...model,
331
- ...model.contextOptions !== void 0 ? { contextOptions: { ...model.contextOptions } } : {}
332
- };
333
- const carried = selectedContextTier({
334
- contextTier: remembered.contextTier,
335
- contextOptions: model.contextOptions ?? remembered.contextOptions
336
- });
337
- const options = model.contextOptions ?? remembered.contextOptions;
338
- const clonedOptions = options !== void 0 ? { contextOptions: { ...options } } : {};
339
- if (carried !== void 0) return {
340
- ...model,
341
- ...clonedOptions,
342
- contextTier: carried.key,
343
- contextWindow: carried.tokenCount
344
- };
345
- const budget = remembered.contextWindow;
346
- return budget === void 0 || model.contextWindow === void 0 ? {
347
- ...model,
348
- ...clonedOptions
349
- } : {
350
- ...model,
351
- ...clonedOptions,
352
- contextWindow: Math.min(budget, model.contextWindow)
353
- };
354
- });
355
- return {
356
- catalog: [...reconciled, ...unavailable],
357
- selected: reconciled,
358
- unavailableIds: new Set(unavailable.map((model) => model.id))
359
- };
360
- }
361
- //#endregion
362
365
  //#region \0dsh-css:/home/runner/work/dsh-provider-qoder/dsh-provider-qoder/src/client/QoderCredentialCard.module.css.mjs
363
366
  const css$1 = ".SRqrhW_credential{color:var(--dsw-alias-label-primary);border-radius:10px;margin-top:12px}.SRqrhW_credential p{margin:8px 0 0}.SRqrhW_credentialEditor{box-sizing:border-box;border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-xl,12px);flex-direction:column;gap:12px;padding:12px 14px;display:flex}.SRqrhW_credentialRowHead{align-items:center;gap:10px;display:flex}.SRqrhW_credentialIdentity{align-items:center;gap:6px;min-width:0;display:inline-flex}.SRqrhW_credentialName{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:22px}.SRqrhW_credentialDot{box-sizing:border-box;border-radius:50%;flex:none;width:8px;height:8px;display:inline-block}.SRqrhW_credentialDotConfigured{background:var(--dsw-alias-state-success-primary)}.SRqrhW_credentialDotMissing{background:var(--dsw-alias-state-error-primary)}.SRqrhW_regionBadge{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary);border-radius:4px;align-items:center;padding:1px 6px;font-size:11px;font-weight:500;line-height:16px;display:inline-flex}.SRqrhW_regionSelector{flex-direction:column;gap:8px;margin-bottom:14px;display:flex}.SRqrhW_regionGroup{gap:8px;display:flex}.SRqrhW_regionOption{border:1px solid var(--dsw-alias-border-l2);cursor:pointer;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);user-select:none;border-radius:8px;align-items:center;gap:6px;padding:6px 12px;font-size:12px;line-height:18px;transition:all .12s;display:inline-flex}.SRqrhW_regionOption:hover:not(.SRqrhW_regionOptionDisabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.SRqrhW_credential button.SRqrhW_regionOptionActive{border-color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-interactive-bg-selected);color:var(--dsw-alias-brand-primary);font-weight:500}.SRqrhW_regionOptionDisabled{cursor:not-allowed;opacity:.5}.SRqrhW_editButton{box-sizing:border-box;font:inherit;background:0 0;margin-left:auto;font-size:12px;line-height:18px;border:1px solid var(--dsw-alias-border-l2)!important;border-radius:14px!important;height:28px!important;padding:0 10px!important}.SRqrhW_editButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-solid)}.SRqrhW_editor{background:var(--dsw-alias-bg-module-platform);border-radius:12px;padding:14px 16px}.SRqrhW_customized{border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-xl,12px);background:var(--dsw-alias-bg-module-platform);margin-top:12px;transition:all .12s;overflow:hidden}.SRqrhW_customizedSummary{box-sizing:border-box;border-radius:var(--dsw-radius-xl,12px);cursor:pointer;width:100%;color:var(--dsw-alias-label-primary);user-select:none;align-items:center;gap:8px;margin:0;padding:12px 14px;font-size:13px;font-weight:600;line-height:18px;list-style:none;transition:background .12s;display:flex}.SRqrhW_customizedSummary:hover{background:var(--dsw-alias-interactive-bg-hover)}.SRqrhW_modelSummaryBadge{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-tertiary);border-radius:4px;align-items:center;padding:1px 6px;font-size:11px;font-weight:400;line-height:16px;display:inline-flex}.SRqrhW_customizedSummary::-webkit-details-marker{display:none}.SRqrhW_customizedSummary:before{content:\"\";border-bottom:1.5px solid;border-right:1.5px solid;width:5px;height:5px;transition:transform .12s;transform:rotate(-45deg)translate(-1px,-1px)}.SRqrhW_customized[open]>.SRqrhW_customizedSummary:before{transform:rotate(45deg)translate(-1px,-1px)}.SRqrhW_customized[open]>.SRqrhW_customizedSummary{border-bottom:.5px solid var(--dsw-alias-border-l2);border-bottom-right-radius:0;border-bottom-left-radius:0}.SRqrhW_customizedBody{padding:12px 14px 14px}.SRqrhW_modelCatalog{flex-direction:column;gap:10px;display:flex}.SRqrhW_modelList{flex-direction:column;gap:10px;max-height:360px;padding-right:4px;display:flex;overflow-y:auto}.SRqrhW_modelList::-webkit-scrollbar{width:4px}.SRqrhW_modelList::-webkit-scrollbar-thumb{background:var(--dsw-alias-border-l3);border-radius:2px}.SRqrhW_modelCatalogHead{justify-content:space-between;align-items:flex-start;gap:12px;display:flex}.SRqrhW_modelCatalogHeadCompact{justify-content:flex-end;align-items:center;gap:12px;display:flex}.SRqrhW_modelCatalogTitle{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;line-height:18px}.SRqrhW_modelCatalogMeta{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;margin:2px 0 0!important}.SRqrhW_linkButton{box-sizing:border-box;font:inherit;background:0 0;font-size:12px;line-height:18px;border:none!important;border-radius:14px!important;height:28px!important;padding:0 10px!important}.SRqrhW_linkButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.SRqrhW_modelChoice{border-radius:6px;flex-direction:column;gap:4px;padding:6px 8px;font-size:13px;display:flex}.SRqrhW_modelChoice:hover{background:var(--dsw-alias-interactive-bg-hover)}.SRqrhW_modelChoiceHead{cursor:pointer;grid-template-columns:auto minmax(0,1fr) auto auto auto;align-items:center;gap:8px;display:grid}.SRqrhW_modelTier{align-items:center;gap:8px;padding-left:24px;display:flex}.SRqrhW_modelTierLabel{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:18px}.SRqrhW_modelTierSelect{box-sizing:border-box;background:var(--dsw-alias-bg-layer-1);height:24px;color:inherit;font:inherit;cursor:pointer;border:0;border-radius:6px;padding:0 6px;font-size:12px}.SRqrhW_modelTierHint{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:18px;margin:0!important}.SRqrhW_modelChoiceName{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.SRqrhW_modelChoice code{color:var(--dsw-alias-label-tertiary);font-size:11px}.SRqrhW_modelRate{min-width:34px;color:var(--dsw-alias-brand-primary);text-align:right;font-size:12px;font-weight:600}.SRqrhW_modelUnavailable{cursor:not-allowed;opacity:.55}.SRqrhW_modelBadge{color:var(--dsw-alias-state-error-primary);font-size:11px}@media (prefers-reduced-motion:reduce){.SRqrhW_customizedSummary:before{transition:none}}.SRqrhW_head{justify-content:space-between;align-items:center;gap:12px;margin-bottom:20px;display:flex}.SRqrhW_heading{margin:0;font-size:18px;font-weight:600}.SRqrhW_status{color:var(--dsw-alias-label-secondary);font-size:12px}.SRqrhW_configured,.SRqrhW_success{color:var(--dsw-alias-state-success-primary)}.SRqrhW_label{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;line-height:18px;display:block}.SRqrhW_input{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);width:100%;height:32px;color:var(--dsw-alias-label-primary);border-radius:8px;outline:none;margin-top:6px;padding:0 10px}.SRqrhW_input:focus{border-color:var(--dsw-alias-brand-primary)}.SRqrhW_input::placeholder{color:var(--dsw-alias-label-dimmed)}.SRqrhW_input:disabled{cursor:not-allowed;opacity:.4}.SRqrhW_hint{color:var(--dsw-alias-label-secondary);font-size:12px}.SRqrhW_actions{justify-content:flex-end;gap:8px;margin-top:14px;display:flex}.SRqrhW_credential button{cursor:pointer;height:32px;color:var(--dsw-alias-label-primary);border:1px solid #0000;border-radius:16px;padding:0 12px}.SRqrhW_credential button:disabled{cursor:not-allowed;opacity:.4}.SRqrhW_primary{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)!important}.SRqrhW_primary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}.SRqrhW_secondary{background:0 0;border-color:var(--dsw-alias-border-l2)!important}.SRqrhW_secondary:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.SRqrhW_danger{background:0 0;border-color:var(--dsw-alias-state-error-primary)!important;color:var(--dsw-alias-state-error-primary)!important}.SRqrhW_danger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger)}.SRqrhW_error{color:var(--dsw-alias-state-error-primary);font-size:12px}.SRqrhW_success{font-size:12px}.SRqrhW_accountSection{border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-xl,12px);background:var(--dsw-alias-bg-module-platform);flex-direction:column;gap:10px;margin-top:12px;padding:12px 14px;display:flex}.SRqrhW_accountHead{justify-content:space-between;align-items:center;gap:8px;display:flex}.SRqrhW_userInfo{align-items:center;gap:10px;min-width:0;display:flex}.SRqrhW_avatar{background:var(--dsw-alias-brand-primary);width:32px;height:32px;color:var(--dsw-alias-label-primary-foreground);text-transform:uppercase;border-radius:50%;flex-shrink:0;justify-content:center;align-items:center;font-size:14px;font-weight:600;display:flex}.SRqrhW_userDetails{flex-direction:column;gap:2px;min-width:0;display:flex}.SRqrhW_userName{color:var(--dsw-alias-label-primary);text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:600;overflow:hidden}.SRqrhW_userEmail{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:11px;overflow:hidden}.SRqrhW_refreshBtn{border:1px solid var(--dsw-alias-border-l2)!important;height:26px!important;color:var(--dsw-alias-label-secondary)!important;background:0 0!important;border-radius:13px!important;padding:0 8px!important;font-size:12px!important}.SRqrhW_refreshBtn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)!important}.SRqrhW_quotaBlock{border-top:1px solid var(--dsw-alias-border-l2);flex-direction:column;gap:6px;padding-top:8px;display:flex}.SRqrhW_quotaHeader{justify-content:space-between;align-items:center;gap:8px;display:flex}.SRqrhW_quotaLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600}.SRqrhW_quotaTop{justify-content:space-between;align-items:baseline;gap:8px;font-size:12px;display:flex}.SRqrhW_quotaTop strong{color:var(--dsw-alias-brand-primary);font-size:13px}.SRqrhW_quotaDescription{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;margin:0!important}.SRqrhW_quotaProgress{background:var(--dsw-alias-border-l3);width:100%;height:6px;accent-color:var(--dsw-alias-brand-primary);-webkit-appearance:none;appearance:none;border:0;border-radius:999px;overflow:hidden}.SRqrhW_quotaProgress::-webkit-progress-bar{background:var(--dsw-alias-border-l3);border-radius:999px}.SRqrhW_quotaProgress::-webkit-progress-value{background:var(--dsw-alias-brand-primary);border-radius:999px}.SRqrhW_quotaProgress::-moz-progress-bar{background:var(--dsw-alias-brand-primary);border-radius:999px}.SRqrhW_quotaMeta{color:var(--dsw-alias-label-tertiary);justify-content:space-between;align-items:center;gap:8px;font-size:11px;display:flex}.SRqrhW_accountLoadingText,.SRqrhW_accountEmptyText{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px}.SRqrhW_userNameRow{align-items:center;gap:8px;display:flex}.SRqrhW_planBadge{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-brand-primary);border-radius:4px;align-items:center;padding:1px 6px;font-size:11px;font-weight:600;line-height:16px;display:inline-flex}.SRqrhW_orgTag{color:var(--dsw-alias-label-tertiary);font-size:11px}.SRqrhW_suspendedBanner{background:var(--dsw-alias-state-error-subtle,#ff4d4f1a);color:var(--dsw-alias-state-error-primary);border-radius:6px;align-items:center;gap:6px;margin-bottom:8px;padding:6px 10px;font-size:12px;display:flex}.SRqrhW_searchModeSection{border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-xl,12px);background:var(--dsw-alias-bg-module-platform);flex-direction:column;gap:10px;margin-top:12px;padding:12px 14px;display:flex}.SRqrhW_searchModeHintText{color:var(--dsw-alias-label-secondary);margin:4px 0 0;font-size:12px;line-height:18px}";
364
367
  const tagId$1 = "dsh-provider-qoder/QoderCredentialCard.module.css";
@@ -610,18 +613,6 @@ window.__ModuleLoader__.load({
610
613
  minute: "2-digit"
611
614
  });
612
615
  }
613
- function modelsOf$1(value, selectedRegion) {
614
- if (typeof value !== "object" || value === null) return defaultModels.map((model) => ({ ...model }));
615
- const section = value;
616
- const region = selectedRegion ?? (section.region === "china" ? "china" : "global");
617
- const scoped = section.modelsByRegion?.[region];
618
- if (Array.isArray(scoped)) return scoped;
619
- return defaultModels.map((model) => ({ ...model }));
620
- }
621
- function regionOf$1(value) {
622
- if (typeof value !== "object" || value === null) return "global";
623
- return value.region === "china" ? "china" : "global";
624
- }
625
616
  /**
626
617
  * Settings card for the Qoder subscription: credential state, subscriber
627
618
  * profile, and every quota block the provider reports.
@@ -928,8 +919,8 @@ window.__ModuleLoader__.load({
928
919
  ]
929
920
  });
930
921
  };
931
- const currentRegion = regionOf$1(modelSnapshot.value);
932
- const currentModels = modelsOf$1(modelSnapshot.value, currentRegion);
922
+ const currentRegion = regionOf(modelSnapshot.value);
923
+ const currentModels = modelsOf(modelSnapshot.value, currentRegion);
933
924
  const canModifyModels = modelSnapshot.status === "ready" && modelSnapshot.writable;
934
925
  const handleModelsChange = async (models) => {
935
926
  if (!canModifyModels) return;
@@ -998,10 +989,6 @@ window.__ModuleLoader__.load({
998
989
  }
999
990
  //#endregion
1000
991
  //#region src/client/QoderCredentialCard.tsx
1001
- function regionOf(value) {
1002
- if (typeof value !== "object" || value === null) return "global";
1003
- return value.region === "china" ? "china" : "global";
1004
- }
1005
992
  function QoderCredentialCard({ operations, t }) {
1006
993
  const [state, setState] = (0, react.useState)({ status: "loading" });
1007
994
  const [editing, setEditing] = (0, react.useState)(false);
@@ -1240,39 +1227,20 @@ window.__ModuleLoader__.load({
1240
1227
  "qoder-subscription"
1241
1228
  ].includes(provider ?? "");
1242
1229
  }
1243
- function modelsOf(section, region, _legacyRegion) {
1244
- const scoped = section?.modelsByRegion?.[region];
1245
- if (scoped !== void 0) return scoped;
1246
- return defaultModels.map((model) => ({ ...model }));
1247
- }
1248
1230
  function historicalContextWindow(current, lastUsed, contextWindow) {
1249
1231
  return current.provider === lastUsed?.provider && current.model === lastUsed?.model ? contextWindow : void 0;
1250
1232
  }
1251
- /** A failed session write must not change defaults; a failed default write is a partial success. */
1252
- async function saveContextSelection(operations, sessionId, region, modelId, tierKey, legacyRegion) {
1233
+ /** Composer selections belong to the current session; model defaults live in settings. */
1234
+ async function saveContextSelection(operations, sessionId, region, modelId, tierKey) {
1253
1235
  try {
1254
- if (!await operations.setSessionTier?.(sessionId, modelId, tierKey, region)) return "session-failed";
1236
+ return await operations.setSessionTier?.(sessionId, modelId, tierKey, region) ? "saved" : "session-failed";
1255
1237
  } catch {
1256
1238
  return "session-failed";
1257
1239
  }
1258
- try {
1259
- const models = modelsOf(operations.getModelSnapshot().value, region, legacyRegion);
1260
- const model = models.find((candidate) => candidate.id === modelId);
1261
- const tier = model && contextTiersOf(model).find((candidate) => candidate.key === tierKey);
1262
- if (!tier) return "default-failed";
1263
- const updated = models.map((candidate) => candidate.id === modelId ? {
1264
- ...candidate,
1265
- contextTier: tierKey,
1266
- contextWindow: tier.tokenCount
1267
- } : candidate);
1268
- return await operations.storeModels(region, updated) ? "saved" : "default-failed";
1269
- } catch {
1270
- return "default-failed";
1271
- }
1272
1240
  }
1273
1241
  //#endregion
1274
1242
  //#region \0dsh-css:/home/runner/work/dsh-provider-qoder/dsh-provider-qoder/src/client/QoderContextSelect.module.css.mjs
1275
- const css = ".hiDrJa_root{align-items:center;display:inline-flex;position:relative}.hiDrJa_trigger{height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;user-select:none;background:0 0;border:none;border-radius:24px;outline:none;align-items:center;gap:4px;padding:0 8px;font-size:13px;font-weight:500;line-height:20px;transition:background .12s,color .12s;display:flex}.hiDrJa_trigger:hover,.hiDrJa_triggerActive{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.hiDrJa_trigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3)}.hiDrJa_icon{color:var(--dsw-alias-label-caption);justify-content:center;align-items:center;display:inline-flex}.hiDrJa_label{white-space:nowrap}.hiDrJa_chevron{color:var(--dsw-alias-label-caption);align-items:center;transition:transform .12s;display:inline-flex}.hiDrJa_chevronOpen{transform:rotate(180deg)}.hiDrJa_menu{z-index:1100;background:var(--dsw-specific-menu);border:1px solid var(--dsw-alias-border-l1);min-width:180px;box-shadow:var(--dsw-elevation-prominent);color:var(--dsw-alias-label-primary);border-radius:14px;flex-direction:column;padding:4px;animation:.12s hiDrJa_fadeIn;display:flex;position:absolute;bottom:calc(100% + 8px);right:0}@keyframes hiDrJa_fadeIn{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.hiDrJa_menuHeader{text-transform:uppercase;letter-spacing:.5px;color:var(--dsw-alias-label-tertiary);border-bottom:1px solid var(--dsw-alias-border-l1);margin-bottom:4px;padding:6px 10px 4px;font-size:11px;font-weight:600}.hiDrJa_option{box-sizing:border-box;width:100%;color:inherit;cursor:pointer;text-align:left;background:0 0;border:none;border-radius:8px;justify-content:space-between;align-items:center;gap:8px;padding:6px 10px;font-size:13px;line-height:20px;transition:background .1s;display:flex}.hiDrJa_option:hover{background:var(--dsw-alias-interactive-bg-hover)}.hiDrJa_optionSelected{color:var(--dsw-alias-state-brand);font-weight:600}.hiDrJa_optionInfo{align-items:center;gap:6px;display:flex}.hiDrJa_tag{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-caption);border-radius:4px;padding:1px 5px;font-size:11px;font-weight:500}.hiDrJa_check{color:var(--dsw-alias-state-brand);align-items:center;font-size:12px;display:inline-flex}.hiDrJa_error{color:var(--dsw-alias-label-primary);max-width:240px;font-size:12px}";
1243
+ const css = ".hiDrJa_root{align-items:center;display:inline-flex;position:relative}.hiDrJa_trigger{height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;user-select:none;background:0 0;border:none;border-radius:24px;outline:none;align-items:center;gap:4px;padding:0 8px;font-size:13px;font-weight:500;line-height:20px;transition:background .12s,color .12s;display:flex}.hiDrJa_trigger:hover,.hiDrJa_triggerActive{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.hiDrJa_trigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3)}.hiDrJa_icon{color:var(--dsw-alias-label-caption);justify-content:center;align-items:center;display:inline-flex}.hiDrJa_label{white-space:nowrap}.hiDrJa_chevron{color:var(--dsw-alias-label-caption);align-items:center;transition:transform .12s;display:inline-flex}.hiDrJa_chevronOpen{transform:rotate(180deg)}.hiDrJa_menu{z-index:1100;background:var(--dsw-menu-surface-fill);min-width:180px;backdrop-filter:var(--dsw-menu-backdrop-filter);border:1px solid var(--dsw-alias-border-l1);box-shadow:var(--dsw-elevation-prominent);color:var(--dsw-alias-label-primary);border-radius:14px;flex-direction:column;padding:4px;animation:.12s hiDrJa_fadeIn;display:flex;position:absolute;bottom:calc(100% + 8px);right:0}@keyframes hiDrJa_fadeIn{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.hiDrJa_menuHeader{text-transform:uppercase;letter-spacing:.5px;color:var(--dsw-alias-label-tertiary);border-bottom:1px solid var(--dsw-alias-border-l1);margin-bottom:4px;padding:6px 10px 4px;font-size:11px;font-weight:600}.hiDrJa_option{box-sizing:border-box;width:100%;color:inherit;cursor:pointer;text-align:left;background:0 0;border:none;border-radius:8px;justify-content:space-between;align-items:center;gap:8px;padding:6px 10px;font-size:13px;line-height:20px;transition:background .1s;display:flex}.hiDrJa_option:hover{background:var(--dsw-alias-interactive-bg-hover)}.hiDrJa_optionSelected{color:var(--dsw-alias-state-brand);font-weight:600}.hiDrJa_optionInfo{align-items:center;gap:6px;display:flex}.hiDrJa_tag{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-caption);border-radius:4px;padding:1px 5px;font-size:11px;font-weight:500}.hiDrJa_check{color:var(--dsw-alias-state-brand);align-items:center;font-size:12px;display:inline-flex}.hiDrJa_error{color:var(--dsw-alias-label-primary);max-width:240px;font-size:12px}";
1276
1244
  const tagId = "dsh-provider-qoder/QoderContextSelect.module.css";
1277
1245
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
1278
1246
  const tag = document.createElement("style");
@@ -1308,7 +1276,6 @@ window.__ModuleLoader__.load({
1308
1276
  const [pending, setPending] = (0, react.useState)(false);
1309
1277
  const [error, setError] = (0, react.useState)(null);
1310
1278
  const saving = (0, react.useRef)(false);
1311
- const legacyRegion = (0, react.useRef)(void 0);
1312
1279
  const rootRef = (0, react.useRef)(null);
1313
1280
  const directoryState = (0, react.useSyncExternalStore)((fn) => directory ? directory.subscribe(fn) : () => () => {}, () => directory ? directory.getSnapshot() : null);
1314
1281
  const modelSnapshot = (0, react.useSyncExternalStore)((fn) => operations ? operations.subscribeModels(fn) : () => () => {}, () => operations ? operations.getModelSnapshot() : null);
@@ -1336,9 +1303,8 @@ window.__ModuleLoader__.load({
1336
1303
  model: projectedCurrent.model
1337
1304
  } : null);
1338
1305
  if (!current) return null;
1339
- const region = modelSnapshot?.value?.region === "china" ? "china" : "global";
1340
- if (legacyRegion.current === void 0 && modelSnapshot?.value) legacyRegion.current = region;
1341
- const models = modelsOf(modelSnapshot?.value, region, legacyRegion.current ?? region);
1306
+ const region = regionOf(modelSnapshot?.value);
1307
+ const models = modelsOf(modelSnapshot?.value, region);
1342
1308
  const currentModelId = current.model.includes("/") ? current.model.split("/")[1] : current.model;
1343
1309
  const currentProviderId = current.provider ?? (current.model.includes("/") ? current.model.split("/")[0] : void 0);
1344
1310
  const activeModel = models.find((m) => m.id === currentModelId || m.id === current.model);
@@ -1362,15 +1328,11 @@ window.__ModuleLoader__.load({
1362
1328
  setPending(true);
1363
1329
  setError(null);
1364
1330
  try {
1365
- const result = await saveContextSelection(operations, sessionId, region, activeModel.id, tierKey, legacyRegion.current ?? region);
1366
- if (result !== "session-failed") setManualTiers((prev) => ({
1331
+ if (await saveContextSelection(operations, sessionId, region, activeModel.id, tierKey) === "saved") setManualTiers((prev) => ({
1367
1332
  ...prev,
1368
1333
  [sessionKey]: tierKey
1369
1334
  }));
1370
- if (result !== "saved") setError({
1371
- key: sessionKey,
1372
- message: result === "session-failed" ? "contextSelectFailed" : "contextDefaultFailed"
1373
- });
1335
+ else setError({ key: sessionKey });
1374
1336
  } finally {
1375
1337
  saving.current = false;
1376
1338
  setPending(false);
@@ -1425,7 +1387,7 @@ window.__ModuleLoader__.load({
1425
1387
  error && error.key === sessionKey && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1426
1388
  role: "alert",
1427
1389
  className: QoderContextSelect_module_css_default.error,
1428
- children: t ? t(error.message) : error.message
1390
+ children: t ? t("contextSelectFailed") : "contextSelectFailed"
1429
1391
  }),
1430
1392
  open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1431
1393
  className: QoderContextSelect_module_css_default.menu,
@@ -1634,10 +1596,7 @@ window.__ModuleLoader__.load({
1634
1596
  storeModels: async (region, models) => {
1635
1597
  try {
1636
1598
  const current = modelScope.getSnapshot().value;
1637
- const clonedModels = models.map((m) => ({
1638
- ...m,
1639
- ...m.contextOptions !== void 0 ? { contextOptions: { ...m.contextOptions } } : {}
1640
- }));
1599
+ const clonedModels = models.map(cloneCatalogModel);
1641
1600
  return await modelScope.set("modelsByRegion", {
1642
1601
  ...current?.modelsByRegion,
1643
1602
  [region]: clonedModels
@@ -1709,25 +1668,31 @@ window.__ModuleLoader__.load({
1709
1668
  activeLocale
1710
1669
  })
1711
1670
  }, QoderCredentialCard));
1712
- ctx.slots.inject("conversation.input.right", () => ctx.slots.register({
1713
- name: "conversation.input.right",
1714
- id: "qoder-context-select",
1715
- order: 10,
1716
- inject: (sessionId) => {
1717
- let directory = void 0;
1718
- try {
1719
- const modelDirectories = ctx.get("modelDirectories");
1720
- directory = sessionId && modelDirectories ? modelDirectories.directoryFor(sessionId)?.store : void 0;
1721
- } catch {}
1722
- return {
1723
- sessionId,
1724
- directory,
1725
- operations,
1726
- t,
1727
- activeLocale
1728
- };
1729
- }
1730
- }, QoderContextSelect));
1671
+ ctx.inject([
1672
+ "modelDirectories",
1673
+ "sessions",
1674
+ "remote.session"
1675
+ ], (scope) => {
1676
+ scope.slots.inject("conversation.input.right", () => scope.slots.register({
1677
+ name: "conversation.input.right",
1678
+ id: "qoder-context-select",
1679
+ order: 10,
1680
+ inject: (sessionId) => {
1681
+ let directory = void 0;
1682
+ try {
1683
+ const modelDirectories = scope.get("modelDirectories");
1684
+ directory = sessionId && modelDirectories ? modelDirectories.directoryFor(sessionId)?.store : void 0;
1685
+ } catch {}
1686
+ return {
1687
+ sessionId,
1688
+ directory,
1689
+ operations,
1690
+ t,
1691
+ activeLocale
1692
+ };
1693
+ }
1694
+ }, QoderContextSelect));
1695
+ });
1731
1696
  }
1732
1697
  //#endregion
1733
1698
  exports.apply = apply;
package/lib/index.d.ts CHANGED
@@ -155,46 +155,6 @@ interface QoderAccountInfo {
155
155
  updatedAt: string;
156
156
  }
157
157
  //#endregion
158
- //#region src/qoder/transport/logging.d.ts
159
- /** Bounded, redacted diagnostics internal to the Qoder transport. */
160
- interface QoderLogger {
161
- debug?(message: string, ...details: unknown[]): void;
162
- warn?(message: string, ...details: unknown[]): void;
163
- error?(message: string, ...details: unknown[]): void;
164
- }
165
- //#endregion
166
- //#region src/qoder/transport/wire/cosy.d.ts
167
- interface CosyCredentials {
168
- userID: string;
169
- authToken: string;
170
- name: string;
171
- email: string;
172
- machineID?: string;
173
- }
174
- //#endregion
175
- //#region src/qoder/transport/search.d.ts
176
- interface QoderSearchClientOptions {
177
- fetch?: typeof fetch;
178
- region?: QoderRegion;
179
- logger?: QoderLogger;
180
- timeoutMs?: number;
181
- resolveCredentials: (signal?: AbortSignal) => Promise<CosyCredentials>;
182
- refreshCredentials?: (signal?: AbortSignal) => Promise<CosyCredentials>;
183
- now?: () => number;
184
- }
185
- declare class QoderSearchClient {
186
- private readonly fetchImpl;
187
- private readonly region;
188
- private readonly logger?;
189
- private readonly timeoutMs;
190
- private readonly resolveCredentials;
191
- private readonly refreshCredentials?;
192
- private readonly now;
193
- constructor(options: QoderSearchClientOptions);
194
- search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult>;
195
- private attempt;
196
- }
197
- //#endregion
198
158
  //#region src/qoder/transport/index.d.ts
199
159
  interface QoderTransport {
200
160
  stream(options: GenerateOptions, model?: QoderCatalogModel): AsyncIterable<StreamChunk>;
@@ -210,16 +170,14 @@ interface QoderTransport {
210
170
  declare const QODER_SEARCH_PROVIDER_ID = "qoder";
211
171
  interface QoderSearchProviderOptions {
212
172
  ctx: Context;
213
- resolveTransport?: () => QoderTransport;
214
- searchClient?: QoderSearchClient;
173
+ resolveTransport: () => QoderTransport;
215
174
  getWebSearchMode: () => QoderWebSearchMode;
216
175
  fallbackProvider?: WebSearchProvider;
217
176
  }
218
177
  declare class QoderSearchProvider implements WebSearchProvider {
219
178
  readonly id = "qoder";
220
179
  private readonly ctx;
221
- private readonly resolveTransport?;
222
- private readonly searchClient?;
180
+ private readonly resolveTransport;
223
181
  private readonly getWebSearchMode;
224
182
  private readonly fallbackProvider?;
225
183
  constructor(options: QoderSearchProviderOptions);
package/lib/index.js CHANGED
@@ -32,6 +32,9 @@ const reasoningEffortOrder = /* @__PURE__ */ new Map([
32
32
  function positiveNumber(value) {
33
33
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
34
34
  }
35
+ function nonNegativeNumber(value) {
36
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
37
+ }
35
38
  function contextOptionsOf(value) {
36
39
  if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
37
40
  const options = {};
@@ -42,8 +45,8 @@ function contextOptionsOf(value) {
42
45
  const isDefault = typeof entry.is_default === "boolean" ? entry.is_default : void 0;
43
46
  if (tokenCount === void 0) continue;
44
47
  options[key] = {
45
- ...tokenCount === void 0 ? {} : { tokenCount },
46
- ...isDefault === void 0 ? {} : { isDefault }
48
+ tokenCount,
49
+ ...isDefault !== void 0 ? { isDefault } : {}
47
50
  };
48
51
  }
49
52
  if (Object.keys(options).length === 0) return void 0;
@@ -96,9 +99,9 @@ function normalizeQoderModels(payload, onConflict) {
96
99
  const contextWindow = (defaultOptions.length === 1 ? defaultOptions[0].tokenCount : void 0) ?? positiveNumber(raw.max_input_tokens) ?? 18e4;
97
100
  const maxContextWindow = Math.max(positiveNumber(raw.max_input_tokens) ?? 0, contextWindow, ...Object.values(contextOptions ?? {}).map((option) => option.tokenCount ?? 0));
98
101
  const isReasoning = thinkingDefault(raw.thinking_config, raw.is_reasoning === true, onConflict);
99
- const priceFactor = typeof raw.price_factor === "number" && Number.isFinite(raw.price_factor) && raw.price_factor >= 0 ? raw.price_factor : typeof raw.priceFactor === "number" && Number.isFinite(raw.priceFactor) && raw.priceFactor >= 0 ? raw.priceFactor : void 0;
100
- const rawOriginalPrice = typeof raw.original_price_factor === "number" && Number.isFinite(raw.original_price_factor) && raw.original_price_factor >= 0 ? raw.original_price_factor : typeof raw.originPriceFactor === "number" && Number.isFinite(raw.originPriceFactor) && raw.originPriceFactor >= 0 ? raw.originPriceFactor : void 0;
101
- const promotionPrice = typeof raw.promotion === "object" && raw.promotion !== null && raw.promotion.active === true && typeof raw.promotion.before_promotion_price_factor === "number" ? raw.promotion.before_promotion_price_factor : void 0;
102
+ const priceFactor = nonNegativeNumber(raw.price_factor) ?? nonNegativeNumber(raw.priceFactor);
103
+ const rawOriginalPrice = nonNegativeNumber(raw.original_price_factor) ?? nonNegativeNumber(raw.originPriceFactor);
104
+ const promotionPrice = typeof raw.promotion === "object" && raw.promotion !== null && raw.promotion.active === true ? nonNegativeNumber(raw.promotion.before_promotion_price_factor) : void 0;
102
105
  const originalPriceFactor = rawOriginalPrice ?? promotionPrice;
103
106
  const isFree = raw.is_free === true || raw.isFree === true || Array.isArray(raw.tags) && raw.tags.includes("limited_time_free") || priceFactor === 0;
104
107
  const reasoning = reasoningEffortsOf(raw.thinking_config);
@@ -187,28 +190,40 @@ function selectedContextTier(model) {
187
190
  function effectiveContextWindow(model) {
188
191
  return selectedContextTier(model)?.tokenCount ?? model.contextWindow;
189
192
  }
193
+ /** Clone a model catalog entry, detaching any contextOptions object. */
194
+ function cloneCatalogModel(model) {
195
+ return {
196
+ ...model,
197
+ ...model.contextOptions !== void 0 ? { contextOptions: { ...model.contextOptions } } : {}
198
+ };
199
+ }
200
+ /** Carry subscriber context choices only while the refreshed catalog supports them. */
201
+ function mergeAdvertisedContext(remembered, advertised, updated) {
202
+ const merged = { ...updated };
203
+ delete merged.contextOptions;
204
+ delete merged.contextTier;
205
+ if (advertised.contextOptions !== void 0) merged.contextOptions = { ...advertised.contextOptions };
206
+ const tier = selectedContextTier({
207
+ contextTier: remembered.contextTier,
208
+ contextOptions: merged.contextOptions
209
+ });
210
+ if (tier !== void 0) {
211
+ merged.contextTier = tier.key;
212
+ merged.contextWindow = tier.tokenCount;
213
+ } else if (advertised.contextWindow !== void 0) merged.contextWindow = remembered.contextTier !== void 0 ? advertised.contextWindow : Math.min(remembered.contextWindow ?? advertised.contextWindow, advertised.contextWindow);
214
+ return merged;
215
+ }
190
216
  function mergeQoderDiscoveryMetadata(configured, discovered) {
191
217
  const catalog = new Map(discovered.map((model) => [model.id, model]));
192
218
  return configured.map((model) => {
193
219
  const advertised = catalog.get(model.id);
194
- if (advertised === void 0) return {
195
- ...model,
196
- ...model.contextOptions !== void 0 ? { contextOptions: { ...model.contextOptions } } : {}
197
- };
220
+ if (advertised === void 0) return cloneCatalogModel(model);
198
221
  const merged = { ...model };
199
- for (const key of discoveredMetadataKeys) delete merged[key];
200
- for (const key of discoveredMetadataKeys) if (advertised[key] !== void 0) {
201
- const val = advertised[key];
202
- Object.assign(merged, { [key]: key === "contextOptions" && typeof val === "object" && val !== null ? { ...val } : val });
203
- }
204
- const tier = selectedContextTier(merged);
205
- if (tier !== void 0) {
206
- merged.contextWindow = tier.tokenCount;
207
- return merged;
208
- }
209
- delete merged.contextTier;
210
- if (advertised.contextWindow !== void 0) merged.contextWindow = Math.min(model.contextWindow ?? advertised.contextWindow, advertised.contextWindow);
211
- return merged;
222
+ for (const key of discoveredMetadataKeys) if (key !== "contextOptions") {
223
+ delete merged[key];
224
+ if (advertised[key] !== void 0) Object.assign(merged, { [key]: advertised[key] });
225
+ }
226
+ return mergeAdvertisedContext(model, advertised, merged);
212
227
  });
213
228
  }
214
229
  function hasSameQoderDiscoveryMetadata(left, right) {
@@ -3233,11 +3248,7 @@ var QoderAdapter = class extends LlmAdapter {
3233
3248
  const base = this.effectiveModels().find((candidate) => candidate.id === modelId);
3234
3249
  if (base === void 0) return void 0;
3235
3250
  if (sessionId) {
3236
- const manualTierKey = this.sessionTiers.get(JSON.stringify([
3237
- this.region(),
3238
- sessionId,
3239
- modelId
3240
- ]));
3251
+ const manualTierKey = this.getSessionTier(sessionId, modelId);
3241
3252
  const reqCtx = this.sessions?.get(sessionId)?.requestContext?.();
3242
3253
  const tier = resolveContextTier(base, manualTierKey, !this.historyRegionChanged && reqCtx?.provider === this.providerId && reqCtx.model === modelId ? reqCtx.contextWindow : void 0);
3243
3254
  if (tier) return {
@@ -3283,8 +3294,7 @@ var QoderAdapter = class extends LlmAdapter {
3283
3294
  */
3284
3295
  publishDiscoveredModels(transport, models) {
3285
3296
  try {
3286
- const pending = this.onModelsDiscovered?.(transport, models);
3287
- if (pending !== void 0 && pending !== null && typeof pending.catch === "function") pending.catch(() => {});
3297
+ Promise.resolve(this.onModelsDiscovered?.(transport, models)).catch(() => {});
3288
3298
  } catch {}
3289
3299
  }
3290
3300
  effectiveModels() {
@@ -3360,17 +3370,23 @@ var QoderAdapter = class extends LlmAdapter {
3360
3370
  * @module dsh-provider-qoder/dsh/search-provider
3361
3371
  */
3362
3372
  const QODER_SEARCH_PROVIDER_ID = "qoder";
3373
+ function initiatingModelProvider(ctx) {
3374
+ const agent = (ctx.get("agents") ?? ctx.agents)?.currentInitiator?.();
3375
+ const defaultModelService = ctx.get("agentDefaultModel");
3376
+ return agent?.options?.provider ?? defaultModelService?.get?.()?.provider;
3377
+ }
3378
+ function shouldUseQoderSearch(mode, provider) {
3379
+ return mode === "always" || mode === "auto" && provider === "dsh-provider-qoder";
3380
+ }
3363
3381
  var QoderSearchProvider = class {
3364
3382
  id = QODER_SEARCH_PROVIDER_ID;
3365
3383
  ctx;
3366
3384
  resolveTransport;
3367
- searchClient;
3368
3385
  getWebSearchMode;
3369
3386
  fallbackProvider;
3370
3387
  constructor(options) {
3371
3388
  this.ctx = options.ctx;
3372
3389
  this.resolveTransport = options.resolveTransport;
3373
- this.searchClient = options.searchClient;
3374
3390
  this.getWebSearchMode = options.getWebSearchMode;
3375
3391
  this.fallbackProvider = options.fallbackProvider;
3376
3392
  }
@@ -3380,13 +3396,15 @@ var QoderSearchProvider = class {
3380
3396
  async search(request, signal) {
3381
3397
  const mode = this.getWebSearchMode();
3382
3398
  if (mode === "disabled") throw new WebError("Qoder web search is disabled in settings.", "WEB_PROVIDER_UNAVAILABLE");
3383
- const agent = (this.ctx.get("agents") ?? this.ctx.agents)?.currentInitiator?.();
3384
- const defaultModelService = this.ctx.get("agentDefaultModel");
3385
- const providerRoute = agent?.options?.provider ?? defaultModelService?.get?.()?.provider;
3386
- if (mode === "always" || providerRoute === "dsh-provider-qoder") {
3387
- if (this.resolveTransport !== void 0) return this.resolveTransport().searchWeb(request, signal);
3388
- if (this.searchClient !== void 0) return this.searchClient.search(request, signal);
3389
- throw new WebError("No Qoder transport configured for web search.", "WEB_PROVIDER_ERROR");
3399
+ const providerRoute = initiatingModelProvider(this.ctx);
3400
+ if (shouldUseQoderSearch(mode, providerRoute)) {
3401
+ const result = await this.resolveTransport().searchWeb(request, signal);
3402
+ if (request.maxResults === void 0 || result.sources.length <= request.maxResults) return result;
3403
+ return {
3404
+ ...result,
3405
+ sources: result.sources.slice(0, request.maxResults),
3406
+ truncated: true
3407
+ };
3390
3408
  }
3391
3409
  const fallback = this.resolveFallback();
3392
3410
  if (fallback && fallback.available()) return fallback.search(request, signal);
@@ -3554,6 +3572,26 @@ function publicError(code, message, details = { issues: [] }) {
3554
3572
  }
3555
3573
  };
3556
3574
  }
3575
+ async function executeRpc(operation, task, signal, logger) {
3576
+ try {
3577
+ return {
3578
+ ok: true,
3579
+ value: await task()
3580
+ };
3581
+ } catch (error) {
3582
+ if (signal.aborted || error instanceof QoderLlmError && error.code === "ABORTED") {
3583
+ logger?.debug?.(`[Qoder RPC] ${operation} was aborted`);
3584
+ return publicError("ABORTED", "Request aborted");
3585
+ }
3586
+ let code = "INTERNAL";
3587
+ if (error instanceof QoderLlmError) if (error.code === "MISSING_CREDENTIAL" || error.code === "NO_CREDENTIALS") code = "NO_CREDENTIALS";
3588
+ else if (error.code === "AUTH") code = "UNAUTHENTICATED";
3589
+ else if (error.code === "TIMEOUT") code = "TIMEOUT";
3590
+ else code = "UPSTREAM_ERROR";
3591
+ logger?.error?.(`[Qoder RPC] Failed to ${operation}`, logError(error));
3592
+ return publicError(code, error instanceof Error ? error.message : `Failed to ${operation}`);
3593
+ }
3594
+ }
3557
3595
  function apply(ctx, input = {}) {
3558
3596
  const config = readConfig(input);
3559
3597
  const logger = ctx.logger;
@@ -3699,27 +3737,7 @@ function apply(ctx, input = {}) {
3699
3737
  const handler = async (endpoint, payload, signal) => {
3700
3738
  if (!isQoderRpcEndpoint(endpoint)) return publicError("UNKNOWN_ENDPOINT", `Unknown endpoint: ${endpoint}`);
3701
3739
  if (signal.aborted) return publicError("ABORTED", "Request aborted");
3702
- const executeRpc = async (operation, task) => {
3703
- try {
3704
- return {
3705
- ok: true,
3706
- value: await task()
3707
- };
3708
- } catch (error) {
3709
- if (signal.aborted || error instanceof QoderLlmError && error.code === "ABORTED") {
3710
- logger?.debug?.(`[Qoder RPC] ${operation} was aborted`);
3711
- return publicError("ABORTED", "Request aborted");
3712
- }
3713
- let code = "INTERNAL";
3714
- if (error instanceof QoderLlmError) if (error.code === "MISSING_CREDENTIAL" || error.code === "NO_CREDENTIALS") code = "NO_CREDENTIALS";
3715
- else if (error.code === "AUTH") code = "UNAUTHENTICATED";
3716
- else if (error.code === "TIMEOUT") code = "TIMEOUT";
3717
- else code = "UPSTREAM_ERROR";
3718
- logger?.error?.(`[Qoder RPC] Failed to ${operation}`, logError(error));
3719
- return publicError(code, error instanceof Error ? error.message : `Failed to ${operation}`);
3720
- }
3721
- };
3722
- if (endpoint === "models") return await executeRpc("discover Qoder models", () => discoverModels(signal));
3740
+ if (endpoint === "models") return await executeRpc("discover Qoder models", () => discoverModels(signal), signal, logger);
3723
3741
  if (endpoint === "sessionTier") {
3724
3742
  const data = payload;
3725
3743
  if (typeof data?.sessionId === "string" && typeof data?.modelId === "string" && typeof data?.tierKey === "string" && (data.region === "global" || data.region === "china")) {
@@ -3727,7 +3745,7 @@ function apply(ctx, input = {}) {
3727
3745
  return await executeRpc("select session context tier", async () => {
3728
3746
  adapter.setSessionTier(sessionId, modelId, tierKey, region);
3729
3747
  return { success: true };
3730
- });
3748
+ }, signal, logger);
3731
3749
  }
3732
3750
  return publicError("INTERNAL", "Invalid sessionTier payload");
3733
3751
  }
@@ -3736,7 +3754,7 @@ function apply(ctx, input = {}) {
3736
3754
  const outcome = await executeRpc("load Qoder account", () => activeTransport.readAccount({
3737
3755
  force,
3738
3756
  signal
3739
- }));
3757
+ }), signal, logger);
3740
3758
  if (outcome.ok) logger?.debug?.("[Qoder RPC] Subscriber account resolved");
3741
3759
  return outcome;
3742
3760
  };
@@ -3753,19 +3771,15 @@ function apply(ctx, input = {}) {
3753
3771
  });
3754
3772
  webCtx.web.registerSearchProvider(searchProvider);
3755
3773
  if (typeof webCtx.web.search === "function") {
3756
- const originalSearch = webCtx.web.search.bind(webCtx.web);
3774
+ const originalSearch = webCtx.web.search;
3757
3775
  webCtx.effect(() => {
3758
- webCtx.web.search = async (request, signal) => {
3759
- const mode = resolveConfig().webSearchMode ?? "auto";
3760
- if (mode === "disabled") return originalSearch(request, signal);
3761
- const agent = (ctx.get("agents") ?? ctx.agents)?.currentInitiator?.();
3762
- const defaultModelService = ctx.get("agentDefaultModel");
3763
- const isQoderActive = (agent?.options?.provider ?? defaultModelService?.get?.()?.provider) === providerQoder;
3764
- if (mode === "always" || isQoderActive) return searchProvider.search(request, signal);
3765
- return originalSearch(request, signal);
3776
+ const routedSearch = async (request, signal) => {
3777
+ if (shouldUseQoderSearch(resolveConfig().webSearchMode ?? "auto", initiatingModelProvider(ctx))) return searchProvider.search(request, signal);
3778
+ return originalSearch.call(webCtx.web, request, signal);
3766
3779
  };
3780
+ webCtx.web.search = routedSearch;
3767
3781
  return () => {
3768
- webCtx.web.search = originalSearch;
3782
+ if (webCtx.web.search === routedSearch) webCtx.web.search = originalSearch;
3769
3783
  };
3770
3784
  }, "provider-qoder: transparent web search router");
3771
3785
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-provider-qoder",
3
3
  "description": "Global Qoder subscription adapter for the DeepSeek Harness LLM seam",
4
- "version": "0.4.1",
4
+ "version": "0.4.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -18,6 +18,10 @@
18
18
  "qoder"
19
19
  ],
20
20
  "type": "module",
21
+ "engines": {
22
+ "dsh": ">=0.1.7-rc.2",
23
+ "node": "^22.19.0 || >=24.0.0"
24
+ },
21
25
  "scripts": {
22
26
  "dev": "tsdown --config tsdown.config.mjs --watch",
23
27
  "typecheck": "tsc --noEmit",
@@ -65,23 +69,21 @@
65
69
  },
66
70
  "license": "MIT",
67
71
  "peerDependencies": {
68
- "@deepseek-ai/cordis": ">=4.0.4 <5",
69
- "@deepseek-ai/dsh-api-remotes": ">=0.1.7-rc.2 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
70
- "@deepseek-ai/dsh-attachment": ">=0.1.7-rc.2 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
71
- "@deepseek-ai/dsh-client-connection": ">=0.1.7-rc.2 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
72
- "@deepseek-ai/dsh-client-locale": ">=0.1.7-rc.2 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
73
- "@deepseek-ai/dsh-client-ui-settings": ">=0.1.7-rc.2 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
74
- "@deepseek-ai/dsh-client-ui-slots": ">=0.1.7-rc.2 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
75
- "@deepseek-ai/dsh-credentials": ">=0.1.7-rc.2 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
76
- "@deepseek-ai/dsh-invariants": ">=0.1.7-rc.2 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
77
- "@deepseek-ai/dsh-llm": ">=0.1.7-rc.2 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
78
- "@deepseek-ai/dsh-settings": ">=0.1.7-rc.2 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
79
- "@deepseek-ai/dsh-web": ">=0.1.7-rc.2 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
72
+ "@deepseek-ai/cordis": ">=4.0.4",
73
+ "@deepseek-ai/dsh-api-remotes": ">=0.1.7-rc.2",
74
+ "@deepseek-ai/dsh-attachment": ">=0.1.7-rc.2",
75
+ "@deepseek-ai/dsh-client-connection": ">=0.1.7-rc.2",
76
+ "@deepseek-ai/dsh-client-locale": ">=0.1.7-rc.2",
77
+ "@deepseek-ai/dsh-client-ui-settings": ">=0.1.7-rc.2",
78
+ "@deepseek-ai/dsh-client-ui-slots": ">=0.1.7-rc.2",
79
+ "@deepseek-ai/dsh-credentials": ">=0.1.7-rc.2",
80
+ "@deepseek-ai/dsh-invariants": ">=0.1.7-rc.2",
81
+ "@deepseek-ai/dsh-llm": ">=0.1.7-rc.2",
82
+ "@deepseek-ai/dsh-settings": ">=0.1.7-rc.2",
83
+ "@deepseek-ai/dsh-web": ">=0.1.7-rc.2",
84
+ "@deepseek-ai/schemastery": "~3.18.4",
80
85
  "react": "^18.2.0"
81
86
  },
82
- "dependencies": {
83
- "@deepseek-ai/schemastery": "3.18.4"
84
- },
85
87
  "devDependencies": {
86
88
  "@deepseek-ai/cordis": "4.0.4",
87
89
  "@deepseek-ai/cordis-plugin-loader": "1.0.5",
@@ -101,6 +103,7 @@
101
103
  "@deepseek-ai/dsh-session": "0.1.7-rc.2",
102
104
  "@deepseek-ai/dsh-settings": "0.1.7-rc.2",
103
105
  "@deepseek-ai/dsh-web": "0.1.7-rc.2",
106
+ "@deepseek-ai/schemastery": "3.18.4",
104
107
  "@types/node": "24.3.0",
105
108
  "@types/react": "18.3.1",
106
109
  "lightningcss": "^1.33.0",