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/README.md +13 -23
- package/README_CN.md +13 -26
- package/lib/client.js +674 -252
- package/lib/index.d.ts +11 -51
- package/lib/index.js +282 -69
- package/package.json +19 -16
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
|
-
|
|
44
|
-
...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 =
|
|
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
|
|
220
|
+
if (advertised === void 0) return cloneCatalogModel(model);
|
|
122
221
|
const merged = { ...model };
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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 ??
|
|
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 ??
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
...
|
|
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.
|
|
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
|
-
|
|
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
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
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 "${
|
|
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
|
|
3270
|
-
const qoderRpcEndpoints = [
|
|
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
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
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
|
-
|
|
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
|
|
3774
|
+
const originalSearch = webCtx.web.search;
|
|
3558
3775
|
webCtx.effect(() => {
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
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,
|
|
3789
|
+
export { PluginConfig as Config, QODER_SEARCH_PROVIDER_ID, QoderSearchProvider, apply, inject, name };
|