dsh-provider-qoder 0.3.2 → 0.4.1

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.d.ts CHANGED
@@ -12,6 +12,11 @@ interface QoderCatalogModel {
12
12
  contextWindow?: number;
13
13
  /** Largest known capacity, independent of the effective input budget. */
14
14
  maxContextWindow?: number;
15
+ /**
16
+ * Provider-owned key of the context tier the subscriber selected. Absent means
17
+ * the provider default tier decides the effective input budget.
18
+ */
19
+ contextTier?: string;
15
20
  maxTokens?: number;
16
21
  source?: string;
17
22
  isReasoning?: boolean;
@@ -23,6 +28,8 @@ interface QoderCatalogModel {
23
28
  }>;
24
29
  defaultReasoningEffort?: string;
25
30
  priceFactor?: number;
31
+ originalPriceFactor?: number;
32
+ isFree?: boolean;
26
33
  contextOptions?: Record<string, {
27
34
  tokenCount?: number;
28
35
  isDefault?: boolean;
@@ -43,8 +50,6 @@ type QoderWebSearchMode = 'auto' | 'always' | 'disabled';
43
50
  interface Config {
44
51
  region?: QoderRegion;
45
52
  modelsByRegion?: QoderModelsByRegion;
46
- /** @deprecated Migrated to modelsByRegion for the selected region. */
47
- models?: QoderCatalogModel[];
48
53
  streamIdleTimeoutMs?: number;
49
54
  responseHeaderTimeoutMs?: number;
50
55
  preserveThinking?: boolean;
@@ -52,7 +57,7 @@ interface Config {
52
57
  }
53
58
  declare const Config: z<Config>;
54
59
  type PluginConfig = Config;
55
- declare const PluginConfig: z<NoInfer<Config>, NoInfer<Config>, "volatile">;
60
+ declare const PluginConfig: z<Config> | z<NoInfer<Config>, NoInfer<Config>, "volatile">;
56
61
  type LiveConfig = ReturnType<typeof PluginConfig>;
57
62
  //#endregion
58
63
  //#region src/dsh/plugin.d.ts
@@ -158,11 +163,6 @@ interface QoderLogger {
158
163
  error?(message: string, ...details: unknown[]): void;
159
164
  }
160
165
  //#endregion
161
- //#region src/qoder/transport/endpoints.d.ts
162
- /** Path of the center web search route. */
163
- declare const qoderWebSearchPath = "/api/v1/webSearch/oneSearch";
164
- declare function getQoderWebSearchUrl(region?: QoderRegion): string;
165
- //#endregion
166
166
  //#region src/qoder/transport/wire/cosy.d.ts
167
167
  interface CosyCredentials {
168
168
  userID: string;
@@ -228,4 +228,4 @@ declare class QoderSearchProvider implements WebSearchProvider {
228
228
  private resolveFallback;
229
229
  }
230
230
  //#endregion
231
- export { PluginConfig as Config, QODER_SEARCH_PROVIDER_ID, type QoderAccountInfo, type QoderCatalogModel, type QoderModelsByRegion, type QoderRegion, QoderSearchClient, type QoderSearchClientOptions, QoderSearchProvider, type QoderSearchProviderOptions, type QoderWebSearchMode, apply, getQoderWebSearchUrl, inject, name, qoderWebSearchPath };
231
+ export { PluginConfig as Config, QODER_SEARCH_PROVIDER_ID, type QoderAccountInfo, type QoderCatalogModel, type QoderModelsByRegion, type QoderRegion, QoderSearchProvider, type QoderSearchProviderOptions, type QoderWebSearchMode, apply, inject, name };
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"
@@ -94,7 +96,11 @@ function normalizeQoderModels(payload, onConflict) {
94
96
  const contextWindow = (defaultOptions.length === 1 ? defaultOptions[0].tokenCount : void 0) ?? positiveNumber(raw.max_input_tokens) ?? 18e4;
95
97
  const maxContextWindow = Math.max(positiveNumber(raw.max_input_tokens) ?? 0, contextWindow, ...Object.values(contextOptions ?? {}).map((option) => option.tokenCount ?? 0));
96
98
  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;
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 originalPriceFactor = rawOriginalPrice ?? promotionPrice;
103
+ const isFree = raw.is_free === true || raw.isFree === true || Array.isArray(raw.tags) && raw.tags.includes("limited_time_free") || priceFactor === 0;
98
104
  const reasoning = reasoningEffortsOf(raw.thinking_config);
99
105
  models.push({
100
106
  id,
@@ -109,20 +115,99 @@ function normalizeQoderModels(payload, onConflict) {
109
115
  ...reasoning.efforts === void 0 ? {} : { reasoningEfforts: reasoning.efforts },
110
116
  ...reasoning.defaultEffort === void 0 ? {} : { defaultReasoningEffort: reasoning.defaultEffort },
111
117
  ...priceFactor === void 0 ? {} : { priceFactor },
118
+ ...originalPriceFactor === void 0 ? {} : { originalPriceFactor },
119
+ ...isFree ? { isFree: true } : {},
112
120
  ...contextOptions === void 0 ? {} : { contextOptions }
113
121
  });
114
122
  }
115
123
  return models;
116
124
  }
125
+ /**
126
+ * Provider-advertised context tiers of one model, smallest first. A tier without
127
+ * a usable capacity cannot be selected and is dropped.
128
+ */
129
+ function contextTiersOf(model) {
130
+ const options = model.contextOptions;
131
+ if (options === void 0) return [];
132
+ return Object.entries(options).flatMap(([key, value]) => {
133
+ const tokenCount = value.tokenCount;
134
+ return typeof tokenCount === "number" && Number.isFinite(tokenCount) && tokenCount > 0 ? [{
135
+ key,
136
+ tokenCount,
137
+ isDefault: value.isDefault === true
138
+ }] : [];
139
+ }).sort((left, right) => left.tokenCount - right.tokenCount);
140
+ }
141
+ /** Resolve a usable selection consistently for the composer and adapter. */
142
+ function resolveContextTier(model, manualTier, historicalWindow) {
143
+ const tiers = contextTiersOf(model);
144
+ 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);
145
+ }
146
+ /** Format a price factor number to string (e.g. 1 -> '1', 1.6 -> '1.6', 0.5 -> '0.5', 1.25 -> '1.25'). */
147
+ function formatRateFactor(value) {
148
+ const rounded = Number(value.toFixed(2));
149
+ return String(rounded);
150
+ }
151
+ /**
152
+ * Format model price factor / rate string.
153
+ * - Free models or price factor 0: '免费' or 'Free'
154
+ * - Promotional models: '0.5x(原 1x)' or '0.5x (was 1x)'
155
+ * - Standard models: '1x'
156
+ */
157
+ function formatModelRate(model, isZh = true) {
158
+ if (model.isFree || model.priceFactor === 0) return isZh ? "免费" : "Free";
159
+ if (model.priceFactor === void 0) return void 0;
160
+ const current = `${formatRateFactor(model.priceFactor)}x`;
161
+ if (model.originalPriceFactor !== void 0 && model.originalPriceFactor > model.priceFactor) {
162
+ const orig = `${formatRateFactor(model.originalPriceFactor)}x`;
163
+ return isZh ? `${current}(原 ${orig})` : `${current} (was ${orig})`;
164
+ }
165
+ return current;
166
+ }
167
+ /**
168
+ * The provider-advertised context tier the subscriber selected, when the
169
+ * provider still advertises that tier with a usable capacity.
170
+ */
171
+ function selectedContextTier(model) {
172
+ const key = model.contextTier;
173
+ if (typeof key !== "string" || key.length === 0) return void 0;
174
+ const tokenCount = positiveNumber(model.contextOptions?.[key]?.tokenCount);
175
+ return tokenCount === void 0 ? void 0 : {
176
+ key,
177
+ tokenCount
178
+ };
179
+ }
180
+ /**
181
+ * Effective input budget carried to DSH for one catalog entry.
182
+ *
183
+ * A selected tier is an explicit subscriber decision, so it outranks the
184
+ * provider default tier and may widen the budget beyond it; without a selection
185
+ * the entry keeps its stored budget.
186
+ */
187
+ function effectiveContextWindow(model) {
188
+ return selectedContextTier(model)?.tokenCount ?? model.contextWindow;
189
+ }
117
190
  function mergeQoderDiscoveryMetadata(configured, discovered) {
118
191
  const catalog = new Map(discovered.map((model) => [model.id, model]));
119
192
  return configured.map((model) => {
120
193
  const advertised = catalog.get(model.id);
121
- if (advertised === void 0) return { ...model };
194
+ if (advertised === void 0) return {
195
+ ...model,
196
+ ...model.contextOptions !== void 0 ? { contextOptions: { ...model.contextOptions } } : {}
197
+ };
122
198
  const merged = { ...model };
123
- if (advertised.contextWindow !== void 0) merged.contextWindow = Math.min(model.contextWindow ?? advertised.contextWindow, advertised.contextWindow);
124
199
  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] });
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);
126
211
  return merged;
127
212
  });
