dsh-provider-qoder 0.4.0 → 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/lib/index.js CHANGED
@@ -16,6 +16,8 @@ const discoveredMetadataKeys = [
16
16
  "reasoningEfforts",
17
17
  "defaultReasoningEffort",
18
18
  "priceFactor",
19
+ "originalPriceFactor",
20
+ "isFree",
19
21
  "contextOptions",
20
22
  "maxContextWindow",
21
23
  "supportsImages"
@@ -30,6 +32,9 @@ const reasoningEffortOrder = /* @__PURE__ */ new Map([
30
32
  function positiveNumber(value) {
31
33
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
32
34
  }
35
+ function nonNegativeNumber(value) {
36
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
37
+ }
33
38
  function contextOptionsOf(value) {
34
39
  if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
35
40
  const options = {};
@@ -40,8 +45,8 @@ function contextOptionsOf(value) {
40
45
  const isDefault = typeof entry.is_default === "boolean" ? entry.is_default : void 0;
41
46
  if (tokenCount === void 0) continue;
42
47
  options[key] = {
43
- ...tokenCount === void 0 ? {} : { tokenCount },
44
- ...isDefault === void 0 ? {} : { isDefault }
48
+ tokenCount,
49
+ ...isDefault !== void 0 ? { isDefault } : {}
45
50
  };
46
51
  }
47
52
  if (Object.keys(options).length === 0) return void 0;
@@ -94,7 +99,11 @@ function normalizeQoderModels(payload, onConflict) {
94
99
  const contextWindow = (defaultOptions.length === 1 ? defaultOptions[0].tokenCount : void 0) ?? positiveNumber(raw.max_input_tokens) ?? 18e4;
95
100
  const maxContextWindow = Math.max(positiveNumber(raw.max_input_tokens) ?? 0, contextWindow, ...Object.values(contextOptions ?? {}).map((option) => option.tokenCount ?? 0));
96
101
  const isReasoning = thinkingDefault(raw.thinking_config, raw.is_reasoning === true, onConflict);
97
- const priceFactor = typeof raw.price_factor === "number" && Number.isFinite(raw.price_factor) && raw.price_factor >= 0 ? raw.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;
105
+ const originalPriceFactor = rawOriginalPrice ?? promotionPrice;
106
+ const isFree = raw.is_free === true || raw.isFree === true || Array.isArray(raw.tags) && raw.tags.includes("limited_time_free") || priceFactor === 0;
98
107
  const reasoning = reasoningEffortsOf(raw.thinking_config);
99
108
  models.push({
100
109
  id,
@@ -109,21 +118,112 @@ function normalizeQoderModels(payload, onConflict) {
109
118
  ...reasoning.efforts === void 0 ? {} : { reasoningEfforts: reasoning.efforts },
110
119
  ...reasoning.defaultEffort === void 0 ? {} : { defaultReasoningEffort: reasoning.defaultEffort },
111
120
  ...priceFactor === void 0 ? {} : { priceFactor },
121
+ ...originalPriceFactor === void 0 ? {} : { originalPriceFactor },
122
+ ...isFree ? { isFree: true } : {},
112
123
  ...contextOptions === void 0 ? {} : { contextOptions }
113
124
  });
114
125
  }
115
126
  return models;
116
127
  }
128
+ /**
129
+ * Provider-advertised context tiers of one model, smallest first. A tier without
130
+ * a usable capacity cannot be selected and is dropped.
131
+ */
132
+ function contextTiersOf(model) {
133
+ const options = model.contextOptions;
134
+ if (options === void 0) return [];
135
+ return Object.entries(options).flatMap(([key, value]) => {
136
+ const tokenCount = value.tokenCount;
137
+ return typeof tokenCount === "number" && Number.isFinite(tokenCount) && tokenCount > 0 ? [{
138
+ key,
139
+ tokenCount,
140
+ isDefault: value.isDefault === true
141
+ }] : [];
142
+ }).sort((left, right) => left.tokenCount - right.tokenCount);
143
+ }
144
+ /** Resolve a usable selection consistently for the composer and adapter. */
145
+ function resolveContextTier(model, manualTier, historicalWindow) {
146
+ const tiers = contextTiersOf(model);
147
+ return tiers.find((tier) => tier.key === manualTier) ?? tiers.find((tier) => tier.tokenCount === historicalWindow) ?? tiers.find((tier) => tier.key === model.contextTier) ?? tiers.find((tier) => tier.tokenCount === model.contextWindow) ?? (model.contextWindow === void 0 ? tiers.find((tier) => tier.isDefault) : void 0);
148
+ }
149
+ /** Format a price factor number to string (e.g. 1 -> '1', 1.6 -> '1.6', 0.5 -> '0.5', 1.25 -> '1.25'). */
150
+ function formatRateFactor(value) {
151
+ const rounded = Number(value.toFixed(2));
152
+ return String(rounded);
153
+ }
154
+ /**
155
+ * Format model price factor / rate string.
156
+ * - Free models or price factor 0: '免费' or 'Free'
157
+ * - Promotional models: '0.5x(原 1x)' or '0.5x (was 1x)'
158
+ * - Standard models: '1x'
159
+ */
160
+ function formatModelRate(model, isZh = true) {
161
+ if (model.isFree || model.priceFactor === 0) return isZh ? "免费" : "Free";
162
+ if (model.priceFactor === void 0) return void 0;
163
+ const current = `${formatRateFactor(model.priceFactor)}x`;
164
+ if (model.originalPriceFactor !== void 0 && model.originalPriceFactor > model.priceFactor) {
165
+ const orig = `${formatRateFactor(model.originalPriceFactor)}x`;
166
+ return isZh ? `${current}(原 ${orig})` : `${current} (was ${orig})`;
167
+ }
168
+ return current;
169
+ }
170
+ /**
171
+ * The provider-advertised context tier the subscriber selected, when the
172
+ * provider still advertises that tier with a usable capacity.
173
+ */
174
+ function selectedContextTier(model) {
175
+ const key = model.contextTier;
176
+ if (typeof key !== "string" || key.length === 0) return void 0;
177
+ const tokenCount = positiveNumber(model.contextOptions?.[key]?.tokenCount);
178
+ return tokenCount === void 0 ? void 0 : {
179
+ key,
180
+ tokenCount
181
+ };
182
+ }
183
+ /**
184
+ * Effective input budget carried to DSH for one catalog entry.
185
+ *
186
+ * A selected tier is an explicit subscriber decision, so it outranks the
187
+ * provider default tier and may widen the budget beyond it; without a selection
188
+ * the entry keeps its stored budget.
189
+ */
190
+ function effectiveContextWindow(model) {
191
+ return selectedContextTier(model)?.tokenCount ?? model.contextWindow;
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
+ }
117
216
  function mergeQoderDiscoveryMetadata(configured, discovered) {
118
217
  const catalog = new Map(discovered.map((model) => [model.id, model]));
119
218
  return configured.map((model) => {
120
219
  const advertised = catalog.get(model.id);
121
- if (advertised === void 0) return { ...model };
220
+ if (advertised === void 0) return cloneCatalogModel(model);
122
221
  const merged = { ...model };
123
- if (advertised.contextWindow !== void 0) merged.contextWindow = Math.min(model.contextWindow ?? advertised.contextWindow, advertised.contextWindow);
124
- for (const key of discoveredMetadataKeys) delete merged[key];
125
- for (const key of discoveredMetadataKeys) if (advertised[key] !== void 0) Object.assign(merged, { [key]: advertised[key] });
126
- 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);
127
227
  });
128
228
  }
129
229
  function hasSameQoderDiscoveryMetadata(left, right) {
@@ -198,8 +298,6 @@ const qoderRegionEndpoints = {
198
298
  };
199
299
  /** Signed path of the center image upload route; it never carries an `/algo` prefix. */
200
300
  const qoderImageUploadPath = "/api/v2/image/upload";
201
- qoderRegionEndpoints.global.baseUrl;
202
- qoderRegionEndpoints.global.openApiUrl;
203
301
  function resolveQoderEndpoints(region = "global") {
204
302
  return qoderRegionEndpoints[region] ?? qoderRegionEndpoints.global;
205
303
  }
@@ -1000,11 +1098,9 @@ async function validateAndTranslateMessages(messages, systemPrompt, attachments,
1000
1098
  }
1001
1099
  if (toolResults.length > 0 && !hasImage && text.trim().length === 0) continue;
1002
1100
  const wireRole = toolResults.length > 0 ? "user" : message.role;
1003
- const wireToolCallId = wireRole === "tool" ? consumeToolCallId(message, void 0, lastAssistantToolCallIds, output.length) : void 0;
1004
1101
  output.push({
1005
1102
  role: wireRole,
1006
- content: hasImage ? userContent.filter((part) => part !== void 0) : text,
1007
- ...wireToolCallId ? { tool_call_id: wireToolCallId } : {}
1103
+ content: hasImage ? userContent.filter((part) => part !== void 0) : text
1008
1104
  });
1009
1105
  }
1010
1106
  return output;
@@ -1345,11 +1441,13 @@ async function buildQoderRequestBody(options, userId, translatedMessages, model,
1345
1441
  const maxTokens = Math.min(options.maxTokens ?? modelMaxTokens, modelMaxTokens);
1346
1442
  const isReasoning = options.reasoningEffort !== void 0 || (model?.isReasoning ?? false);
1347
1443
  const tools = translateTools(options.tools);
1444
+ const selectedTier = model === void 0 ? void 0 : selectedContextTier(model);
1348
1445
  const defaultContexts = Object.values(model?.contextOptions ?? {}).filter((option) => option.isDefault === true && typeof option.tokenCount === "number" && Number.isFinite(option.tokenCount) && option.tokenCount > 0);
1349
- const contextConfig = model?.contextOptions === void 0 || defaultContexts.length !== 1 ? void 0 : Object.fromEntries(Object.entries(model.contextOptions).map(([key, value]) => [key, {
1446
+ const contextConfig = model?.contextOptions === void 0 || selectedTier === void 0 && defaultContexts.length !== 1 ? void 0 : Object.fromEntries(Object.entries(model.contextOptions).map(([key, value]) => [key, {
1350
1447
  ...value.tokenCount === void 0 ? {} : { token_count: value.tokenCount },
1351
- ...value.isDefault === void 0 ? {} : { is_default: value.isDefault }
1448
+ ...selectedTier === void 0 ? value.isDefault === void 0 ? {} : { is_default: value.isDefault } : { is_default: key === selectedTier.key }
1352
1449
  }]));
1450
+ const effectiveBudget = selectedTier?.tokenCount ?? model?.contextWindow;
1353
1451
  let lastUserText = "";
1354
1452
  for (let index = messages.length - 1; index >= 0; index--) if (messages[index].role === "user") {
1355
1453
  lastUserText = wireMessageText(messages[index]);
@@ -1383,6 +1481,7 @@ async function buildQoderRequestBody(options, userId, translatedMessages, model,
1383
1481
  tools,
1384
1482
  parameters: {
1385
1483
  max_tokens: maxTokens,
1484
+ ...effectiveBudget === void 0 ? {} : { context_length: effectiveBudget },
1386
1485
  ...options.reasoningEffort === void 0 ? {} : { reasoning_effort: String(options.reasoningEffort) }
1387
1486
  },
1388
1487
  chat_context: {
@@ -2066,6 +2165,18 @@ function qoderEncodeBody(plaintext) {
2066
2165
  }
2067
2166
  return out;
2068
2167
  }
2168
+ //#endregion
2169
+ //#region src/qoder/transport/search.ts
2170
+ /**
2171
+ * Web search transport client communicating with the Qoder center service.
2172
+ *
2173
+ * Dispatches query requests to the center-hosted `oneSearch` route using the
2174
+ * subscriber's COSY credentials, handling payload serialization, signing,
2175
+ * 401/403 credential refresh retry, and mapping to DSH WebSearchResult.
2176
+ *
2177
+ * @module dsh-provider-qoder/qoder/transport/search
2178
+ */
2179
+ const defaultSearchTimeoutMs = 3e4;
2069
2180
  function searchAborted(signal, fallback) {
2070
2181
  return new WebError("Web search was cancelled.", "WEB_ABORTED", { cause: signal?.aborted === true ? signal.reason : fallback });
2071
2182
  }
@@ -2084,7 +2195,7 @@ var QoderSearchClient = class {
2084
2195
  this.fetchImpl = options.fetch ?? globalThis.fetch;
2085
2196
  this.region = options.region ?? "global";
2086
2197
  this.logger = options.logger;
2087
- this.timeoutMs = options.timeoutMs ?? 3e4;
2198
+ this.timeoutMs = options.timeoutMs ?? defaultSearchTimeoutMs;
2088
2199
  this.resolveCredentials = options.resolveCredentials;
2089
2200
  this.refreshCredentials = options.refreshCredentials;
2090
2201
  this.now = options.now ?? Date.now;
@@ -2384,6 +2495,7 @@ var QoderThinkingParser = class {
2384
2495
  //#region src/qoder/transport/wire/sse.ts
2385
2496
  /** Decode Qoder's outer SSE envelope and inner model chunks. */
2386
2497
  const doneMarker = "[DONE]";
2498
+ const defaultMaxSseBufferChars = 2 * 1024 * 1024;
2387
2499
  function malformed(message) {
2388
2500
  return new QoderLlmError(message, "MALFORMED_RESPONSE");
2389
2501
  }
@@ -2486,7 +2598,7 @@ async function* parseQoderSse(stream, options = {}) {
2486
2598
  let terminalKind = "stop";
2487
2599
  let sawFinishReason = false;
2488
2600
  let sawContent = false;
2489
- const maxBufferChars = options.maxBufferChars ?? 2097152;
2601
+ const maxBufferChars = options.maxBufferChars ?? defaultMaxSseBufferChars;
2490
2602
  const closeTextual = () => {
2491
2603
  if (activeTextual === void 0) return [];
2492
2604
  const state = activeTextual;
@@ -2750,6 +2862,16 @@ async function* streamQoderChat(options, model, credentials, messages, dependenc
2750
2862
  }, dependencies.streamIdleTimeoutMs);
2751
2863
  };
2752
2864
  try {
2865
+ const markedTiers = Object.entries(request.model_config.context_config ?? {}).filter(([, tier]) => tier.is_default === true);
2866
+ dependencies.logger?.debug?.("[Qoder Stream] Request context", redactLogValue({
2867
+ sessionId: options.sessionId ?? null,
2868
+ requestId: request.request_id,
2869
+ model: request.model_config.key,
2870
+ region: dependencies.region,
2871
+ purpose: options.purpose ?? "chat",
2872
+ contextTier: markedTiers.length === 1 ? markedTiers[0][0] : null,
2873
+ context_length: request.parameters.context_length ?? null
2874
+ }));
2753
2875
  const response = await dependencies.fetch(chatUrl, {
2754
2876
  method: "POST",
2755
2877
  headers: {
@@ -3006,6 +3128,7 @@ const catalogModel = z.object({
3006
3128
  description: z.string(),
3007
3129
  contextWindow: z.number().step(1).min(1),
3008
3130
  maxContextWindow: z.number().step(1).min(1),
3131
+ contextTier: z.string(),
3009
3132
  maxTokens: z.number().step(1).min(1),
3010
3133
  source: z.string(),
3011
3134
  isReasoning: z.boolean(),
@@ -3017,6 +3140,8 @@ const catalogModel = z.object({
3017
3140
  })),
3018
3141
  defaultReasoningEffort: z.string(),
3019
3142
  priceFactor: z.number().min(0),
3143
+ originalPriceFactor: z.number().min(0),
3144
+ isFree: z.boolean(),
3020
3145
  supportsImages: z.boolean(),
3021
3146
  contextOptions: z.dict(z.object({
3022
3147
  tokenCount: z.number().step(1).min(1),
@@ -3036,7 +3161,7 @@ const Config = z.object({
3036
3161
  "disabled"
3037
3162
  ]).default("auto")
3038
3163
  });
3039
- const PluginConfig = Config.volatile();
3164
+ const PluginConfig = typeof Config.volatile === "function" ? Config.volatile() : Config;
3040
3165
  function readConfig(value) {
3041
3166
  return "get" in value ? Config(structuredClone(value.get())) : value;
3042
3167
  }
@@ -3062,10 +3187,11 @@ const QODER_PROVIDER_ID = "dsh-provider-qoder";
3062
3187
  //#region src/dsh/adapter.ts
3063
3188
  /** Thin DSH adapter over the Qoder transport seam. */
3064
3189
  function modelInfo(provider, model) {
3190
+ const rate = formatModelRate(model, true);
3065
3191
  return {
3066
3192
  provider,
3067
3193
  id: model.id,
3068
- name: model.priceFactor === void 0 ? model.name : `${model.name} (${model.priceFactor}x)`,
3194
+ name: rate === void 0 ? model.name : `${model.name} (${rate})`,
3069
3195
  description: model.description,
3070
3196
  inputModalities: model.supportsImages === true ? ["text", "image"] : ["text"]
3071
3197
  };
@@ -3077,6 +3203,12 @@ var QoderAdapter = class extends LlmAdapter {
3077
3203
  providerName;
3078
3204
  onModelsDiscovered;
3079
3205
  discoveries = /* @__PURE__ */ new WeakMap();
3206
+ historyRegion;
3207
+ historyRegionChanged = false;
3208
+ region;
3209
+ sessionTiers = /* @__PURE__ */ new Map();
3210
+ sessions;
3211
+ agents;
3080
3212
  constructor(options) {
3081
3213
  super();
3082
3214
  this.resolveTransport = options.resolveTransport;
@@ -3084,6 +3216,48 @@ var QoderAdapter = class extends LlmAdapter {
3084
3216
  this.providerId = options.providerId ?? "dsh-provider-qoder";
3085
3217
  this.providerName = options.providerName ?? "Qoder";
3086
3218
  this.onModelsDiscovered = options.onModelsDiscovered;
3219
+ this.region = options.region ?? (() => "global");
3220
+ this.historyRegion = this.region();
3221
+ this.sessions = options.sessions;
3222
+ this.agents = options.agents;
3223
+ }
3224
+ setSessionTier(sessionId, modelId, tierKey, region = this.region()) {
3225
+ const model = this.effectiveModels().find((candidate) => candidate.id === modelId);
3226
+ if (region !== this.region() || !sessionId || !model || !contextTiersOf(model).some((tier) => tier.key === tierKey) || this.sessions && !this.sessions.get(sessionId)) throw new QoderLlmError("Invalid session context tier selection.", "INVALID_REQUEST");
3227
+ this.sessionTiers.set(JSON.stringify([
3228
+ region,
3229
+ sessionId,
3230
+ modelId
3231
+ ]), tierKey);
3232
+ }
3233
+ getSessionTier(sessionId, modelId) {
3234
+ return this.sessionTiers.get(JSON.stringify([
3235
+ this.region(),
3236
+ sessionId,
3237
+ modelId
3238
+ ]));
3239
+ }
3240
+ observeRegion() {
3241
+ if (this.region() !== this.historyRegion) {
3242
+ this.historyRegionChanged = true;
3243
+ this.historyRegion = this.region();
3244
+ }
3245
+ }
3246
+ resolveEffectiveModelForSession(modelId, sessionId) {
3247
+ this.observeRegion();
3248
+ const base = this.effectiveModels().find((candidate) => candidate.id === modelId);
3249
+ if (base === void 0) return void 0;
3250
+ if (sessionId) {
3251
+ const manualTierKey = this.getSessionTier(sessionId, modelId);
3252
+ const reqCtx = this.sessions?.get(sessionId)?.requestContext?.();
3253
+ const tier = resolveContextTier(base, manualTierKey, !this.historyRegionChanged && reqCtx?.provider === this.providerId && reqCtx.model === modelId ? reqCtx.contextWindow : void 0);
3254
+ if (tier) return {
3255
+ ...base,
3256
+ contextTier: tier.key,
3257
+ contextWindow: tier.tokenCount
3258
+ };
3259
+ }
3260
+ return base;
3087
3261
  }
3088
3262
  providerInfo(provider) {
3089
3263
  return {
@@ -3120,14 +3294,14 @@ var QoderAdapter = class extends LlmAdapter {
3120
3294
  */
3121
3295
  publishDiscoveredModels(transport, models) {
3122
3296
  try {
3123
- const pending = this.onModelsDiscovered?.(transport, models);
3124
- if (pending !== void 0 && pending !== null && typeof pending.catch === "function") pending.catch(() => {});
3297
+ Promise.resolve(this.onModelsDiscovered?.(transport, models)).catch(() => {});
3125
3298
  } catch {}
3126
3299
  }
3127
3300
  effectiveModels() {
3128
3301
  return mergeQoderDiscoveryMetadata(this.catalogModels, this.discoveries.get(this.resolveTransport())?.models ?? []);
3129
3302
  }
3130
3303
  replaceModels(models) {
3304
+ this.observeRegion();
3131
3305
  this.catalogModels = models;
3132
3306
  }
3133
3307
  /** Publish an explicit discovery without letting older in-flight reads overwrite it. */
@@ -3139,16 +3313,34 @@ var QoderAdapter = class extends LlmAdapter {
3139
3313
  }
3140
3314
  resolveModel(provider, modelId, signal) {
3141
3315
  if (signal?.aborted) return Promise.reject(new QoderLlmError("Qoder model resolution was aborted.", "ABORTED"));
3142
- const configured = this.effectiveModels().find((model) => model.id === modelId);
3316
+ const sessionId = this.agents?.currentInitiator()?.session?.id;
3317
+ const configured = this.resolveEffectiveModelForSession(modelId, sessionId);
3318
+ return this.resolvedModel(provider, modelId, configured);
3319
+ }
3320
+ prepareCall(provider, modelId, signal) {
3321
+ if (signal?.aborted) return Promise.reject(new QoderLlmError("Qoder model resolution was aborted.", "ABORTED"));
3322
+ const configured = this.resolveEffectiveModelForSession(modelId, this.agents?.currentInitiator()?.session?.id);
3323
+ const snapshot = configured === void 0 ? void 0 : structuredClone(configured);
3324
+ const transport = this.resolveTransport();
3325
+ return this.resolvedModel(provider, modelId, snapshot).then((model) => ({
3326
+ model,
3327
+ stream: (options) => {
3328
+ if (options.provider !== this.providerId) throw new QoderLlmError(`Qoder adapter does not own provider "${options.provider}".`, "INVALID_PROVIDER");
3329
+ return transport.stream(options, snapshot);
3330
+ }
3331
+ }));
3332
+ }
3333
+ resolvedModel(provider, modelId, configured) {
3143
3334
  if (configured === void 0) return Promise.resolve({
3144
3335
  provider,
3145
3336
  id: modelId,
3146
3337
  name: modelId,
3147
3338
  inputModalities: ["text"]
3148
3339
  });
3340
+ const contextWindow = effectiveContextWindow(configured);
3149
3341
  return Promise.resolve({
3150
3342
  ...modelInfo(provider, configured),
3151
- ...configured.contextWindow === void 0 ? {} : { context: { contextWindow: configured.contextWindow } },
3343
+ ...contextWindow === void 0 ? {} : { context: { contextWindow } },
3152
3344
  ...configured.maxTokens === void 0 ? {} : { defaultMaxTokens: configured.maxTokens },
3153
3345
  ...configured.reasoningEfforts === void 0 || configured.reasoningEfforts.length === 0 ? {} : { reasoning: {
3154
3346
  efforts: configured.reasoningEfforts.map((effort) => ({
@@ -3162,7 +3354,7 @@ var QoderAdapter = class extends LlmAdapter {
3162
3354
  }
3163
3355
  stream(options) {
3164
3356
  if (options.provider !== this.providerId) throw new QoderLlmError(`Qoder adapter does not own provider "${options.provider}".`, "INVALID_PROVIDER");
3165
- const model = this.effectiveModels().find((candidate) => candidate.id === (options.model || "cmodel"));
3357
+ const model = this.resolveEffectiveModelForSession(options.model || "cmodel", options.sessionId);
3166
3358
  return this.resolveTransport().stream(options, model);
3167
3359
  }
3168
3360
  };
@@ -3178,18 +3370,23 @@ var QoderAdapter = class extends LlmAdapter {
3178
3370
  * @module dsh-provider-qoder/dsh/search-provider
3179
3371
  */
3180
3372
  const QODER_SEARCH_PROVIDER_ID = "qoder";
3181
- const QODER_MODEL_PROVIDER_ROUTE = QODER_PROVIDER_ID;
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
+ }
3182
3381
  var QoderSearchProvider = class {
3183
3382
  id = QODER_SEARCH_PROVIDER_ID;
3184
3383
  ctx;
3185
3384
  resolveTransport;
3186
- searchClient;
3187
3385
  getWebSearchMode;
3188
3386
  fallbackProvider;
3189
3387
  constructor(options) {
3190
3388
  this.ctx = options.ctx;
3191
3389
  this.resolveTransport = options.resolveTransport;
3192
- this.searchClient = options.searchClient;
3193
3390
  this.getWebSearchMode = options.getWebSearchMode;
3194
3391
  this.fallbackProvider = options.fallbackProvider;
3195
3392
  }
@@ -3199,17 +3396,19 @@ var QoderSearchProvider = class {
3199
3396
  async search(request, signal) {
3200
3397
  const mode = this.getWebSearchMode();
3201
3398
  if (mode === "disabled") throw new WebError("Qoder web search is disabled in settings.", "WEB_PROVIDER_UNAVAILABLE");
3202
- const agent = (this.ctx.get("agents") ?? this.ctx.agents)?.currentInitiator?.();
3203
- const defaultModelService = this.ctx.get("agentDefaultModel");
3204
- const providerRoute = agent?.options?.provider ?? defaultModelService?.get?.()?.provider;
3205
- if (mode === "always" || providerRoute === QODER_MODEL_PROVIDER_ROUTE) {
3206
- if (this.resolveTransport !== void 0) return this.resolveTransport().searchWeb(request, signal);
3207
- if (this.searchClient !== void 0) return this.searchClient.search(request, signal);
3208
- 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
+ };
3209
3408
  }
3210
3409
  const fallback = this.resolveFallback();
3211
3410
  if (fallback && fallback.available()) return fallback.search(request, signal);
3212
- throw new WebError(`Current model provider is "${providerRoute ?? "unknown"}" rather than "${QODER_MODEL_PROVIDER_ROUTE}", and no fallback web search provider is available. Switch to a Qoder model or configure an ambient search provider.`, "WEB_PROVIDER_UNAVAILABLE");
3411
+ throw new WebError(`Current model provider is "${providerRoute ?? "unknown"}" rather than "${QODER_PROVIDER_ID}", and no fallback web search provider is available. Switch to a Qoder model or configure an ambient search provider.`, "WEB_PROVIDER_UNAVAILABLE");
3213
3412
  }
3214
3413
  resolveFallback() {
3215
3414
  if (this.fallbackProvider && this.fallbackProvider.available()) return this.fallbackProvider;
@@ -3266,8 +3465,12 @@ function bindQoderSettings(owner, ctx, input, options) {
3266
3465
  * channel at `/api`, and every exact Fetch route lives directly below it.
3267
3466
  */
3268
3467
  const qoderRpcApiPath = `/api/qoder-subscription`;
3269
- /** Endpoints the account card and the model catalog call. */
3270
- const qoderRpcEndpoints = ["account", "models"];
3468
+ /** Endpoints the account card, the model catalog, and the composer context select call. */
3469
+ const qoderRpcEndpoints = [
3470
+ "account",
3471
+ "models",
3472
+ "sessionTier"
3473
+ ];
3271
3474
  /** Absolute Fetch-route path of one endpoint. */
3272
3475
  function qoderRpcPath(endpoint) {
3273
3476
  return `${qoderRpcApiPath}/${endpoint}`;
@@ -3369,6 +3572,26 @@ function publicError(code, message, details = { issues: [] }) {
3369
3572
  }
3370
3573
  };
3371
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
+ }
3372
3595
  function apply(ctx, input = {}) {
3373
3596
  const config = readConfig(input);
3374
3597
  const logger = ctx.logger;
@@ -3418,8 +3641,11 @@ function apply(ctx, input = {}) {
3418
3641
  const adapter = new QoderAdapter({
3419
3642
  resolveTransport: () => activeTransport,
3420
3643
  models: initial.models,
3644
+ region: () => activeTransportConfig.region,
3421
3645
  providerId: providerQoder,
3422
3646
  providerName: "Qoder",
3647
+ sessions: ctx.get("sessions"),
3648
+ agents: ctx.get("agents"),
3423
3649
  onModelsDiscovered: (transport, models) => {
3424
3650
  if (transport !== activeTransport || ctx.fiber.state === fiberUnloading || ctx.fiber.state === fiberDisposed) return;
3425
3651
  const region = activeTransportConfig.region;
@@ -3511,33 +3737,24 @@ function apply(ctx, input = {}) {
3511
3737
  const handler = async (endpoint, payload, signal) => {
3512
3738
  if (!isQoderRpcEndpoint(endpoint)) return publicError("UNKNOWN_ENDPOINT", `Unknown endpoint: ${endpoint}`);
3513
3739
  if (signal.aborted) return publicError("ABORTED", "Request aborted");
3514
- const executeRpc = async (operation, task) => {
3515
- try {
3516
- return {
3517
- ok: true,
3518
- value: await task()
3519
- };
3520
- } catch (error) {
3521
- if (signal.aborted || error instanceof QoderLlmError && error.code === "ABORTED") {
3522
- logger?.debug?.(`[Qoder RPC] ${operation} was aborted`);
3523
- return publicError("ABORTED", "Request aborted");
3524
- }
3525
- let code = "INTERNAL";
3526
- if (error instanceof QoderLlmError) if (error.code === "MISSING_CREDENTIAL" || error.code === "NO_CREDENTIALS") code = "NO_CREDENTIALS";
3527
- else if (error.code === "AUTH") code = "UNAUTHENTICATED";
3528
- else if (error.code === "TIMEOUT") code = "TIMEOUT";
3529
- else code = "UPSTREAM_ERROR";
3530
- logger?.error?.(`[Qoder RPC] Failed to ${operation}`, logError(error));
3531
- return publicError(code, error instanceof Error ? error.message : `Failed to ${operation}`);
3740
+ if (endpoint === "models") return await executeRpc("discover Qoder models", () => discoverModels(signal), signal, logger);
3741
+ if (endpoint === "sessionTier") {
3742
+ const data = payload;
3743
+ if (typeof data?.sessionId === "string" && typeof data?.modelId === "string" && typeof data?.tierKey === "string" && (data.region === "global" || data.region === "china")) {
3744
+ const { sessionId, modelId, tierKey, region } = data;
3745
+ return await executeRpc("select session context tier", async () => {
3746
+ adapter.setSessionTier(sessionId, modelId, tierKey, region);
3747
+ return { success: true };
3748
+ }, signal, logger);
3532
3749
  }
3533
- };
3534
- if (endpoint === "models") return await executeRpc("discover Qoder models", () => discoverModels(signal));
3750
+ return publicError("INTERNAL", "Invalid sessionTier payload");
3751
+ }
3535
3752
  const force = typeof payload === "object" && payload !== null && "force" in payload ? payload.force === true : false;
3536
3753
  logger?.debug?.("[Qoder RPC] Reading subscriber account", { force });
3537
3754
  const outcome = await executeRpc("load Qoder account", () => activeTransport.readAccount({
3538
3755
  force,
3539
3756
  signal
3540
- }));
3757
+ }), signal, logger);
3541
3758
  if (outcome.ok) logger?.debug?.("[Qoder RPC] Subscriber account resolved");
3542
3759
  return outcome;
3543
3760
  };
@@ -3554,23 +3771,19 @@ function apply(ctx, input = {}) {
3554
3771
  });
3555
3772
  webCtx.web.registerSearchProvider(searchProvider);
3556
3773
  if (typeof webCtx.web.search === "function") {
3557
- const originalSearch = webCtx.web.search.bind(webCtx.web);
3774
+ const originalSearch = webCtx.web.search;
3558
3775
  webCtx.effect(() => {
3559
- webCtx.web.search = async (request, signal) => {
3560
- const mode = resolveConfig().webSearchMode ?? "auto";
3561
- if (mode === "disabled") return originalSearch(request, signal);
3562
- const agent = (ctx.get("agents") ?? ctx.agents)?.currentInitiator?.();
3563
- const defaultModelService = ctx.get("agentDefaultModel");
3564
- const isQoderActive = (agent?.options?.provider ?? defaultModelService?.get?.()?.provider) === providerQoder;
3565
- if (mode === "always" || isQoderActive) return searchProvider.search(request, signal);
3566
- 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);
3567
3779
  };
3780
+ webCtx.web.search = routedSearch;
3568
3781
  return () => {
3569
- webCtx.web.search = originalSearch;
3782
+ if (webCtx.web.search === routedSearch) webCtx.web.search = originalSearch;
3570
3783
  };
3571
3784
  }, "provider-qoder: transparent web search router");
3572
3785
  }
3573
3786
  });
3574
3787
  }
3575
3788
  //#endregion
3576
- export { PluginConfig as Config, QODER_SEARCH_PROVIDER_ID, QoderSearchClient, QoderSearchProvider, apply, getQoderWebSearchUrl, inject, name, qoderWebSearchPath };
3789
+ export { PluginConfig as Config, QODER_SEARCH_PROVIDER_ID, QoderSearchProvider, apply, inject, name };