dsh-provider-qoder 0.4.0 → 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;
@@ -50,7 +57,7 @@ interface Config {
50
57
  }
51
58
  declare const Config: z<Config>;
52
59
  type PluginConfig = Config;
53
- declare const PluginConfig: z<NoInfer<Config>, NoInfer<Config>, "volatile">;
60
+ declare const PluginConfig: z<Config> | z<NoInfer<Config>, NoInfer<Config>, "volatile">;
54
61
  type LiveConfig = ReturnType<typeof PluginConfig>;
55
62
  //#endregion
56
63
  //#region src/dsh/plugin.d.ts
@@ -156,11 +163,6 @@ interface QoderLogger {
156
163
  error?(message: string, ...details: unknown[]): void;
157
164
  }
158
165
  //#endregion
159
- //#region src/qoder/transport/endpoints.d.ts
160
- /** Path of the center web search route. */
161
- declare const qoderWebSearchPath = "/api/v1/webSearch/oneSearch";
162
- declare function getQoderWebSearchUrl(region?: QoderRegion): string;
163
- //#endregion
164
166
  //#region src/qoder/transport/wire/cosy.d.ts
165
167
  interface CosyCredentials {
166
168
  userID: string;
@@ -226,4 +228,4 @@ declare class QoderSearchProvider implements WebSearchProvider {
226
228
  private resolveFallback;
227
229
  }
228
230
  //#endregion
229
- 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
  }
@@ -1000,11 +1083,9 @@ async function validateAndTranslateMessages(messages, systemPrompt, attachments,
1000
1083
  }
1001
1084
  if (toolResults.length > 0 && !hasImage && text.trim().length === 0) continue;
1002
1085
  const wireRole = toolResults.length > 0 ? "user" : message.role;
1003
- const wireToolCallId = wireRole === "tool" ? consumeToolCallId(message, void 0, lastAssistantToolCallIds, output.length) : void 0;
1004
1086
  output.push({
1005
1087
  role: wireRole,
1006
- content: hasImage ? userContent.filter((part) => part !== void 0) : text,
1007
- ...wireToolCallId ? { tool_call_id: wireToolCallId } : {}
1088
+ content: hasImage ? userContent.filter((part) => part !== void 0) : text
1008
1089
  });
1009
1090
  }
1010
1091
  return output;
@@ -1345,11 +1426,13 @@ async function buildQoderRequestBody(options, userId, translatedMessages, model,
1345
1426
  const maxTokens = Math.min(options.maxTokens ?? modelMaxTokens, modelMaxTokens);
1346
1427
  const isReasoning = options.reasoningEffort !== void 0 || (model?.isReasoning ?? false);
1347
1428
  const tools = translateTools(options.tools);
1429
+ const selectedTier = model === void 0 ? void 0 : selectedContextTier(model);
1348
1430
  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, {
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, {
1350
1432
  ...value.tokenCount === void 0 ? {} : { token_count: value.tokenCount },
1351
- ...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 }
1352
1434
  }]));
1435
+ const effectiveBudget = selectedTier?.tokenCount ?? model?.contextWindow;
1353
1436
  let lastUserText = "";