128
213
  }
@@ -198,8 +283,6 @@ const qoderRegionEndpoints = {
198
283
  };
199
284
  /** Signed path of the center image upload route; it never carries an `/algo` prefix. */
200
285
  const qoderImageUploadPath = "/api/v2/image/upload";
201
- qoderRegionEndpoints.global.baseUrl;
202
- qoderRegionEndpoints.global.openApiUrl;
203
286
  function resolveQoderEndpoints(region = "global") {
204
287
  return qoderRegionEndpoints[region] ?? qoderRegionEndpoints.global;
205
288
  }
@@ -830,8 +913,7 @@ async function resolveImagePart(block, context) {
830
913
  const target = {
831
914
  width: Math.max(1, Math.floor(width * scale)),
832
915
  height: Math.max(1, Math.floor(height * scale)),
833
- maxBytes: limits.maxImageBytes,
834
- maxPixels: limits.maxImagePixels
916
+ maxBytes: limits.maxImageBytes
835
917
  };
836
918
  image = await attachments.readImageRequest(block.attachment, target, signal);
837
919
  } catch (error) {
@@ -1001,11 +1083,9 @@ async function validateAndTranslateMessages(messages, systemPrompt, attachments,
1001
1083
  }
1002
1084
  if (toolResults.length > 0 && !hasImage && text.trim().length === 0) continue;
1003
1085
  const wireRole = toolResults.length > 0 ? "user" : message.role;
1004
- const wireToolCallId = wireRole === "tool" ? consumeToolCallId(message, void 0, lastAssistantToolCallIds, output.length) : void 0;
1005
1086
  output.push({
1006
1087
  role: wireRole,
1007
- content: hasImage ? userContent.filter((part) => part !== void 0) : text,
1008
- ...wireToolCallId ? { tool_call_id: wireToolCallId } : {}
1088
+ content: hasImage ? userContent.filter((part) => part !== void 0) : text
1009
1089
  });
1010
1090
  }
1011
1091
  return output;
@@ -1346,11 +1426,13 @@ async function buildQoderRequestBody(options, userId, translatedMessages, model,
1346
1426
  const maxTokens = Math.min(options.maxTokens ?? modelMaxTokens, modelMaxTokens);
1347
1427
  const isReasoning = options.reasoningEffort !== void 0 || (model?.isReasoning ?? false);
1348
1428
  const tools = translateTools(options.tools);
1429
+ const selectedTier = model === void 0 ? void 0 : selectedContextTier(model);
1349
1430
  const defaultContexts = Object.values(model?.contextOptions ?? {}).filter((option) => option.isDefault === true && typeof option.tokenCount === "number" && Number.isFinite(option.tokenCount) && option.tokenCount > 0);
1350
- const contextConfig = model?.contextOptions === void 0 || defaultContexts.length !== 1 ? void 0 : Object.fromEntries(Object.entries(model.contextOptions).map(([key, value]) => [key, {
1431
+ const contextConfig = model?.contextOptions === void 0 || selectedTier === void 0 && defaultContexts.length !== 1 ? void 0 : Object.fromEntries(Object.entries(model.contextOptions).map(([key, value]) => [key, {
1351
1432
  ...value.tokenCount === void 0 ? {} : { token_count: value.tokenCount },
1352
- ...value.isDefault === void 0 ? {} : { is_default: value.isDefault }
1433
+ ...selectedTier === void 0 ? value.isDefault === void 0 ? {} : { is_default: value.isDefault } : { is_default: key === selectedTier.key }
1353
1434
  }]));
1435
+ const effectiveBudget = selectedTier?.tokenCount ?? model?.contextWindow;
1354
1436
  let lastUserText = "";
1355
1437
  for (let index = messages.length - 1; index >= 0; index--) if (messages[index].role === "user") {
1356
1438
  lastUserText = wireMessageText(messages[index]);
@@ -1384,6 +1466,7 @@ async function buildQoderRequestBody(options, userId, translatedMessages, model,
1384
1466
  tools,
1385
1467
  parameters: {
1386
1468
  max_tokens: maxTokens,
1469
+ ...effectiveBudget === void 0 ? {} : { context_length: effectiveBudget },
1387
1470
  ...options.reasoningEffort === void 0 ? {} : { reasoning_effort: String(options.reasoningEffort) }
1388
1471
  },
1389
1472
  chat_context: {
@@ -2067,6 +2150,18 @@ function qoderEncodeBody(plaintext) {
2067
2150
  }
2068
2151
  return out;
2069
2152
  }
2153
+ //#endregion
2154
+ //#region src/qoder/transport/search.ts
2155
+ /**
2156
+ * Web search transport client communicating with the Qoder center service.
2157
+ *
2158
+ * Dispatches query requests to the center-hosted `oneSearch` route using the
2159
+ * subscriber's COSY credentials, handling payload serialization, signing,
2160
+ * 401/403 credential refresh retry, and mapping to DSH WebSearchResult.
2161
+ *
2162
+ * @module dsh-provider-qoder/qoder/transport/search
2163
+ */
2164
+ const defaultSearchTimeoutMs = 3e4;
2070
2165
  function searchAborted(signal, fallback) {
2071
2166
  return new WebError("Web search was cancelled.", "WEB_ABORTED", { cause: signal?.aborted === true ? signal.reason : fallback });
2072
2167
  }
@@ -2085,7 +2180,7 @@ var QoderSearchClient = class {
2085
2180
  this.fetchImpl = options.fetch ?? globalThis.fetch;
2086
2181
  this.region = options.region ?? "global";
2087
2182
  this.logger = options.logger;
2088
- this.timeoutMs = options.timeoutMs ?? 3e4;
2183
+ this.timeoutMs = options.timeoutMs ?? defaultSearchTimeoutMs;
2089
2184
  this.resolveCredentials = options.resolveCredentials;
2090
2185
  this.refreshCredentials = options.refreshCredentials;
2091
2186
  this.now = options.now ?? Date.now;
@@ -2385,6 +2480,7 @@ var QoderThinkingParser = class {
2385
2480
  //#region src/qoder/transport/wire/sse.ts
2386
2481
  /** Decode Qoder's outer SSE envelope and inner model chunks. */
2387
2482
  const doneMarker = "[DONE]";
2483
+ const defaultMaxSseBufferChars = 2 * 1024 * 1024;
2388
2484
  function malformed(message) {
2389
2485
  return new QoderLlmError(message, "MALFORMED_RESPONSE");
2390
2486
  }
@@ -2487,7 +2583,7 @@ async function* parseQoderSse(stream, options = {}) {
2487
2583
  let terminalKind = "stop";
2488
2584
  let sawFinishReason = false;
2489
2585
  let sawContent = false;
2490
- const maxBufferChars = options.maxBufferChars ?? 2097152;
2586
+ const maxBufferChars = options.maxBufferChars ?? defaultMaxSseBufferChars;
2491
2587
  const closeTextual = () => {
2492
2588
  if (activeTextual === void 0) return [];
2493
2589
  const state = activeTextual;
@@ -2751,6 +2847,16 @@ async function* streamQoderChat(options, model, credentials, messages, dependenc
2751
2847
  }, dependencies.streamIdleTimeoutMs);
2752
2848
  };
2753
2849
  try {
2850
+ const markedTiers = Object.entries(request.model_config.context_config ?? {}).filter(([, tier]) => tier.is_default === true);
2851
+ dependencies.logger?.debug?.("[Qoder Stream] Request context", redactLogValue({
2852
+ sessionId: options.sessionId ?? null,
2853
+ requestId: request.request_id,
2854
+ model: request.model_config.key,
2855
+ region: dependencies.region,
2856
+ purpose: options.purpose ?? "chat",
2857
+ contextTier: markedTiers.length === 1 ? markedTiers[0][0] : null,
2858
+ context_length: request.parameters.context_length ?? null
2859
+ }));
2754
2860
  const response = await dependencies.fetch(chatUrl, {
2755
2861
  method: "POST",
2756
2862
  headers: {
@@ -3000,13 +3106,14 @@ function createQoderTransport(options) {
3000
3106
  }
3001
3107
  //#endregion
3002
3108
  //#region src/dsh/config.ts
3003
- /** DSH configuration schema and region-scoped model catalog migration rules. */
3109
+ /** DSH configuration schema and region-scoped model catalog selection. */
3004
3110
  const catalogModel = z.object({
3005
3111
  id: z.string().required(),
3006
3112
  name: z.string().required(),
3007
3113
  description: z.string(),
3008
3114
  contextWindow: z.number().step(1).min(1),
3009
3115
  maxContextWindow: z.number().step(1).min(1),
3116
+ contextTier: z.string(),
3010
3117
  maxTokens: z.number().step(1).min(1),
3011
3118
  source: z.string(),
3012
3119
  isReasoning: z.boolean(),
@@ -3018,6 +3125,8 @@ const catalogModel = z.object({
3018
3125
  })),
3019
3126
  defaultReasoningEffort: z.string(),
3020
3127
  priceFactor: z.number().min(0),
3128
+ originalPriceFactor: z.number().min(0),
3129
+ isFree: z.boolean(),
3021
3130
  supportsImages: z.boolean(),
3022
3131
  contextOptions: z.dict(z.object({
3023
3132
  tokenCount: z.number().step(1).min(1),
@@ -3028,7 +3137,6 @@ const modelsByRegionSchema = z.dict(z.array(catalogModel));
3028
3137
  const Config = z.object({
3029
3138
  region: z.union(["global", "china"]).default("global"),
3030
3139
  modelsByRegion: modelsByRegionSchema.default({}),
3031
- models: z.array(catalogModel),
3032
3140
  streamIdleTimeoutMs: z.number().step(1).min(1).default(defaultStreamIdleTimeoutMs),
3033
3141
  responseHeaderTimeoutMs: z.number().step(1).min(1).default(defaultResponseHeaderTimeoutMs),
3034
3142
  preserveThinking: z.boolean().default(true),
@@ -3038,7 +3146,7 @@ const Config = z.object({
3038
3146
  "disabled"
3039
3147
  ]).default("auto")
3040
3148
  });
3041
- const PluginConfig = Config.volatile();
3149
+ const PluginConfig = typeof Config.volatile === "function" ? Config.volatile() : Config;
3042
3150
  function readConfig(value) {
3043
3151
  return "get" in value ? Config(structuredClone(value.get())) : value;
3044
3152
  }
@@ -3053,11 +3161,8 @@ function resolveModels(models) {
3053
3161
  return { ...model };
3054
3162
  });
3055
3163
  }
3056
- function modelsFor(config, region, legacyModelsRegion = config.region ?? "global") {
3057
- const scoped = config.modelsByRegion?.[region];
3058
- if (scoped !== void 0) return resolveModels(scoped);
3059
- if (legacyModelsRegion === region && config.models !== void 0 && config.models.length > 0) return resolveModels(config.models);
3060
- return resolveModels(defaultModels);
3164
+ function modelsFor(config, region) {
3165
+ return resolveModels(config.modelsByRegion?.[region]);
3061
3166
  }
3062
3167
  //#endregion
3063
3168
  //#region src/dsh/provider.ts
@@ -3067,10 +3172,11 @@ const QODER_PROVIDER_ID = "dsh-provider-qoder";
3067
3172
  //#region src/dsh/adapter.ts
3068
3173
  /** Thin DSH adapter over the Qoder transport seam. */
3069
3174
  function modelInfo(provider, model) {
3175
+ const rate = formatModelRate(model, true);
3070
3176
  return {
3071
3177
  provider,
3072
3178
  id: model.id,
3073
- name: model.priceFactor === void 0 ? model.name : `${model.name} (${model.priceFactor}x)`,
3179
+ name: rate === void 0 ? model.name : `${model.name} (${rate})`,
3074
3180
  description: model.description,
3075
3181
  inputModalities: model.supportsImages === true ? ["text", "image"] : ["text"]
3076
3182
  };
@@ -3082,6 +3188,12 @@ var QoderAdapter = class extends LlmAdapter {
3082
3188
  providerName;
3083
3189
  onModelsDiscovered;
3084
3190
  discoveries = /* @__PURE__ */ new WeakMap();
3191
+ historyRegion;
3192
+ historyRegionChanged = false;
3193
+ region;
3194
+ sessionTiers = /* @__PURE__ */ new Map();
3195
+ sessions;
3196
+ agents;
3085
3197
  constructor(options) {
3086
3198
  super();
3087
3199
  this.resolveTransport = options.resolveTransport;
@@ -3089,6 +3201,52 @@ var QoderAdapter = class extends LlmAdapter {
3089
3201
  this.providerId = options.providerId ?? "dsh-provider-qoder";
3090
3202
  this.providerName = options.providerName ?? "Qoder";
3091
3203
  this.onModelsDiscovered = options.onModelsDiscovered;
3204
+ this.region = options.region ?? (() => "global");
3205
+ this.historyRegion = this.region();
3206
+ this.sessions = options.sessions;
3207
+ this.agents = options.agents;
3208
+ }
3209
+ setSessionTier(sessionId, modelId, tierKey, region = this.region()) {
3210
+ const model = this.effectiveModels().find((candidate) => candidate.id === modelId);
3211
+ 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");
3212
+ this.sessionTiers.set(JSON.stringify([
3213
+ region,
3214
+ sessionId,
3215
+ modelId
3216
+ ]), tierKey);
3217
+ }
3218
+ getSessionTier(sessionId, modelId) {
3219
+ return this.sessionTiers.get(JSON.stringify([
3220
+ this.region(),
3221
+ sessionId,
3222
+ modelId
3223
+ ]));
3224
+ }
3225
+ observeRegion() {
3226
+ if (this.region() !== this.historyRegion) {
3227
+ this.historyRegionChanged = true;
3228
+ this.historyRegion = this.region();
3229
+ }
3230
+ }
3231
+ resolveEffectiveModelForSession(modelId, sessionId) {
3232
+ this.observeRegion();
3233
+ const base = this.effectiveModels().find((candidate) => candidate.id === modelId);
3234
+ if (base === void 0) return void 0;
3235
+ if (sessionId) {
3236
+ const manualTierKey = this.sessionTiers.get(JSON.stringify([
3237
+ this.region(),
3238
+ sessionId,
3239
+ modelId
3240
+ ]));
3241
+ const reqCtx = this.sessions?.get(sessionId)?.requestContext?.();
3242
+ const tier = resolveContextTier(base, manualTierKey, !this.historyRegionChanged && reqCtx?.provider === this.providerId && reqCtx.model === modelId ? reqCtx.contextWindow : void 0);
3243
+ if (tier) return {
3244
+ ...base,
3245
+ contextTier: tier.key,
3246
+ contextWindow: tier.tokenCount
3247
+ };
3248
+ }
3249
+ return base;
3092
3250
  }
3093
3251
  providerInfo(provider) {
3094
3252
  return {
@@ -3133,6 +3291,7 @@ var QoderAdapter = class extends LlmAdapter {
3133
3291
  return mergeQoderDiscoveryMetadata(this.catalogModels, this.discoveries.get(this.resolveTransport())?.models ?? []);
3134
3292
  }
3135
3293
  replaceModels(models) {
3294
+ this.observeRegion();
3136
3295
  this.catalogModels = models;
3137
3296
  }
3138
3297
  /** Publish an explicit discovery without letting older in-flight reads overwrite it. */
@@ -3144,16 +3303,34 @@ var QoderAdapter = class extends LlmAdapter {
3144
3303
  }
3145
3304
  resolveModel(provider, modelId, signal) {
3146
3305
  if (signal?.aborted) return Promise.reject(new QoderLlmError("Qoder model resolution was aborted.", "ABORTED"));
3147
- const configured = this.effectiveModels().find((model) => model.id === modelId);
3306
+ const sessionId = this.agents?.currentInitiator()?.session?.id;
3307
+ const configured = this.resolveEffectiveModelForSession(modelId, sessionId);
3308
+ return this.resolvedModel(provider, modelId, configured);
3309
+ }
3310
+ prepareCall(provider, modelId, signal) {
3311
+ if (signal?.aborted) return Promise.reject(new QoderLlmError("Qoder model resolution was aborted.", "ABORTED"));
3312
+ const configured = this.resolveEffectiveModelForSession(modelId, this.agents?.currentInitiator()?.session?.id);
3313
+ const snapshot = configured === void 0 ? void 0 : structuredClone(configured);
3314
+ const transport = this.resolveTransport();
3315
+ return this.resolvedModel(provider, modelId, snapshot).then((model) => ({
3316
+ model,
3317
+ stream: (options) => {
3318
+ if (options.provider !== this.providerId) throw new QoderLlmError(`Qoder adapter does not own provider "${options.provider}".`, "INVALID_PROVIDER");
3319
+ return transport.stream(options, snapshot);
3320
+ }
3321
+ }));
3322
+ }
3323
+ resolvedModel(provider, modelId, configured) {
3148
3324
  if (configured === void 0) return Promise.resolve({
3149
3325
  provider,
3150
3326
  id: modelId,
3151
3327
  name: modelId,
3152
3328
  inputModalities: ["text"]
3153
3329
  });
3330
+ const contextWindow = effectiveContextWindow(configured);
3154
3331
  return Promise.resolve({
3155
3332
  ...modelInfo(provider, configured),
3156
- ...configured.contextWindow === void 0 ? {} : { context: { contextWindow: configured.contextWindow } },
3333
+ ...contextWindow === void 0 ? {} : { context: { contextWindow } },
3157
3334
  ...configured.maxTokens === void 0 ? {} : { defaultMaxTokens: configured.maxTokens },
3158
3335
  ...configured.reasoningEfforts === void 0 || configured.reasoningEfforts.length === 0 ? {} : { reasoning: {
3159
3336
  efforts: configured.reasoningEfforts.map((effort) => ({
@@ -3167,7 +3344,7 @@ var QoderAdapter = class extends LlmAdapter {
3167
3344
  }
3168
3345
  stream(options) {
3169
3346
  if (options.provider !== this.providerId) throw new QoderLlmError(`Qoder adapter does not own provider "${options.provider}".`, "INVALID_PROVIDER");
3170
- const model = this.effectiveModels().find((candidate) => candidate.id === (options.model || "cmodel"));
3347
+ const model = this.resolveEffectiveModelForSession(options.model || "cmodel", options.sessionId);
3171
3348
  return this.resolveTransport().stream(options, model);
3172
3349
  }
3173
3350
  };
@@ -3183,7 +3360,6 @@ var QoderAdapter = class extends LlmAdapter {
3183
3360
  * @module dsh-provider-qoder/dsh/search-provider
3184
3361
  */
3185
3362
  const QODER_SEARCH_PROVIDER_ID = "qoder";
3186
- const QODER_MODEL_PROVIDER_ROUTE = QODER_PROVIDER_ID;
3187
3363
  var QoderSearchProvider = class {
3188
3364
  id = QODER_SEARCH_PROVIDER_ID;
3189
3365
  ctx;
@@ -3207,14 +3383,14 @@ var QoderSearchProvider = class {
3207
3383
  const agent = (this.ctx.get("agents") ?? this.ctx.agents)?.currentInitiator?.();
3208
3384
  const defaultModelService = this.ctx.get("agentDefaultModel");
3209
3385
  const providerRoute = agent?.options?.provider ?? defaultModelService?.get?.()?.provider;
3210
- if (mode === "always" || providerRoute === QODER_MODEL_PROVIDER_ROUTE) {
3386
+ if (mode === "always" || providerRoute === "dsh-provider-qoder") {
3211
3387
  if (this.resolveTransport !== void 0) return this.resolveTransport().searchWeb(request, signal);
3212
3388
  if (this.searchClient !== void 0) return this.searchClient.search(request, signal);
3213
3389
  throw new WebError("No Qoder transport configured for web search.", "WEB_PROVIDER_ERROR");
3214
3390
  }
3215
3391
  const fallback = this.resolveFallback();
3216
3392
  if (fallback && fallback.available()) return fallback.search(request, signal);
3217
- 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");
3393
+ 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");
3218
3394
  }
3219
3395
  resolveFallback() {
3220
3396
  if (this.fallbackProvider && this.fallbackProvider.available()) return this.fallbackProvider;
@@ -3245,14 +3421,6 @@ async function resolveManagedQoderPat(credentials) {
3245
3421
  //#endregion
3246
3422
  //#region src/dsh/settings.ts
3247
3423
  function bindQoderSettings(owner, ctx, input, options) {
3248
- const settings = ctx.settings;
3249
- if (typeof settings.register === "function") {
3250
- const namespace = "provider-qoder";
3251
- return {
3252
- namespace,
3253
- scope: settings.register(namespace, Config, options)
3254
- };
3255
- }
3256
3424
  const namespace = owner.fiber.entry?.options.id ?? "provider-qoder";
3257
3425
  const get = () => readConfig(input);
3258
3426
  options.validate?.(get());
@@ -3265,10 +3433,9 @@ function bindQoderSettings(owner, ctx, input, options) {
3265
3433
  namespace,
3266
3434
  scope: {
3267
3435
  get,
3268
- update: (patch) => settings.update(namespace, patch),
3436
+ update: (patch) => ctx.settings.update(namespace, patch),
3269
3437
  watch: (listener) => {
3270
- const on = owner.on.bind(owner);
3271
- return ctx.effect(() => on("loader/volatile-update", () => {
3438
+ return ctx.effect(() => owner.on("loader/volatile-update", () => {
3272
3439
  Promise.resolve().then(listener).catch((error) => ctx.emit("internal/error", error));
3273
3440
  }));
3274
3441
  }
@@ -3280,8 +3447,12 @@ function bindQoderSettings(owner, ctx, input, options) {
3280
3447
  * channel at `/api`, and every exact Fetch route lives directly below it.
3281
3448
  */
3282
3449
  const qoderRpcApiPath = `/api/qoder-subscription`;
3283
- /** Endpoints the account card and the model catalog call. */
3284
- const qoderRpcEndpoints = ["account", "models"];
3450
+ /** Endpoints the account card, the model catalog, and the composer context select call. */
3451
+ const qoderRpcEndpoints = [
3452
+ "account",
3453
+ "models",
3454
+ "sessionTier"
3455
+ ];
3285
3456
  /** Absolute Fetch-route path of one endpoint. */
3286
3457
  function qoderRpcPath(endpoint) {
3287
3458
  return `${qoderRpcApiPath}/${endpoint}`;
@@ -3313,7 +3484,7 @@ function registerQoderRpc(ctx, handler) {
3313
3484
  fetch: (request) => handleQoderRpcRequest(endpoint, handler, request)
3314
3485
  }));
3315
3486
  return () => {
3316
- for (const dispose of disposers) if (typeof dispose === "function") dispose();
3487
+ for (const dispose of disposers) dispose();
3317
3488
  };
3318
3489
  }
3319
3490
  /**
@@ -3386,19 +3557,15 @@ function publicError(code, message, details = { issues: [] }) {
3386
3557
  function apply(ctx, input = {}) {
3387
3558
  const config = readConfig(input);
3388
3559
  const logger = ctx.logger;
3389
- const initialRegion = config.region ?? "global";
3390
- const hasConfiguredModels = config.models !== void 0 && config.models.length > 0;
3391
3560
  const baseConfig = {
3392
- region: initialRegion,
3561
+ region: config.region ?? "global",
3393
3562
  modelsByRegion: { ...config.modelsByRegion },
3394
- ...hasConfiguredModels ? { models: resolveModels(config.models) } : {},
3395
3563
  streamIdleTimeoutMs: config.streamIdleTimeoutMs ?? 3e5,
3396
3564
  responseHeaderTimeoutMs: config.responseHeaderTimeoutMs ?? 6e4,
3397
3565
  preserveThinking: config.preserveThinking ?? true,
3398
3566
  webSearchMode: config.webSearchMode ?? "auto"
3399
3567
  };
3400
3568
  let current = () => baseConfig;
3401
- let legacyModelsRegion = initialRegion;
3402
3569
  let persistDiscoveredModels = async (_region) => {};
3403
3570
  const discoveredCatalogs = {
3404
3571
  global: [],
@@ -3418,7 +3585,7 @@ function apply(ctx, input = {}) {
3418
3585
  const region = value.region ?? "global";
3419
3586
  return {
3420
3587
  region,
3421
- models: mergeQoderDiscoveryMetadata(modelsFor(value, region, legacyModelsRegion), discoveredCatalogs[region]),
3588
+ models: mergeQoderDiscoveryMetadata(modelsFor(value, region), discoveredCatalogs[region]),
3422
3589
  streamIdleTimeoutMs: value.streamIdleTimeoutMs ?? 3e5,
3423
3590
  responseHeaderTimeoutMs: value.responseHeaderTimeoutMs ?? 6e4,
3424
3591
  preserveThinking: value.preserveThinking ?? true,
@@ -3436,8 +3603,11 @@ function apply(ctx, input = {}) {
3436
3603
  const adapter = new QoderAdapter({
3437
3604
  resolveTransport: () => activeTransport,
3438
3605
  models: initial.models,
3606
+ region: () => activeTransportConfig.region,
3439
3607
  providerId: providerQoder,
3440
3608
  providerName: "Qoder",
3609
+ sessions: ctx.get("sessions"),
3610
+ agents: ctx.get("agents"),
3441
3611
  onModelsDiscovered: (transport, models) => {
3442
3612
  if (transport !== activeTransport || ctx.fiber.state === fiberUnloading || ctx.fiber.state === fiberDisposed) return;
3443
3613
  const region = activeTransportConfig.region;
@@ -3464,15 +3634,11 @@ function apply(ctx, input = {}) {
3464
3634
  registration.replace([providerQoder]);
3465
3635
  };
3466
3636
  ctx.inject(["settings"], (settingsCtx) => {
3467
- const { scope, namespace } = bindQoderSettings(ctx, settingsCtx, input, {
3468
- base: baseConfig,
3469
- validate: (value) => {
3470
- for (const region of Object.keys(value.modelsByRegion ?? {})) if (region !== "global" && region !== "china") throw new Error(`provider-qoder: unsupported model catalog region "${region}"`);
3471
- modelsFor(value, "global", legacyModelsRegion);
3472
- modelsFor(value, "china", legacyModelsRegion);
3473
- }
3474
- });
3475
- legacyModelsRegion = scope.get().region ?? initialRegion;
3637
+ const { scope, namespace } = bindQoderSettings(ctx, settingsCtx, input, { validate: (value) => {
3638
+ for (const region of Object.keys(value.modelsByRegion ?? {})) if (region !== "global" && region !== "china") throw new Error(`provider-qoder: unsupported model catalog region "${region}"`);
3639
+ modelsFor(value, "global");
3640
+ modelsFor(value, "china");
3641
+ } });
3476
3642
  current = () => scope.get();
3477
3643
  let bindingActive = true;
3478
3644
  const persistCatalog = async (region) => {
@@ -3480,7 +3646,7 @@ function apply(ctx, input = {}) {
3480
3646
  if (!settingsCtx.settings.writable || discoveredCatalogs[region].length === 0) return;
3481
3647
  const snapshot = settingsCtx.settings.describe().find((section) => section.ns === namespace);
3482
3648
  if (snapshot === void 0) return;
3483
- const selected = modelsFor(snapshot.value, region, legacyModelsRegion);
3649
+ const selected = modelsFor(snapshot.value, region);
3484
3650
  const enriched = modelsFor(Config({ modelsByRegion: { [region]: mergeQoderDiscoveryMetadata(selected, discoveredCatalogs[region]) } }), region);
3485
3651
  if (hasSameQoderDiscoveryMetadata(selected, enriched)) return;
3486
3652
  try {
@@ -3493,9 +3659,6 @@ function apply(ctx, input = {}) {
3493
3659
  };
3494
3660
  persistDiscoveredModels = persistCatalog;
3495
3661
  refreshAdapter();
3496
- const loaded = scope.get();
3497
- const loadedRegion = loaded.region ?? "global";
3498
- if (loaded.models !== void 0 && loaded.models.length > 0 && loaded.modelsByRegion?.[loadedRegion] === void 0) scope.update({ modelsByRegion: { [loadedRegion]: resolveModels(loaded.models) } }).catch((error) => logger?.error?.("[Qoder Settings] Failed to migrate model catalog", logError(error)));
3499
3662
  scope.watch(async () => {
3500
3663
  if (!bindingActive || ctx.fiber.state === fiberUnloading || ctx.fiber.state === fiberDisposed) return;
3501
3664
  refreshAdapter();
@@ -3557,6 +3720,17 @@ function apply(ctx, input = {}) {
3557
3720
  }
3558
3721
  };
3559
3722
  if (endpoint === "models") return await executeRpc("discover Qoder models", () => discoverModels(signal));
3723
+ if (endpoint === "sessionTier") {
3724
+ const data = payload;
3725
+ if (typeof data?.sessionId === "string" && typeof data?.modelId === "string" && typeof data?.tierKey === "string" && (data.region === "global" || data.region === "china")) {
3726
+ const { sessionId, modelId, tierKey, region } = data;
3727
+ return await executeRpc("select session context tier", async () => {
3728
+ adapter.setSessionTier(sessionId, modelId, tierKey, region);
3729
+ return { success: true };
3730
+ });
3731
+ }
3732
+ return publicError("INTERNAL", "Invalid sessionTier payload");
3733
+ }
3560
3734
  const force = typeof payload === "object" && payload !== null && "force" in payload ? payload.force === true : false;
3561
3735
  logger?.debug?.("[Qoder RPC] Reading subscriber account", { force });
3562
3736
  const outcome = await executeRpc("load Qoder account", () => activeTransport.readAccount({
@@ -3598,4 +3772,4 @@ function apply(ctx, input = {}) {
3598
3772
  });
3599
3773
  }
3600
3774
  //#endregion
3601
- export { PluginConfig as Config, QODER_SEARCH_PROVIDER_ID, QoderSearchClient, QoderSearchProvider, apply, getQoderWebSearchUrl, inject, name, qoderWebSearchPath };
3775
+ export { PluginConfig as Config, QODER_SEARCH_PROVIDER_ID, QoderSearchProvider, apply, inject, name };