1354
1437
  for (let index = messages.length - 1; index >= 0; index--) if (messages[index].role === "user") {
1355
1438
  lastUserText = wireMessageText(messages[index]);
@@ -1383,6 +1466,7 @@ async function buildQoderRequestBody(options, userId, translatedMessages, model,
1383
1466
  tools,
1384
1467
  parameters: {
1385
1468
  max_tokens: maxTokens,
1469
+ ...effectiveBudget === void 0 ? {} : { context_length: effectiveBudget },
1386
1470
  ...options.reasoningEffort === void 0 ? {} : { reasoning_effort: String(options.reasoningEffort) }
1387
1471
  },
1388
1472
  chat_context: {
@@ -2066,6 +2150,18 @@ function qoderEncodeBody(plaintext) {
2066
2150
  }
2067
2151
  return out;
2068
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;
2069
2165
  function searchAborted(signal, fallback) {
2070
2166
  return new WebError("Web search was cancelled.", "WEB_ABORTED", { cause: signal?.aborted === true ? signal.reason : fallback });
2071
2167
  }
@@ -2084,7 +2180,7 @@ var QoderSearchClient = class {
2084
2180
  this.fetchImpl = options.fetch ?? globalThis.fetch;
2085
2181
  this.region = options.region ?? "global";
2086
2182
  this.logger = options.logger;
2087
- this.timeoutMs = options.timeoutMs ?? 3e4;
2183
+ this.timeoutMs = options.timeoutMs ?? defaultSearchTimeoutMs;
2088
2184
  this.resolveCredentials = options.resolveCredentials;
2089
2185
  this.refreshCredentials = options.refreshCredentials;
2090
2186
  this.now = options.now ?? Date.now;
@@ -2384,6 +2480,7 @@ var QoderThinkingParser = class {
2384
2480
  //#region src/qoder/transport/wire/sse.ts
2385
2481
  /** Decode Qoder's outer SSE envelope and inner model chunks. */
2386
2482
  const doneMarker = "[DONE]";
2483
+ const defaultMaxSseBufferChars = 2 * 1024 * 1024;
2387
2484
  function malformed(message) {
2388
2485
  return new QoderLlmError(message, "MALFORMED_RESPONSE");
2389
2486
  }
@@ -2486,7 +2583,7 @@ async function* parseQoderSse(stream, options = {}) {
2486
2583
  let terminalKind = "stop";
2487
2584
  let sawFinishReason = false;
2488
2585
  let sawContent = false;
2489
- const maxBufferChars = options.maxBufferChars ?? 2097152;
2586
+ const maxBufferChars = options.maxBufferChars ?? defaultMaxSseBufferChars;
2490
2587
  const closeTextual = () => {
2491
2588
  if (activeTextual === void 0) return [];
2492
2589
  const state = activeTextual;
@@ -2750,6 +2847,16 @@ async function* streamQoderChat(options, model, credentials, messages, dependenc
2750
2847
  }, dependencies.streamIdleTimeoutMs);
2751
2848
  };
2752
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
+ }));
2753
2860
  const response = await dependencies.fetch(chatUrl, {
2754
2861
  method: "POST",
2755
2862
  headers: {
@@ -3006,6 +3113,7 @@ const catalogModel = z.object({
3006
3113
  description: z.string(),
3007
3114
  contextWindow: z.number().step(1).min(1),
3008
3115
  maxContextWindow: z.number().step(1).min(1),
3116
+ contextTier: z.string(),
3009
3117
  maxTokens: z.number().step(1).min(1),
3010
3118
  source: z.string(),
3011
3119
  isReasoning: z.boolean(),
@@ -3017,6 +3125,8 @@ const catalogModel = z.object({
3017
3125
  })),
3018
3126
  defaultReasoningEffort: z.string(),
3019
3127
  priceFactor: z.number().min(0),
3128
+ originalPriceFactor: z.number().min(0),
3129
+ isFree: z.boolean(),
3020
3130
  supportsImages: z.boolean(),
3021
3131
  contextOptions: z.dict(z.object({
3022
3132
  tokenCount: z.number().step(1).min(1),
@@ -3036,7 +3146,7 @@ const Config = z.object({
3036
3146
  "disabled"
3037
3147
  ]).default("auto")
3038
3148
  });
3039
- const PluginConfig = Config.volatile();
3149
+ const PluginConfig = typeof Config.volatile === "function" ? Config.volatile() : Config;
3040
3150
  function readConfig(value) {
3041
3151
  return "get" in value ? Config(structuredClone(value.get())) : value;
3042
3152
  }
@@ -3062,10 +3172,11 @@ const QODER_PROVIDER_ID = "dsh-provider-qoder";
3062
3172
  //#region src/dsh/adapter.ts
3063
3173
  /** Thin DSH adapter over the Qoder transport seam. */
3064
3174
  function modelInfo(provider, model) {
3175
+ const rate = formatModelRate(model, true);
3065
3176
  return {
3066
3177
  provider,
3067
3178
  id: model.id,
3068
- name: model.priceFactor === void 0 ? model.name : `${model.name} (${model.priceFactor}x)`,
3179
+ name: rate === void 0 ? model.name : `${model.name} (${rate})`,
3069
3180
  description: model.description,
3070
3181
  inputModalities: model.supportsImages === true ? ["text", "image"] : ["text"]
3071
3182
  };
@@ -3077,6 +3188,12 @@ var QoderAdapter = class extends LlmAdapter {
3077
3188
  providerName;
3078
3189
  onModelsDiscovered;
3079
3190
  discoveries = /* @__PURE__ */ new WeakMap();
3191
+ historyRegion;
3192
+ historyRegionChanged = false;
3193
+ region;
3194
+ sessionTiers = /* @__PURE__ */ new Map();
3195
+ sessions;
3196
+ agents;
3080
3197
  constructor(options) {
3081
3198
  super();
3082
3199
  this.resolveTransport = options.resolveTransport;
@@ -3084,6 +3201,52 @@ var QoderAdapter = class extends LlmAdapter {
3084
3201
  this.providerId = options.providerId ?? "dsh-provider-qoder";
3085
3202
  this.providerName = options.providerName ?? "Qoder";
3086
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;
3087
3250
  }
3088
3251
  providerInfo(provider) {
3089
3252
  return {
@@ -3128,6 +3291,7 @@ var QoderAdapter = class extends LlmAdapter {
3128
3291
  return mergeQoderDiscoveryMetadata(this.catalogModels, this.discoveries.get(this.resolveTransport())?.models ?? []);
3129
3292
  }
3130
3293
  replaceModels(models) {
3294
+ this.observeRegion();
3131
3295
  this.catalogModels = models;
3132
3296
  }
3133
3297
  /** Publish an explicit discovery without letting older in-flight reads overwrite it. */
@@ -3139,16 +3303,34 @@ var QoderAdapter = class extends LlmAdapter {
3139
3303
  }
3140
3304
  resolveModel(provider, modelId, signal) {
3141
3305
  if (signal?.aborted) return Promise.reject(new QoderLlmError("Qoder model resolution was aborted.", "ABORTED"));
3142
- 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) {
3143
3324
  if (configured === void 0) return Promise.resolve({
3144
3325
  provider,
3145
3326
  id: modelId,
3146
3327
  name: modelId,
3147
3328
  inputModalities: ["text"]
3148
3329
  });
3330
+ const contextWindow = effectiveContextWindow(configured);
3149
3331
  return Promise.resolve({
3150
3332
  ...modelInfo(provider, configured),
3151
- ...configured.contextWindow === void 0 ? {} : { context: { contextWindow: configured.contextWindow } },
3333
+ ...contextWindow === void 0 ? {} : { context: { contextWindow } },
3152
3334
  ...configured.maxTokens === void 0 ? {} : { defaultMaxTokens: configured.maxTokens },
3153
3335
  ...configured.reasoningEfforts === void 0 || configured.reasoningEfforts.length === 0 ? {} : { reasoning: {
3154
3336
  efforts: configured.reasoningEfforts.map((effort) => ({
@@ -3162,7 +3344,7 @@ var QoderAdapter = class extends LlmAdapter {
3162
3344
  }
3163
3345
  stream(options) {
3164
3346
  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"));
3347
+ const model = this.resolveEffectiveModelForSession(options.model || "cmodel", options.sessionId);
3166
3348
  return this.resolveTransport().stream(options, model);
3167
3349
  }
3168
3350
  };
@@ -3178,7 +3360,6 @@ var QoderAdapter = class extends LlmAdapter {
3178
3360
  * @module dsh-provider-qoder/dsh/search-provider
3179
3361
  */
3180
3362
  const QODER_SEARCH_PROVIDER_ID = "qoder";
3181
- const QODER_MODEL_PROVIDER_ROUTE = QODER_PROVIDER_ID;
3182
3363
  var QoderSearchProvider = class {
3183
3364
  id = QODER_SEARCH_PROVIDER_ID;
3184
3365
  ctx;
@@ -3202,14 +3383,14 @@ var QoderSearchProvider = class {
3202
3383
  const agent = (this.ctx.get("agents") ?? this.ctx.agents)?.currentInitiator?.();
3203
3384
  const defaultModelService = this.ctx.get("agentDefaultModel");
3204
3385
  const providerRoute = agent?.options?.provider ?? defaultModelService?.get?.()?.provider;
3205
- if (mode === "always" || providerRoute === QODER_MODEL_PROVIDER_ROUTE) {
3386
+ if (mode === "always" || providerRoute === "dsh-provider-qoder") {
3206
3387
  if (this.resolveTransport !== void 0) return this.resolveTransport().searchWeb(request, signal);
3207
3388
  if (this.searchClient !== void 0) return this.searchClient.search(request, signal);
3208
3389
  throw new WebError("No Qoder transport configured for web search.", "WEB_PROVIDER_ERROR");
3209
3390
  }
3210
3391
  const fallback = this.resolveFallback();
3211
3392
  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");
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");
3213
3394
  }
3214
3395
  resolveFallback() {
3215
3396
  if (this.fallbackProvider && this.fallbackProvider.available()) return this.fallbackProvider;
@@ -3266,8 +3447,12 @@ function bindQoderSettings(owner, ctx, input, options) {
3266
3447
  * channel at `/api`, and every exact Fetch route lives directly below it.
3267
3448
  */
3268
3449
  const qoderRpcApiPath = `/api/qoder-subscription`;
3269
- /** Endpoints the account card and the model catalog call. */
3270
- 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
+ ];
3271
3456
  /** Absolute Fetch-route path of one endpoint. */
3272
3457
  function qoderRpcPath(endpoint) {
3273
3458
  return `${qoderRpcApiPath}/${endpoint}`;
@@ -3418,8 +3603,11 @@ function apply(ctx, input = {}) {
3418
3603
  const adapter = new QoderAdapter({
3419
3604
  resolveTransport: () => activeTransport,
3420
3605
  models: initial.models,
3606
+ region: () => activeTransportConfig.region,
3421
3607
  providerId: providerQoder,
3422
3608
  providerName: "Qoder",
3609
+ sessions: ctx.get("sessions"),
3610
+ agents: ctx.get("agents"),
3423
3611
  onModelsDiscovered: (transport, models) => {
3424
3612
  if (transport !== activeTransport || ctx.fiber.state === fiberUnloading || ctx.fiber.state === fiberDisposed) return;
3425
3613
  const region = activeTransportConfig.region;
@@ -3532,6 +3720,17 @@ function apply(ctx, input = {}) {
3532
3720
  }
3533
3721
  };
3534
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
+ }
3535
3734
  const force = typeof payload === "object" && payload !== null && "force" in payload ? payload.force === true : false;
3536
3735
  logger?.debug?.("[Qoder RPC] Reading subscriber account", { force });
3537
3736
  const outcome = await executeRpc("load Qoder account", () => activeTransport.readAccount({
@@ -3573,4 +3772,4 @@ function apply(ctx, input = {}) {
3573
3772
  });
3574
3773
  }
3575
3774
  //#endregion
3576
- 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 };
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.0",
4
+ "version": "0.4.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },