dsh-provider-qoder 0.2.0 → 0.3.0
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 +9 -8
- package/README_CN.md +9 -8
- package/cordis.patch.yml +5 -0
- package/lib/client.js +408 -176
- package/lib/index.d.ts +113 -2
- package/lib/index.js +638 -79
- package/package.json +4 -2
package/lib/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import crypto, { createHash } from "node:crypto";
|
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
|
+
import { WebError } from "@deepseek-ai/dsh-web";
|
|
7
8
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
8
9
|
//#region src/qoder/catalog.ts
|
|
9
10
|
const discoveredMetadataKeys = [
|
|
@@ -234,6 +235,12 @@ function getQoderUserStatusUrl(region = "global") {
|
|
|
234
235
|
const { openApiUrl } = resolveQoderEndpoints(region);
|
|
235
236
|
return `${openApiUrl}/api/v3/user/status`;
|
|
236
237
|
}
|
|
238
|
+
/** Path of the center web search route. */
|
|
239
|
+
const qoderWebSearchPath = "/api/v1/webSearch/oneSearch";
|
|
240
|
+
function getQoderWebSearchUrl(region = "global") {
|
|
241
|
+
const { centerUrl } = resolveQoderEndpoints(region);
|
|
242
|
+
return `${centerUrl.replace(/\/+$/u, "")}/algo${qoderWebSearchPath}?Encode=1`;
|
|
243
|
+
}
|
|
237
244
|
//#endregion
|
|
238
245
|
//#region src/qoder/errors.ts
|
|
239
246
|
/**
|
|
@@ -333,7 +340,9 @@ function redactLogValue(value, depth = 0) {
|
|
|
333
340
|
if (value instanceof Error) return {
|
|
334
341
|
name: value.name,
|
|
335
342
|
message: redactString(value.message),
|
|
336
|
-
..."code" in value ? { code: redactLogValue(value.code, depth + 1) } : {}
|
|
343
|
+
..."code" in value ? { code: redactLogValue(value.code, depth + 1) } : {},
|
|
344
|
+
..."cause" in value ? { cause: redactLogValue(value.cause, depth + 1) } : {},
|
|
345
|
+
...value instanceof AggregateError ? { errors: redactLogValue(value.errors, depth + 1) } : {}
|
|
337
346
|
};
|
|
338
347
|
if (Array.isArray(value)) return value.slice(0, maxEntries).map((item) => redactLogValue(item, depth + 1));
|
|
339
348
|
return Object.fromEntries(Object.entries(value).slice(0, maxEntries).map(([key, item]) => {
|
|
@@ -448,6 +457,7 @@ function buildAuthHeaders(body, requestURL, creds) {
|
|
|
448
457
|
"X-Request-Id": crypto.randomUUID()
|
|
449
458
|
};
|
|
450
459
|
}
|
|
460
|
+
const defaultResponseHeaderTimeoutMs = 6e4;
|
|
451
461
|
const defaultMaxJsonBytes = 2 * 1024 * 1024;
|
|
452
462
|
const defaultMaxErrorBytes = 16 * 1024;
|
|
453
463
|
const metadataRetryBaseDelayMs = 200;
|
|
@@ -823,10 +833,7 @@ function validateMessageShapes(messages) {
|
|
|
823
833
|
if (message.role !== "assistant") throw unsupported("Qoder tool calls are valid only in assistant messages.");
|
|
824
834
|
continue;
|
|
825
835
|
}
|
|
826
|
-
if (block.type === "reasoning")
|
|
827
|
-
if (message.role !== "assistant") throw unsupported("Qoder historical reasoning is valid only in assistant messages.");
|
|
828
|
-
continue;
|
|
829
|
-
}
|
|
836
|
+
if (block.type === "reasoning") continue;
|
|
830
837
|
throw unsupported(`Qoder transport encountered unsupported block type: ${String(block.type)}`);
|
|
831
838
|
}
|
|
832
839
|
}
|
|
@@ -881,6 +888,7 @@ async function validateAndTranslateMessages(messages, systemPrompt, attachments,
|
|
|
881
888
|
uploader: pipeline?.uploader,
|
|
882
889
|
credentials: pipeline?.credentials
|
|
883
890
|
};
|
|
891
|
+
const preserveThinking = pipeline?.preserveThinking ?? true;
|
|
884
892
|
const output = [];
|
|
885
893
|
if (typeof systemPrompt === "string" && systemPrompt.trim().length > 0) output.push({
|
|
886
894
|
role: "system",
|
|
@@ -911,6 +919,7 @@ async function validateAndTranslateMessages(messages, systemPrompt, attachments,
|
|
|
911
919
|
continue;
|
|
912
920
|
}
|
|
913
921
|
let text = "";
|
|
922
|
+
let reasoningText = "";
|
|
914
923
|
const userContent = [];
|
|
915
924
|
const pendingImages = [];
|
|
916
925
|
let hasImage = false;
|
|
@@ -944,14 +953,19 @@ async function validateAndTranslateMessages(messages, systemPrompt, attachments,
|
|
|
944
953
|
});
|
|
945
954
|
continue;
|
|
946
955
|
}
|
|
947
|
-
if (block.type === "reasoning")
|
|
956
|
+
if (block.type === "reasoning") {
|
|
957
|
+
if (message.role === "assistant") reasoningText += block.text;
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
948
960
|
}
|
|
949
961
|
if (message.role === "assistant") {
|
|
950
|
-
|
|
962
|
+
const hasReasoning = preserveThinking && reasoningText.length > 0;
|
|
963
|
+
if (!text && toolCalls.length === 0 && !hasReasoning) continue;
|
|
951
964
|
output.push({
|
|
952
965
|
role: "assistant",
|
|
953
966
|
content: text || " ",
|
|
954
|
-
...toolCalls.length === 0 ? {} : { tool_calls: toolCalls }
|
|
967
|
+
...toolCalls.length === 0 ? {} : { tool_calls: toolCalls },
|
|
968
|
+
...hasReasoning ? { reasoning_content: reasoningText } : {}
|
|
955
969
|
});
|
|
956
970
|
continue;
|
|
957
971
|
}
|
|
@@ -1422,13 +1436,20 @@ function normalizeQuota(raw) {
|
|
|
1422
1436
|
unit
|
|
1423
1437
|
};
|
|
1424
1438
|
}
|
|
1439
|
+
/**
|
|
1440
|
+
* Normalize an epoch-millis (or parseable date string) expiry to ISO-8601.
|
|
1441
|
+
*
|
|
1442
|
+
* The provider uses the int64 maximum as a "never expires" sentinel, which is
|
|
1443
|
+
* outside the `Date` range: an unusable value resolves to `undefined` instead
|
|
1444
|
+
* of throwing, so one bad field cannot abort the whole account read.
|
|
1445
|
+
*/
|
|
1425
1446
|
function normalizeExpiresAt(rawExpires) {
|
|
1426
1447
|
if (rawExpires === void 0 || rawExpires === null) return void 0;
|
|
1427
|
-
|
|
1428
|
-
if (
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1448
|
+
const parsed = typeof rawExpires === "number" ? rawExpires : typeof rawExpires === "string" && rawExpires.length > 0 ? Date.parse(rawExpires) : NaN;
|
|
1449
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return void 0;
|
|
1450
|
+
const date = new Date(parsed);
|
|
1451
|
+
if (!Number.isFinite(date.getTime())) return void 0;
|
|
1452
|
+
return date.toISOString();
|
|
1432
1453
|
}
|
|
1433
1454
|
function asString(value) {
|
|
1434
1455
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
@@ -1443,6 +1464,76 @@ function asNumber(value) {
|
|
|
1443
1464
|
if (Number.isFinite(num)) return num;
|
|
1444
1465
|
}
|
|
1445
1466
|
}
|
|
1467
|
+
/**
|
|
1468
|
+
* The provider names a resource package with internal identifiers (campaign
|
|
1469
|
+
* ids) that must never reach the UI, so the subscriber-facing copy only ever
|
|
1470
|
+
* comes from `displayLabels`.
|
|
1471
|
+
*/
|
|
1472
|
+
function findDisplayLabel(raw, dimension) {
|
|
1473
|
+
if (!Array.isArray(raw)) return void 0;
|
|
1474
|
+
for (const entry of raw) {
|
|
1475
|
+
if (!entry || typeof entry !== "object") continue;
|
|
1476
|
+
const obj = entry;
|
|
1477
|
+
if (asString(obj.dimension) === dimension) return obj;
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Normalize one `displayLabels` entry into language-tagged copy.
|
|
1482
|
+
*
|
|
1483
|
+
* The provider's value lives under `valueI18n`/`value_i18n`, keyed by language
|
|
1484
|
+
* tag, plus `value` as the untagged fallback. An entry with neither is dropped
|
|
1485
|
+
* rather than surfaced as an empty label.
|
|
1486
|
+
*/
|
|
1487
|
+
function normalizeLocalizedText(raw) {
|
|
1488
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
1489
|
+
const obj = raw;
|
|
1490
|
+
const values = {};
|
|
1491
|
+
const localized = obj.valueI18n ?? obj.value_i18n;
|
|
1492
|
+
if (localized && typeof localized === "object") for (const [locale, text] of Object.entries(localized)) {
|
|
1493
|
+
const resolved = asString(text);
|
|
1494
|
+
if (resolved !== void 0) values[locale] = resolved;
|
|
1495
|
+
}
|
|
1496
|
+
const fallback = asString(obj.value);
|
|
1497
|
+
if (fallback === void 0 && Object.keys(values).length === 0) return void 0;
|
|
1498
|
+
return {
|
|
1499
|
+
values,
|
|
1500
|
+
fallback: fallback ?? ""
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1504
|
+
* Normalize the dedicated (entitlement-scoped) resource packages.
|
|
1505
|
+
*
|
|
1506
|
+
* Copy is read from `displayLabels` only, never from `name`/`description`,
|
|
1507
|
+
* which carry internal campaign identifiers. Entries without a usable size are
|
|
1508
|
+
* dropped and a non-array value yields `undefined`, so an unexpected payload
|
|
1509
|
+
* never fails the account read that also feeds the model catalog.
|
|
1510
|
+
*/
|
|
1511
|
+
function normalizeResourcePackages(raw) {
|
|
1512
|
+
if (!Array.isArray(raw)) return void 0;
|
|
1513
|
+
const packages = [];
|
|
1514
|
+
for (const entry of raw) {
|
|
1515
|
+
if (!entry || typeof entry !== "object") continue;
|
|
1516
|
+
const obj = entry;
|
|
1517
|
+
const quota = normalizeQuota(obj);
|
|
1518
|
+
if (quota === void 0 || quota.total <= 0 && quota.used <= 0 && quota.remaining <= 0) continue;
|
|
1519
|
+
const id = asString(obj.id);
|
|
1520
|
+
const title = normalizeLocalizedText(findDisplayLabel(obj.displayLabels ?? obj.display_labels, "title"));
|
|
1521
|
+
const description = normalizeLocalizedText(findDisplayLabel(obj.displayLabels ?? obj.display_labels, "description"));
|
|
1522
|
+
const expiresAt = normalizeExpiresAt(obj.expiresAt);
|
|
1523
|
+
const available = asBoolean(obj.available);
|
|
1524
|
+
const status = asString(obj.status);
|
|
1525
|
+
packages.push({
|
|
1526
|
+
...id !== void 0 ? { id } : {},
|
|
1527
|
+
...title !== void 0 ? { title } : {},
|
|
1528
|
+
...description !== void 0 ? { description } : {},
|
|
1529
|
+
...quota,
|
|
1530
|
+
...expiresAt !== void 0 ? { expiresAt } : {},
|
|
1531
|
+
...available !== void 0 ? { available } : {},
|
|
1532
|
+
...status !== void 0 ? { status } : {}
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
return packages.length > 0 ? packages : void 0;
|
|
1536
|
+
}
|
|
1446
1537
|
function normalizeOrganization(raw) {
|
|
1447
1538
|
if (!raw || typeof raw !== "object") return void 0;
|
|
1448
1539
|
const obj = raw;
|
|
@@ -1565,6 +1656,14 @@ var QoderUsageReader = class {
|
|
|
1565
1656
|
if (pat) this.cache.delete(`${this.region}:${opaqueCredentialKey(pat)}`);
|
|
1566
1657
|
else this.cache.clear();
|
|
1567
1658
|
}
|
|
1659
|
+
/**
|
|
1660
|
+
* Read `GET /api/v2/quota/usage` and normalize it for the host/client seam.
|
|
1661
|
+
*
|
|
1662
|
+
* The `raw` echo keeps the upstream fields for diagnostics, but never
|
|
1663
|
+
* `dedicatedResourcePackages`: those entries carry the campaign identifiers
|
|
1664
|
+
* that the normalized list deliberately discards, and this payload crosses
|
|
1665
|
+
* the seam into the browser.
|
|
1666
|
+
*/
|
|
1568
1667
|
async fetchUsage(jobToken, signal) {
|
|
1569
1668
|
const data = await openApiJsonRequest(this.fetchImpl, {
|
|
1570
1669
|
url: getQoderUsageUrl(this.region),
|
|
@@ -1575,13 +1674,17 @@ var QoderUsageReader = class {
|
|
|
1575
1674
|
operation: "Usage",
|
|
1576
1675
|
logCategory: "account.usage"
|
|
1577
1676
|
});
|
|
1677
|
+
const dedicatedResourcePackages = normalizeResourcePackages(data.dedicatedResourcePackages);
|
|
1678
|
+
const rawUsage = { ...data };
|
|
1679
|
+
delete rawUsage.dedicatedResourcePackages;
|
|
1578
1680
|
return {
|
|
1579
1681
|
userQuota: normalizeQuota(data.userQuota),
|
|
1580
1682
|
orgResourcePackage: normalizeQuota(data.orgResourcePackage),
|
|
1683
|
+
...dedicatedResourcePackages !== void 0 ? { dedicatedResourcePackages } : {},
|
|
1581
1684
|
totalUsagePercentage: typeof data.totalUsagePercentage === "number" ? data.totalUsagePercentage : void 0,
|
|
1582
1685
|
isQuotaExceeded: typeof data.isQuotaExceeded === "boolean" ? data.isQuotaExceeded : false,
|
|
1583
1686
|
expiresAt: normalizeExpiresAt(data.expiresAt),
|
|
1584
|
-
raw:
|
|
1687
|
+
raw: rawUsage
|
|
1585
1688
|
};
|
|
1586
1689
|
}
|
|
1587
1690
|
async fetchPlan(jobToken, signal) {
|
|
@@ -1655,6 +1758,163 @@ function qoderEncodeBody(plaintext) {
|
|
|
1655
1758
|
}
|
|
1656
1759
|
return out;
|
|
1657
1760
|
}
|
|
1761
|
+
function searchAborted(signal, fallback) {
|
|
1762
|
+
return new WebError("Web search was cancelled.", "WEB_ABORTED", { cause: signal?.aborted === true ? signal.reason : fallback });
|
|
1763
|
+
}
|
|
1764
|
+
function isAbortError(error) {
|
|
1765
|
+
return error instanceof DOMException && error.name === "AbortError";
|
|
1766
|
+
}
|
|
1767
|
+
var QoderSearchClient = class {
|
|
1768
|
+
fetchImpl;
|
|
1769
|
+
region;
|
|
1770
|
+
logger;
|
|
1771
|
+
timeoutMs;
|
|
1772
|
+
resolveCredentials;
|
|
1773
|
+
refreshCredentials;
|
|
1774
|
+
now;
|
|
1775
|
+
constructor(options) {
|
|
1776
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
1777
|
+
this.region = options.region ?? "global";
|
|
1778
|
+
this.logger = options.logger;
|
|
1779
|
+
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
1780
|
+
this.resolveCredentials = options.resolveCredentials;
|
|
1781
|
+
this.refreshCredentials = options.refreshCredentials;
|
|
1782
|
+
this.now = options.now ?? Date.now;
|
|
1783
|
+
}
|
|
1784
|
+
async search(request, signal) {
|
|
1785
|
+
if (signal?.aborted) throw searchAborted(signal);
|
|
1786
|
+
let credentials = await this.resolveCredentials(signal);
|
|
1787
|
+
if (signal?.aborted) throw searchAborted(signal);
|
|
1788
|
+
const first = await this.attempt(request, credentials, signal);
|
|
1789
|
+
if (first.result !== void 0) return first.result;
|
|
1790
|
+
if (first.retryable && this.refreshCredentials !== void 0) {
|
|
1791
|
+
this.logger?.debug?.("[web-search] refreshing credentials before retry", {
|
|
1792
|
+
region: this.region,
|
|
1793
|
+
reason: first.reason
|
|
1794
|
+
});
|
|
1795
|
+
try {
|
|
1796
|
+
credentials = await this.refreshCredentials(signal);
|
|
1797
|
+
} catch (error) {
|
|
1798
|
+
if (signal?.aborted) throw searchAborted(signal, error);
|
|
1799
|
+
throw new WebError(`Qoder credential refresh failed: ${String(error)}`, "WEB_PROVIDER_ERROR", { cause: error });
|
|
1800
|
+
}
|
|
1801
|
+
const second = await this.attempt(request, credentials, signal);
|
|
1802
|
+
if (second.result !== void 0) return second.result;
|
|
1803
|
+
throw new WebError(`Qoder web search failed: ${second.reason}`, "WEB_PROVIDER_ERROR");
|
|
1804
|
+
}
|
|
1805
|
+
throw new WebError(`Qoder web search failed: ${first.reason}`, "WEB_PROVIDER_ERROR");
|
|
1806
|
+
}
|
|
1807
|
+
async attempt(request, credentials, signal) {
|
|
1808
|
+
const url = getQoderWebSearchUrl(this.region);
|
|
1809
|
+
const innerPayload = {
|
|
1810
|
+
query: request.query,
|
|
1811
|
+
timeRange: "NoLimit",
|
|
1812
|
+
contents: {
|
|
1813
|
+
mainText: false,
|
|
1814
|
+
markdownText: false,
|
|
1815
|
+
summary: false
|
|
1816
|
+
}
|
|
1817
|
+
};
|
|
1818
|
+
const encodedBody = qoderEncodeBody(JSON.stringify({
|
|
1819
|
+
payload: JSON.stringify(innerPayload),
|
|
1820
|
+
encodeVersion: "1"
|
|
1821
|
+
}));
|
|
1822
|
+
const timeout = AbortSignal.timeout(this.timeoutMs);
|
|
1823
|
+
const requestSignal = signal !== void 0 ? AbortSignal.any([signal, timeout]) : timeout;
|
|
1824
|
+
const startedAt = this.now();
|
|
1825
|
+
this.logger?.debug?.("[web-search] started", {
|
|
1826
|
+
region: this.region,
|
|
1827
|
+
url,
|
|
1828
|
+
query: request.query
|
|
1829
|
+
});
|
|
1830
|
+
let response;
|
|
1831
|
+
try {
|
|
1832
|
+
const authHeaders = buildAuthHeaders(encodedBody, url, credentials);
|
|
1833
|
+
response = await this.fetchImpl(url, {
|
|
1834
|
+
method: "POST",
|
|
1835
|
+
headers: {
|
|
1836
|
+
...authHeaders,
|
|
1837
|
+
"Content-Type": "application/json",
|
|
1838
|
+
"Accept": "application/json",
|
|
1839
|
+
"Accept-Encoding": "identity"
|
|
1840
|
+
},
|
|
1841
|
+
body: encodedBody,
|
|
1842
|
+
signal: requestSignal
|
|
1843
|
+
});
|
|
1844
|
+
} catch (error) {
|
|
1845
|
+
if (signal?.aborted) throw searchAborted(signal, error);
|
|
1846
|
+
if (timeout.aborted || isAbortError(error)) return {
|
|
1847
|
+
retryable: false,
|
|
1848
|
+
reason: "request timed out"
|
|
1849
|
+
};
|
|
1850
|
+
this.logger?.warn?.("[web-search] network failure", { region: this.region }, redactLogValue(error));
|
|
1851
|
+
return {
|
|
1852
|
+
retryable: false,
|
|
1853
|
+
reason: `network failure: ${String(error)}`
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
const elapsedMs = this.now() - startedAt;
|
|
1857
|
+
this.logger?.debug?.("[web-search] response received", {
|
|
1858
|
+
status: response.status,
|
|
1859
|
+
elapsedMs
|
|
1860
|
+
});
|
|
1861
|
+
if (!response.ok) return {
|
|
1862
|
+
retryable: response.status === 401 || response.status === 403,
|
|
1863
|
+
reason: `HTTP ${response.status} ${response.statusText}`.trim()
|
|
1864
|
+
};
|
|
1865
|
+
let text;
|
|
1866
|
+
try {
|
|
1867
|
+
text = await readLimitedText(response, defaultMaxJsonBytes, "Qoder web search response");
|
|
1868
|
+
} catch (error) {
|
|
1869
|
+
if (signal?.aborted) throw searchAborted(signal, error);
|
|
1870
|
+
return {
|
|
1871
|
+
retryable: false,
|
|
1872
|
+
reason: `failed to read response body: ${String(error)}`
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
let parsed;
|
|
1876
|
+
try {
|
|
1877
|
+
parsed = JSON.parse(text);
|
|
1878
|
+
} catch (error) {
|
|
1879
|
+
return {
|
|
1880
|
+
retryable: false,
|
|
1881
|
+
reason: `invalid JSON response: ${String(error)}`
|
|
1882
|
+
};
|
|
1883
|
+
}
|
|
1884
|
+
if (parsed.errorCode != null && parsed.errorCode !== 0) return {
|
|
1885
|
+
retryable: false,
|
|
1886
|
+
reason: `backend error code ${parsed.errorCode}: ${parsed.errorMsg ?? "unknown"}`
|
|
1887
|
+
};
|
|
1888
|
+
const rawItems = parsed.pageItems ?? [];
|
|
1889
|
+
const sources = [];
|
|
1890
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1891
|
+
for (const item of rawItems) {
|
|
1892
|
+
if (!item.link || item.link.trim().length === 0) continue;
|
|
1893
|
+
const link = item.link.trim();
|
|
1894
|
+
if (seen.has(link)) continue;
|
|
1895
|
+
seen.add(link);
|
|
1896
|
+
sources.push({
|
|
1897
|
+
url: link,
|
|
1898
|
+
...item.title !== void 0 && item.title.trim().length > 0 ? { title: item.title.trim() } : {},
|
|
1899
|
+
...item.snippet !== void 0 && item.snippet.trim().length > 0 ? { snippet: item.snippet.trim() } : {},
|
|
1900
|
+
...item.publishedAt !== void 0 && item.publishedAt.trim().length > 0 ? { publishedAt: item.publishedAt.trim() } : {}
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
this.logger?.debug?.("[web-search] succeeded", {
|
|
1904
|
+
region: this.region,
|
|
1905
|
+
resultsCount: sources.length,
|
|
1906
|
+
elapsedMs
|
|
1907
|
+
});
|
|
1908
|
+
return {
|
|
1909
|
+
result: {
|
|
1910
|
+
sources,
|
|
1911
|
+
truncated: false
|
|
1912
|
+
},
|
|
1913
|
+
retryable: false,
|
|
1914
|
+
reason: ""
|
|
1915
|
+
};
|
|
1916
|
+
}
|
|
1917
|
+
};
|
|
1658
1918
|
//#endregion
|
|
1659
1919
|
//#region src/qoder/transport/wire/thinking.ts
|
|
1660
1920
|
const thinkingTags = [{
|
|
@@ -1862,10 +2122,18 @@ function finishKind(value) {
|
|
|
1862
2122
|
if (value === void 0 || value === null || value === "") return void 0;
|
|
1863
2123
|
if (value === "stop") return "stop";
|
|
1864
2124
|
if (value === "length") return "max-tokens";
|
|
1865
|
-
if (value === "tool_calls" || value === "toolUse") return "tool-calls";
|
|
2125
|
+
if (value === "tool_calls" || value === "toolUse" || value === "function_call") return "tool-calls";
|
|
1866
2126
|
if (value === "content_filter") throw new QoderLlmError("Qoder blocked the response through its content filter.", "PROVIDER_ERROR");
|
|
1867
2127
|
throw malformed(`Qoder returned unknown finish reason "${value}".`);
|
|
1868
2128
|
}
|
|
2129
|
+
/** Whether a choice delta streams content, reasoning, or tool-call data. Control-only frames do not. */
|
|
2130
|
+
function carriesStreamDelta(delta) {
|
|
2131
|
+
if (delta == null) return false;
|
|
2132
|
+
if (Array.isArray(delta.tool_calls) && delta.tool_calls.length > 0) return true;
|
|
2133
|
+
if (typeof delta.content === "string" && delta.content) return true;
|
|
2134
|
+
if (typeof delta.reasoning_content === "string" && delta.reasoning_content) return true;
|
|
2135
|
+
return false;
|
|
2136
|
+
}
|
|
1869
2137
|
async function* parseQoderSse(stream, options = {}) {
|
|
1870
2138
|
const reader = stream.getReader();
|
|
1871
2139
|
const decoder = new TextDecoder();
|
|
@@ -1878,6 +2146,7 @@ async function* parseQoderSse(stream, options = {}) {
|
|
|
1878
2146
|
let activeTextual;
|
|
1879
2147
|
let pendingUsage;
|
|
1880
2148
|
let terminalKind = "stop";
|
|
2149
|
+
let sawFinishReason = false;
|
|
1881
2150
|
let sawContent = false;
|
|
1882
2151
|
const maxBufferChars = options.maxBufferChars ?? 2097152;
|
|
1883
2152
|
const closeTextual = () => {
|
|
@@ -1981,7 +2250,11 @@ async function* parseQoderSse(stream, options = {}) {
|
|
|
1981
2250
|
const innerChunk = parseInner(body);
|
|
1982
2251
|
pendingUsage = tokenUsage(innerChunk) ?? pendingUsage;
|
|
1983
2252
|
for (const choice of innerChunk.choices ?? []) {
|
|
1984
|
-
|
|
2253
|
+
const kind = finishKind(choice.finish_reason);
|
|
2254
|
+
if (kind !== void 0) {
|
|
2255
|
+
terminalKind = kind;
|
|
2256
|
+
sawFinishReason = true;
|
|
2257
|
+
} else if (carriesStreamDelta(choice.delta)) sawFinishReason = false;
|
|
1985
2258
|
const delta = choice.delta;
|
|
1986
2259
|
if (!delta) continue;
|
|
1987
2260
|
if (typeof delta.reasoning_content === "string" && delta.reasoning_content) for (const segment of thinkingParser.pushReasoning(delta.reasoning_content)) for (const chunk of appendSegment(segment)) yield chunk;
|
|
@@ -2002,16 +2275,22 @@ async function* parseQoderSse(stream, options = {}) {
|
|
|
2002
2275
|
};
|
|
2003
2276
|
toolCalls.set(upstreamIndex, state);
|
|
2004
2277
|
}
|
|
2005
|
-
if (rawCall.id !== void 0) {
|
|
2006
|
-
|
|
2278
|
+
if (rawCall.id !== void 0) if (rawCall.id === "" || rawCall.id === null) {} else if (typeof rawCall.id !== "string") throw malformed("Qoder tool call has an invalid id.");
|
|
2279
|
+
else {
|
|
2007
2280
|
if (state.id && state.id !== rawCall.id) throw malformed("Qoder changed a streamed tool-call id.");
|
|
2008
2281
|
state.id = rawCall.id;
|
|
2009
2282
|
}
|
|
2283
|
+
let hasNewName = false;
|
|
2010
2284
|
if (rawCall.function?.name !== void 0) {
|
|
2011
2285
|
const name = rawCall.function.name;
|
|
2012
|
-
if (
|
|
2013
|
-
|
|
2014
|
-
|
|
2286
|
+
if (name === "" || name === null) {} else if (typeof name !== "string") throw malformed("Qoder tool call has an invalid name.");
|
|
2287
|
+
else {
|
|
2288
|
+
if (state.name && state.name !== name) throw malformed("Qoder changed a streamed tool-call name.");
|
|
2289
|
+
if (state.name !== name) {
|
|
2290
|
+
state.name = name;
|
|
2291
|
+
hasNewName = true;
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2015
2294
|
}
|
|
2016
2295
|
if (rawCall.function?.arguments !== void 0) {
|
|
2017
2296
|
if (typeof rawCall.function.arguments !== "string") throw malformed("Qoder tool-call arguments delta is not a string.");
|
|
@@ -2029,7 +2308,7 @@ async function* parseQoderSse(stream, options = {}) {
|
|
|
2029
2308
|
...state.name ? { name: state.name } : {},
|
|
2030
2309
|
argumentsDelta
|
|
2031
2310
|
};
|
|
2032
|
-
} else if (wasOpen && state.blockIndex !== void 0 &&
|
|
2311
|
+
} else if (wasOpen && state.blockIndex !== void 0 && hasNewName) yield {
|
|
2033
2312
|
type: "tool-call-delta",
|
|
2034
2313
|
index: state.blockIndex,
|
|
2035
2314
|
id: ToolCallId(state.id),
|
|
@@ -2045,7 +2324,7 @@ async function* parseQoderSse(stream, options = {}) {
|
|
|
2045
2324
|
await reader.cancel().catch(() => void 0);
|
|
2046
2325
|
reader.releaseLock();
|
|
2047
2326
|
}
|
|
2048
|
-
if (!sawDone) throw new QoderLlmError("SSE stream ended prematurely without [DONE].", "TRANSPORT");
|
|
2327
|
+
if (!sawDone && !sawFinishReason) throw new QoderLlmError("SSE stream ended prematurely without [DONE].", "TRANSPORT");
|
|
2049
2328
|
for (const segment of thinkingParser.finish()) for (const chunk of appendSegment(segment)) yield chunk;
|
|
2050
2329
|
for (const chunk of closeTextual()) yield chunk;
|
|
2051
2330
|
for (const [, state] of [...toolCalls.entries()].sort(([left], [right]) => left - right)) {
|
|
@@ -2113,7 +2392,10 @@ async function* streamQoderChat(options, model, credentials, messages, dependenc
|
|
|
2113
2392
|
let headerTimedOut = false;
|
|
2114
2393
|
let idleTimedOut = false;
|
|
2115
2394
|
let idleTimer;
|
|
2395
|
+
let chunkCount = 0;
|
|
2396
|
+
let reqId;
|
|
2116
2397
|
const startedAt = performance.now();
|
|
2398
|
+
let lastActivityAt = startedAt;
|
|
2117
2399
|
const onCallerAbort = () => requestController.abort(options.signal?.reason);
|
|
2118
2400
|
options.signal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
2119
2401
|
const headerTimer = setTimeout(() => {
|
|
@@ -2121,6 +2403,7 @@ async function* streamQoderChat(options, model, credentials, messages, dependenc
|
|
|
2121
2403
|
requestController.abort("response header timeout");
|
|
2122
2404
|
}, dependencies.responseHeaderTimeoutMs);
|
|
2123
2405
|
const resetIdleTimer = () => {
|
|
2406
|
+
lastActivityAt = performance.now();
|
|
2124
2407
|
if (idleTimer !== void 0) clearTimeout(idleTimer);
|
|
2125
2408
|
idleTimer = setTimeout(() => {
|
|
2126
2409
|
idleTimedOut = true;
|
|
@@ -2144,34 +2427,79 @@ async function* streamQoderChat(options, model, credentials, messages, dependenc
|
|
|
2144
2427
|
signal: requestController.signal
|
|
2145
2428
|
});
|
|
2146
2429
|
clearTimeout(headerTimer);
|
|
2430
|
+
reqId = qoderRequestId(response.headers);
|
|
2147
2431
|
dependencies.logger?.debug?.("[Qoder Stream] Response headers received", {
|
|
2148
2432
|
region: dependencies.region,
|
|
2149
2433
|
status: response.status,
|
|
2150
2434
|
durationMs: Math.round(performance.now() - startedAt),
|
|
2151
|
-
...
|
|
2435
|
+
...reqId === void 0 ? {} : { requestId: reqId }
|
|
2152
2436
|
});
|
|
2153
2437
|
resetIdleTimer();
|
|
2154
2438
|
if (!response.ok) throw qoderHttpError(`Qoder upstream service returned HTTP ${response.status}.`, response);
|
|
2155
2439
|
if (!response.body) throw new QoderLlmError("Qoder response contains no readable body stream.", "EMPTY_RESPONSE");
|
|
2156
|
-
|
|
2440
|
+
let firstChunkDurationMs;
|
|
2441
|
+
let tokenUsage;
|
|
2442
|
+
let finishReason;
|
|
2443
|
+
const streamStartedAt = performance.now();
|
|
2444
|
+
for await (const chunk of parseQoderSse(response.body, { onActivity: resetIdleTimer })) {
|
|
2445
|
+
chunkCount++;
|
|
2446
|
+
if (firstChunkDurationMs === void 0) {
|
|
2447
|
+
firstChunkDurationMs = Math.round(performance.now() - startedAt);
|
|
2448
|
+
dependencies.logger?.debug?.("[Qoder Stream] First chunk received", {
|
|
2449
|
+
durationMs: firstChunkDurationMs,
|
|
2450
|
+
...reqId === void 0 ? {} : { requestId: reqId }
|
|
2451
|
+
});
|
|
2452
|
+
}
|
|
2453
|
+
if (chunk.type === "usage") tokenUsage = chunk.usage;
|
|
2454
|
+
if (chunk.type === "finish") finishReason = chunk.reason.kind;
|
|
2455
|
+
yield chunk;
|
|
2456
|
+
}
|
|
2457
|
+
dependencies.logger?.debug?.("[Qoder Stream] Stream completed", {
|
|
2458
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
2459
|
+
streamDurationMs: Math.round(performance.now() - streamStartedAt),
|
|
2460
|
+
chunkCount,
|
|
2461
|
+
...finishReason === void 0 ? {} : { finishReason },
|
|
2462
|
+
...tokenUsage === void 0 ? {} : { usage: tokenUsage },
|
|
2463
|
+
...reqId === void 0 ? {} : { requestId: reqId }
|
|
2464
|
+
});
|
|
2157
2465
|
} catch (error) {
|
|
2158
2466
|
if (options.signal?.aborted) throw aborted$1("Request was aborted.");
|
|
2467
|
+
const elapsedMs = Math.round(performance.now() - startedAt);
|
|
2159
2468
|
if (headerTimedOut) {
|
|
2160
2469
|
const failure = new QoderLlmError("Qoder model request exceeded its response header timeout.", "TIMEOUT");
|
|
2161
|
-
dependencies.logger?.error?.("[Qoder Stream] Request failed", redactLogValue(failure)
|
|
2470
|
+
dependencies.logger?.error?.("[Qoder Stream] Request failed", redactLogValue(failure), {
|
|
2471
|
+
phase: "header",
|
|
2472
|
+
elapsedMs,
|
|
2473
|
+
timeoutMs: dependencies.responseHeaderTimeoutMs
|
|
2474
|
+
});
|
|
2162
2475
|
throw failure;
|
|
2163
2476
|
}
|
|
2164
2477
|
if (idleTimedOut) {
|
|
2165
2478
|
const failure = new QoderLlmError("Qoder model stream exceeded its idle timeout.", "TIMEOUT");
|
|
2166
|
-
dependencies.logger?.error?.("[Qoder Stream] Request failed", redactLogValue(failure)
|
|
2479
|
+
dependencies.logger?.error?.("[Qoder Stream] Request failed", redactLogValue(failure), {
|
|
2480
|
+
phase: "stream-idle",
|
|
2481
|
+
chunkCount,
|
|
2482
|
+
idleDurationMs: Math.round(performance.now() - lastActivityAt),
|
|
2483
|
+
elapsedMs,
|
|
2484
|
+
...reqId === void 0 ? {} : { requestId: reqId }
|
|
2485
|
+
});
|
|
2167
2486
|
throw failure;
|
|
2168
2487
|
}
|
|
2169
2488
|
if (error instanceof QoderLlmError) {
|
|
2170
|
-
dependencies.logger?.error?.("[Qoder Stream] Request failed", redactLogValue(error)
|
|
2489
|
+
dependencies.logger?.error?.("[Qoder Stream] Request failed", redactLogValue(error), {
|
|
2490
|
+
chunkCount,
|
|
2491
|
+
elapsedMs,
|
|
2492
|
+
...reqId === void 0 ? {} : { requestId: reqId }
|
|
2493
|
+
});
|
|
2171
2494
|
throw error;
|
|
2172
2495
|
}
|
|
2173
2496
|
const failure = new QoderLlmError("Qoder transport request failed.", "TRANSPORT", { cause: error });
|
|
2174
|
-
dependencies.logger?.error?.("[Qoder Stream] Request failed", redactLogValue(failure), {
|
|
2497
|
+
dependencies.logger?.error?.("[Qoder Stream] Request failed", redactLogValue(failure), {
|
|
2498
|
+
chunkCount,
|
|
2499
|
+
elapsedMs,
|
|
2500
|
+
...reqId === void 0 ? {} : { requestId: reqId },
|
|
2501
|
+
cause: redactLogValue(error)
|
|
2502
|
+
});
|
|
2175
2503
|
throw failure;
|
|
2176
2504
|
} finally {
|
|
2177
2505
|
clearTimeout(headerTimer);
|
|
@@ -2199,15 +2527,18 @@ var DefaultQoderTransport = class {
|
|
|
2199
2527
|
modelFlights = new SingleFlight();
|
|
2200
2528
|
attachments;
|
|
2201
2529
|
imageUploader;
|
|
2530
|
+
searchClient;
|
|
2531
|
+
preserveThinking;
|
|
2202
2532
|
constructor(options) {
|
|
2203
2533
|
this.region = options.region;
|
|
2204
2534
|
this.resolvePat = options.resolvePat;
|
|
2205
2535
|
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
2206
2536
|
this.logger = options.logger;
|
|
2207
2537
|
this.streamIdleTimeoutMs = options.streamIdleTimeoutMs ?? 3e5;
|
|
2208
|
-
this.responseHeaderTimeoutMs = options.responseHeaderTimeoutMs ??
|
|
2538
|
+
this.responseHeaderTimeoutMs = options.responseHeaderTimeoutMs ?? 6e4;
|
|
2209
2539
|
this.metadataTimeoutMs = options.metadataTimeoutMs;
|
|
2210
2540
|
this.attachments = options.attachments;
|
|
2541
|
+
this.preserveThinking = options.preserveThinking;
|
|
2211
2542
|
this.auth = new QoderAuthService({
|
|
2212
2543
|
fetch: this.fetchImpl,
|
|
2213
2544
|
logger: this.logger,
|
|
@@ -2233,6 +2564,20 @@ var DefaultQoderTransport = class {
|
|
|
2233
2564
|
return this.auth.getCredentials(pat, signal);
|
|
2234
2565
|
}
|
|
2235
2566
|
});
|
|
2567
|
+
this.searchClient = new QoderSearchClient({
|
|
2568
|
+
fetch: this.fetchImpl,
|
|
2569
|
+
logger: this.logger,
|
|
2570
|
+
region: this.region,
|
|
2571
|
+
resolveCredentials: async (signal) => {
|
|
2572
|
+
const pat = await this.requirePat(signal);
|
|
2573
|
+
return this.auth.getCredentials(pat, signal);
|
|
2574
|
+
},
|
|
2575
|
+
refreshCredentials: async (signal) => {
|
|
2576
|
+
const pat = await this.requirePat(signal);
|
|
2577
|
+
this.auth.clear(pat);
|
|
2578
|
+
return this.auth.getCredentials(pat, signal);
|
|
2579
|
+
}
|
|
2580
|
+
});
|
|
2236
2581
|
}
|
|
2237
2582
|
stream(options, model) {
|
|
2238
2583
|
return this.generate(options, model);
|
|
@@ -2258,6 +2603,9 @@ var DefaultQoderTransport = class {
|
|
|
2258
2603
|
signal: options?.signal
|
|
2259
2604
|
});
|
|
2260
2605
|
}
|
|
2606
|
+
async searchWeb(request, signal) {
|
|
2607
|
+
return this.searchClient.search(request, signal);
|
|
2608
|
+
}
|
|
2261
2609
|
async requirePat(signal) {
|
|
2262
2610
|
if (signal?.aborted) throw aborted("Qoder request was aborted.");
|
|
2263
2611
|
const pat = (await this.resolvePat()).trim();
|
|
@@ -2272,7 +2620,8 @@ var DefaultQoderTransport = class {
|
|
|
2272
2620
|
const credentials = await this.auth.getCredentials(pat, options.signal);
|
|
2273
2621
|
const messages = await translateQoderMessages(options, this.attachments, {
|
|
2274
2622
|
uploader: this.imageUploader,
|
|
2275
|
-
credentials
|
|
2623
|
+
credentials,
|
|
2624
|
+
preserveThinking: this.preserveThinking
|
|
2276
2625
|
});
|
|
2277
2626
|
yield* streamQoderChat(options, model, await this.auth.getCredentials(pat, options.signal), messages, {
|
|
2278
2627
|
fetch: this.fetchImpl,
|
|
@@ -2319,7 +2668,14 @@ const Config = z.object({
|
|
|
2319
2668
|
region: z.union(["global", "china"]).default("global"),
|
|
2320
2669
|
modelsByRegion: modelsByRegionSchema.default({}),
|
|
2321
2670
|
models: z.array(catalogModel),
|
|
2322
|
-
streamIdleTimeoutMs: z.number().step(1).min(1).default(defaultStreamIdleTimeoutMs)
|
|
2671
|
+
streamIdleTimeoutMs: z.number().step(1).min(1).default(defaultStreamIdleTimeoutMs),
|
|
2672
|
+
responseHeaderTimeoutMs: z.number().step(1).min(1).default(defaultResponseHeaderTimeoutMs),
|
|
2673
|
+
preserveThinking: z.boolean().default(true),
|
|
2674
|
+
webSearchMode: z.union([
|
|
2675
|
+
"auto",
|
|
2676
|
+
"always",
|
|
2677
|
+
"disabled"
|
|
2678
|
+
]).default("auto")
|
|
2323
2679
|
});
|
|
2324
2680
|
function resolveModels(models) {
|
|
2325
2681
|
const resolved = models ?? defaultModels;
|
|
@@ -2339,6 +2695,10 @@ function modelsFor(config, region, legacyModelsRegion = config.region ?? "global
|
|
|
2339
2695
|
return resolveModels(defaultModels);
|
|
2340
2696
|
}
|
|
2341
2697
|
//#endregion
|
|
2698
|
+
//#region src/dsh/provider.ts
|
|
2699
|
+
/** DSH model provider route, matching the plugin package name. */
|
|
2700
|
+
const QODER_PROVIDER_ID = "dsh-provider-qoder";
|
|
2701
|
+
//#endregion
|
|
2342
2702
|
//#region src/dsh/adapter.ts
|
|
2343
2703
|
/** Thin DSH adapter over the Qoder transport seam. */
|
|
2344
2704
|
function modelInfo(provider, model) {
|
|
@@ -2355,11 +2715,12 @@ var QoderAdapter = class extends LlmAdapter {
|
|
|
2355
2715
|
catalogModels;
|
|
2356
2716
|
providerId;
|
|
2357
2717
|
providerName;
|
|
2718
|
+
discoveries = /* @__PURE__ */ new WeakMap();
|
|
2358
2719
|
constructor(options) {
|
|
2359
2720
|
super();
|
|
2360
2721
|
this.resolveTransport = options.resolveTransport;
|
|
2361
2722
|
this.catalogModels = options.models && options.models.length > 0 ? options.models : defaultModels;
|
|
2362
|
-
this.providerId = options.providerId ?? "qoder
|
|
2723
|
+
this.providerId = options.providerId ?? "dsh-provider-qoder";
|
|
2363
2724
|
this.providerName = options.providerName ?? "Qoder";
|
|
2364
2725
|
}
|
|
2365
2726
|
providerInfo(provider) {
|
|
@@ -2368,15 +2729,41 @@ var QoderAdapter = class extends LlmAdapter {
|
|
|
2368
2729
|
name: this.providerName
|
|
2369
2730
|
};
|
|
2370
2731
|
}
|
|
2371
|
-
|
|
2372
|
-
|
|
2732
|
+
/** Refresh advertised metadata on catalog reads, sharing a five-minute transport-local cache. */
|
|
2733
|
+
async listModels(provider) {
|
|
2734
|
+
const transport = this.resolveTransport();
|
|
2735
|
+
let cached = this.discoveries.get(transport);
|
|
2736
|
+
if (cached === void 0) {
|
|
2737
|
+
cached = { expiresAt: 0 };
|
|
2738
|
+
this.discoveries.set(transport, cached);
|
|
2739
|
+
}
|
|
2740
|
+
const entry = cached;
|
|
2741
|
+
if (entry.inflight === void 0 && Date.now() >= entry.expiresAt) entry.inflight = Promise.resolve().then(() => transport.discoverModels()).then((models) => {
|
|
2742
|
+
entry.models = models;
|
|
2743
|
+
entry.expiresAt = Date.now() + 300 * 1e3;
|
|
2744
|
+
}).catch(() => {}).finally(() => {
|
|
2745
|
+
entry.inflight = void 0;
|
|
2746
|
+
});
|
|
2747
|
+
await entry.inflight;
|
|
2748
|
+
if (this.resolveTransport() !== transport) return this.listModels(provider);
|
|
2749
|
+
return this.effectiveModels().map((model) => modelInfo(provider, model));
|
|
2750
|
+
}
|
|
2751
|
+
effectiveModels() {
|
|
2752
|
+
return mergeQoderDiscoveryMetadata(this.catalogModels, this.discoveries.get(this.resolveTransport())?.models ?? []);
|
|
2373
2753
|
}
|
|
2374
2754
|
replaceModels(models) {
|
|
2375
2755
|
this.catalogModels = models;
|
|
2376
2756
|
}
|
|
2757
|
+
/** Publish an explicit discovery without letting older in-flight reads overwrite it. */
|
|
2758
|
+
updateDiscoveredModels(transport, models) {
|
|
2759
|
+
this.discoveries.set(transport, {
|
|
2760
|
+
models,
|
|
2761
|
+
expiresAt: Date.now() + 300 * 1e3
|
|
2762
|
+
});
|
|
2763
|
+
}
|
|
2377
2764
|
resolveModel(provider, modelId, signal) {
|
|
2378
2765
|
if (signal?.aborted) return Promise.reject(new QoderLlmError("Qoder model resolution was aborted.", "ABORTED"));
|
|
2379
|
-
const configured = this.
|
|
2766
|
+
const configured = this.effectiveModels().find((model) => model.id === modelId);
|
|
2380
2767
|
if (configured === void 0) return Promise.resolve({
|
|
2381
2768
|
provider,
|
|
2382
2769
|
id: modelId,
|
|
@@ -2399,11 +2786,64 @@ var QoderAdapter = class extends LlmAdapter {
|
|
|
2399
2786
|
}
|
|
2400
2787
|
stream(options) {
|
|
2401
2788
|
if (options.provider !== this.providerId) throw new QoderLlmError(`Qoder adapter does not own provider "${options.provider}".`, "INVALID_PROVIDER");
|
|
2402
|
-
const model = this.
|
|
2789
|
+
const model = this.effectiveModels().find((candidate) => candidate.id === (options.model || "cmodel"));
|
|
2403
2790
|
return this.resolveTransport().stream(options, model);
|
|
2404
2791
|
}
|
|
2405
2792
|
};
|
|
2406
2793
|
//#endregion
|
|
2794
|
+
//#region src/dsh/search-provider.ts
|
|
2795
|
+
/**
|
|
2796
|
+
* DSH WebSearchProvider implementation with initiator-aware dynamic routing.
|
|
2797
|
+
*
|
|
2798
|
+
* Implements the `@deepseek-ai/dsh-web` capability seam, routing search queries
|
|
2799
|
+
* to Qoder Center when an agent runs with a Qoder model (`dsh-provider-qoder`),
|
|
2800
|
+
* or delegating to a fallback search provider when another model is active.
|
|
2801
|
+
*
|
|
2802
|
+
* @module dsh-provider-qoder/dsh/search-provider
|
|
2803
|
+
*/
|
|
2804
|
+
const QODER_SEARCH_PROVIDER_ID = "qoder";
|
|
2805
|
+
const QODER_MODEL_PROVIDER_ROUTE = QODER_PROVIDER_ID;
|
|
2806
|
+
var QoderSearchProvider = class {
|
|
2807
|
+
id = QODER_SEARCH_PROVIDER_ID;
|
|
2808
|
+
ctx;
|
|
2809
|
+
resolveTransport;
|
|
2810
|
+
searchClient;
|
|
2811
|
+
getWebSearchMode;
|
|
2812
|
+
fallbackProvider;
|
|
2813
|
+
constructor(options) {
|
|
2814
|
+
this.ctx = options.ctx;
|
|
2815
|
+
this.resolveTransport = options.resolveTransport;
|
|
2816
|
+
this.searchClient = options.searchClient;
|
|
2817
|
+
this.getWebSearchMode = options.getWebSearchMode;
|
|
2818
|
+
this.fallbackProvider = options.fallbackProvider;
|
|
2819
|
+
}
|
|
2820
|
+
available() {
|
|
2821
|
+
return this.getWebSearchMode() !== "disabled";
|
|
2822
|
+
}
|
|
2823
|
+
async search(request, signal) {
|
|
2824
|
+
const mode = this.getWebSearchMode();
|
|
2825
|
+
if (mode === "disabled") throw new WebError("Qoder web search is disabled in settings.", "WEB_PROVIDER_UNAVAILABLE");
|
|
2826
|
+
const agent = (this.ctx.get("agents") ?? this.ctx.agents)?.currentInitiator?.();
|
|
2827
|
+
const defaultModelService = this.ctx.get("agentDefaultModel");
|
|
2828
|
+
const providerRoute = agent?.options?.provider ?? defaultModelService?.get?.()?.provider;
|
|
2829
|
+
if (mode === "always" || providerRoute === QODER_MODEL_PROVIDER_ROUTE) {
|
|
2830
|
+
if (this.resolveTransport !== void 0) return this.resolveTransport().searchWeb(request, signal);
|
|
2831
|
+
if (this.searchClient !== void 0) return this.searchClient.search(request, signal);
|
|
2832
|
+
throw new WebError("No Qoder transport configured for web search.", "WEB_PROVIDER_ERROR");
|
|
2833
|
+
}
|
|
2834
|
+
const fallback = this.resolveFallback();
|
|
2835
|
+
if (fallback && fallback.available()) return fallback.search(request, signal);
|
|
2836
|
+
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");
|
|
2837
|
+
}
|
|
2838
|
+
resolveFallback() {
|
|
2839
|
+
if (this.fallbackProvider && this.fallbackProvider.available()) return this.fallbackProvider;
|
|
2840
|
+
const providers = this.ctx.get("web")?.searchProviders;
|
|
2841
|
+
if (providers && typeof providers.entries === "function") {
|
|
2842
|
+
for (const [id, provider] of providers.entries()) if (id !== "qoder" && provider.available()) return provider;
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
};
|
|
2846
|
+
//#endregion
|
|
2407
2847
|
//#region src/dsh/credential-contract.ts
|
|
2408
2848
|
/** Shared DSH host/browser contract for the one managed Qoder credential. */
|
|
2409
2849
|
const qoderCredentialRef = "QODER_MANAGED_CREDENTIAL";
|
|
@@ -2421,6 +2861,83 @@ async function resolveManagedQoderPat(credentials) {
|
|
|
2421
2861
|
if (hit === void 0 || isEnvironmentCredentialSource(hit.source)) return "";
|
|
2422
2862
|
return hit.value.trim();
|
|
2423
2863
|
}
|
|
2864
|
+
/**
|
|
2865
|
+
* Absolute Fetch-route prefix. Connection mounts its shared, authenticated API
|
|
2866
|
+
* channel at `/api`, and every exact Fetch route lives directly below it.
|
|
2867
|
+
*/
|
|
2868
|
+
const qoderRpcApiPath = `/api/qoder-subscription`;
|
|
2869
|
+
/** Endpoints the account card and the model catalog call. */
|
|
2870
|
+
const qoderRpcEndpoints = ["account", "models"];
|
|
2871
|
+
/** Absolute Fetch-route path of one endpoint. */
|
|
2872
|
+
function qoderRpcPath(endpoint) {
|
|
2873
|
+
return `${qoderRpcApiPath}/${endpoint}`;
|
|
2874
|
+
}
|
|
2875
|
+
/** Type guard for an untrusted endpoint name. */
|
|
2876
|
+
function isQoderRpcEndpoint(value) {
|
|
2877
|
+
return typeof value === "string" && qoderRpcEndpoints.includes(value);
|
|
2878
|
+
}
|
|
2879
|
+
//#endregion
|
|
2880
|
+
//#region src/dsh/rpc.ts
|
|
2881
|
+
/**
|
|
2882
|
+
* Mount every Qoder settings endpoint on the shared API channel.
|
|
2883
|
+
*
|
|
2884
|
+
* Registering is caller-owned: the returned disposer removes the routes again,
|
|
2885
|
+
* so the caller can tie them to its fiber with `ctx.effect`.
|
|
2886
|
+
*
|
|
2887
|
+
* @param ctx - host context carrying the Connection service.
|
|
2888
|
+
* @param handler - decoded endpoint handler, shared by every endpoint.
|
|
2889
|
+
* @returns disposer removing every registered route.
|
|
2890
|
+
* @throws when Connection exposes no exact Fetch registry.
|
|
2891
|
+
*/
|
|
2892
|
+
function registerQoderRpc(ctx, handler) {
|
|
2893
|
+
const registry = ctx.connection?.fetch;
|
|
2894
|
+
if (typeof registry?.register !== "function") throw new Error("provider-qoder: connection exposes no exact Fetch route registry");
|
|
2895
|
+
const disposers = qoderRpcEndpoints.map((endpoint) => registry.register({
|
|
2896
|
+
path: qoderRpcPath(endpoint),
|
|
2897
|
+
methods: ["POST"],
|
|
2898
|
+
requestBody: "buffered",
|
|
2899
|
+
fetch: (request) => handleQoderRpcRequest(endpoint, handler, request)
|
|
2900
|
+
}));
|
|
2901
|
+
return () => {
|
|
2902
|
+
for (const dispose of disposers) if (typeof dispose === "function") dispose();
|
|
2903
|
+
};
|
|
2904
|
+
}
|
|
2905
|
+
/**
|
|
2906
|
+
* Decode one request, run the endpoint handler, and encode its result.
|
|
2907
|
+
*
|
|
2908
|
+
* Failures stay inside the response envelope so the client keeps a single error
|
|
2909
|
+
* path; only a malformed body and a handler crash use a transport status.
|
|
2910
|
+
*/
|
|
2911
|
+
async function handleQoderRpcRequest(endpoint, handler, request) {
|
|
2912
|
+
if (request.signal.aborted) return Response.json({
|
|
2913
|
+
ok: false,
|
|
2914
|
+
error: {
|
|
2915
|
+
code: "ABORTED",
|
|
2916
|
+
message: "Request aborted",
|
|
2917
|
+
details: { issues: [] }
|
|
2918
|
+
}
|
|
2919
|
+
});
|
|
2920
|
+
let payload = {};
|
|
2921
|
+
const body = await request.text();
|
|
2922
|
+
if (body.trim().length > 0) try {
|
|
2923
|
+
payload = JSON.parse(body);
|
|
2924
|
+
} catch {
|
|
2925
|
+
return new Response("body is not JSON", { status: 400 });
|
|
2926
|
+
}
|
|
2927
|
+
try {
|
|
2928
|
+
return Response.json(await handler(endpoint, payload, request.signal));
|
|
2929
|
+
} catch (error) {
|
|
2930
|
+
if (request.signal.aborted) return Response.json({
|
|
2931
|
+
ok: false,
|
|
2932
|
+
error: {
|
|
2933
|
+
code: "ABORTED",
|
|
2934
|
+
message: "Request aborted",
|
|
2935
|
+
details: { issues: [] }
|
|
2936
|
+
}
|
|
2937
|
+
});
|
|
2938
|
+
return new Response(`handler failure: ${String(error)}`, { status: 500 });
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2424
2941
|
//#endregion
|
|
2425
2942
|
//#region src/dsh/plugin.ts
|
|
2426
2943
|
const name = "provider-qoder";
|
|
@@ -2430,9 +2947,8 @@ const inject = [
|
|
|
2430
2947
|
"connection",
|
|
2431
2948
|
"attachments"
|
|
2432
2949
|
];
|
|
2433
|
-
const providerQoder =
|
|
2950
|
+
const providerQoder = QODER_PROVIDER_ID;
|
|
2434
2951
|
const settingsNamespace = "provider-qoder";
|
|
2435
|
-
const qoderChannel = "/qoder-subscription";
|
|
2436
2952
|
const fiberDisposed = 4;
|
|
2437
2953
|
const fiberUnloading = 5;
|
|
2438
2954
|
function logError(error) {
|
|
@@ -2443,13 +2959,13 @@ function logError(error) {
|
|
|
2443
2959
|
..."code" in error ? { code: error.code } : {}
|
|
2444
2960
|
};
|
|
2445
2961
|
}
|
|
2446
|
-
function publicError(message) {
|
|
2962
|
+
function publicError(code, message, details = { issues: [] }) {
|
|
2447
2963
|
return {
|
|
2448
2964
|
ok: false,
|
|
2449
2965
|
error: {
|
|
2450
|
-
code
|
|
2966
|
+
code,
|
|
2451
2967
|
message,
|
|
2452
|
-
details
|
|
2968
|
+
details
|
|
2453
2969
|
}
|
|
2454
2970
|
};
|
|
2455
2971
|
}
|
|
@@ -2461,7 +2977,10 @@ function apply(ctx, config = {}) {
|
|
|
2461
2977
|
region: initialRegion,
|
|
2462
2978
|
modelsByRegion: { ...config.modelsByRegion },
|
|
2463
2979
|
...hasConfiguredModels ? { models: resolveModels(config.models) } : {},
|
|
2464
|
-
streamIdleTimeoutMs: config.streamIdleTimeoutMs ?? 3e5
|
|
2980
|
+
streamIdleTimeoutMs: config.streamIdleTimeoutMs ?? 3e5,
|
|
2981
|
+
responseHeaderTimeoutMs: config.responseHeaderTimeoutMs ?? 6e4,
|
|
2982
|
+
preserveThinking: config.preserveThinking ?? true,
|
|
2983
|
+
webSearchMode: config.webSearchMode ?? "auto"
|
|
2465
2984
|
};
|
|
2466
2985
|
let current = () => baseConfig;
|
|
2467
2986
|
let legacyModelsRegion = initialRegion;
|
|
@@ -2470,12 +2989,14 @@ function apply(ctx, config = {}) {
|
|
|
2470
2989
|
global: [],
|
|
2471
2990
|
china: []
|
|
2472
2991
|
};
|
|
2473
|
-
const createTransport = (region, streamIdleTimeoutMs, resolvePat = () => resolveManagedQoderPat(ctx.credentials)) => createQoderTransport({
|
|
2992
|
+
const createTransport = (region, streamIdleTimeoutMs, responseHeaderTimeoutMs, preserveThinking, resolvePat = () => resolveManagedQoderPat(ctx.credentials)) => createQoderTransport({
|
|
2474
2993
|
region,
|
|
2475
2994
|
resolvePat,
|
|
2476
2995
|
logger,
|
|
2477
2996
|
streamIdleTimeoutMs,
|
|
2478
|
-
|
|
2997
|
+
responseHeaderTimeoutMs,
|
|
2998
|
+
attachments: ctx.attachments,
|
|
2999
|
+
preserveThinking
|
|
2479
3000
|
});
|
|
2480
3001
|
const resolveConfig = () => {
|
|
2481
3002
|
const value = current();
|
|
@@ -2483,14 +3004,19 @@ function apply(ctx, config = {}) {
|
|
|
2483
3004
|
return {
|
|
2484
3005
|
region,
|
|
2485
3006
|
models: mergeQoderDiscoveryMetadata(modelsFor(value, region, legacyModelsRegion), discoveredCatalogs[region]),
|
|
2486
|
-
streamIdleTimeoutMs: value.streamIdleTimeoutMs ?? 3e5
|
|
3007
|
+
streamIdleTimeoutMs: value.streamIdleTimeoutMs ?? 3e5,
|
|
3008
|
+
responseHeaderTimeoutMs: value.responseHeaderTimeoutMs ?? 6e4,
|
|
3009
|
+
preserveThinking: value.preserveThinking ?? true,
|
|
3010
|
+
webSearchMode: value.webSearchMode ?? "auto"
|
|
2487
3011
|
};
|
|
2488
3012
|
};
|
|
2489
3013
|
const initial = resolveConfig();
|
|
2490
|
-
let activeTransport = createTransport(initial.region, initial.streamIdleTimeoutMs);
|
|
3014
|
+
let activeTransport = createTransport(initial.region, initial.streamIdleTimeoutMs, initial.responseHeaderTimeoutMs, initial.preserveThinking);
|
|
2491
3015
|
let activeTransportConfig = {
|
|
2492
3016
|
region: initial.region,
|
|
2493
|
-
streamIdleTimeoutMs: initial.streamIdleTimeoutMs
|
|
3017
|
+
streamIdleTimeoutMs: initial.streamIdleTimeoutMs,
|
|
3018
|
+
responseHeaderTimeoutMs: initial.responseHeaderTimeoutMs,
|
|
3019
|
+
preserveThinking: initial.preserveThinking
|
|
2494
3020
|
};
|
|
2495
3021
|
const adapter = new QoderAdapter({
|
|
2496
3022
|
resolveTransport: () => activeTransport,
|
|
@@ -2501,11 +3027,13 @@ function apply(ctx, config = {}) {
|
|
|
2501
3027
|
const registration = ctx.llm.registerAdapter([providerQoder], adapter);
|
|
2502
3028
|
const refreshAdapter = () => {
|
|
2503
3029
|
const next = resolveConfig();
|
|
2504
|
-
if (next.region !== activeTransportConfig.region || next.streamIdleTimeoutMs !== activeTransportConfig.streamIdleTimeoutMs) {
|
|
2505
|
-
activeTransport = createTransport(next.region, next.streamIdleTimeoutMs);
|
|
3030
|
+
if (next.region !== activeTransportConfig.region || next.streamIdleTimeoutMs !== activeTransportConfig.streamIdleTimeoutMs || next.responseHeaderTimeoutMs !== activeTransportConfig.responseHeaderTimeoutMs || next.preserveThinking !== activeTransportConfig.preserveThinking) {
|
|
3031
|
+
activeTransport = createTransport(next.region, next.streamIdleTimeoutMs, next.responseHeaderTimeoutMs, next.preserveThinking);
|
|
2506
3032
|
activeTransportConfig = {
|
|
2507
3033
|
region: next.region,
|
|
2508
|
-
streamIdleTimeoutMs: next.streamIdleTimeoutMs
|
|
3034
|
+
streamIdleTimeoutMs: next.streamIdleTimeoutMs,
|
|
3035
|
+
responseHeaderTimeoutMs: next.responseHeaderTimeoutMs,
|
|
3036
|
+
preserveThinking: next.preserveThinking
|
|
2509
3037
|
};
|
|
2510
3038
|
}
|
|
2511
3039
|
adapter.replaceModels(next.models);
|
|
@@ -2558,8 +3086,10 @@ function apply(ctx, config = {}) {
|
|
|
2558
3086
|
});
|
|
2559
3087
|
const discoverModels = async (signal, suppliedPat) => {
|
|
2560
3088
|
const snapshot = resolveConfig();
|
|
3089
|
+
const snapshotTransport = activeTransport;
|
|
2561
3090
|
const normalizedPat = suppliedPat?.trim();
|
|
2562
|
-
const models = await (normalizedPat ? createTransport(snapshot.region, snapshot.streamIdleTimeoutMs, () => Promise.resolve(normalizedPat)) :
|
|
3091
|
+
const models = await (normalizedPat ? createTransport(snapshot.region, snapshot.streamIdleTimeoutMs, snapshot.responseHeaderTimeoutMs, snapshot.preserveThinking, () => Promise.resolve(normalizedPat)) : snapshotTransport).discoverModels(signal);
|
|
3092
|
+
adapter.updateDiscoveredModels(snapshotTransport, models);
|
|
2563
3093
|
discoveredCatalogs[snapshot.region] = models;
|
|
2564
3094
|
refreshAdapter();
|
|
2565
3095
|
await persistDiscoveredModels(snapshot.region);
|
|
@@ -2574,40 +3104,69 @@ function apply(ctx, config = {}) {
|
|
|
2574
3104
|
maxTokens: model.maxTokens
|
|
2575
3105
|
}));
|
|
2576
3106
|
});
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
if (endpoint === "models") try {
|
|
2582
|
-
return {
|
|
2583
|
-
ok: true,
|
|
2584
|
-
value: await discoverModels(signal)
|
|
2585
|
-
};
|
|
2586
|
-
} catch (error) {
|
|
2587
|
-
if (signal.aborted || error instanceof QoderLlmError && error.code === "ABORTED") throw error;
|
|
2588
|
-
logger?.error?.("[Qoder RPC] Failed to discover models", logError(error));
|
|
2589
|
-
return publicError(error instanceof Error ? error.message : "Failed to discover Qoder models");
|
|
2590
|
-
}
|
|
2591
|
-
const force = typeof payload === "object" && payload !== null && "force" in payload ? payload.force === true : false;
|
|
2592
|
-
logger?.debug?.("[Qoder RPC] Reading subscriber account", { force });
|
|
3107
|
+
const handler = async (endpoint, payload, signal) => {
|
|
3108
|
+
if (!isQoderRpcEndpoint(endpoint)) return publicError("UNKNOWN_ENDPOINT", `Unknown endpoint: ${endpoint}`);
|
|
3109
|
+
if (signal.aborted) return publicError("ABORTED", "Request aborted");
|
|
3110
|
+
const executeRpc = async (operation, task) => {
|
|
2593
3111
|
try {
|
|
2594
|
-
const account = await activeTransport.readAccount({
|
|
2595
|
-
force,
|
|
2596
|
-
signal
|
|
2597
|
-
});
|
|
2598
|
-
logger?.debug?.("[Qoder RPC] Subscriber account resolved");
|
|
2599
3112
|
return {
|
|
2600
3113
|
ok: true,
|
|
2601
|
-
value:
|
|
3114
|
+
value: await task()
|
|
2602
3115
|
};
|
|
2603
3116
|
} catch (error) {
|
|
2604
|
-
if (signal.aborted || error instanceof QoderLlmError && error.code === "ABORTED")
|
|
2605
|
-
|
|
2606
|
-
|
|
3117
|
+
if (signal.aborted || error instanceof QoderLlmError && error.code === "ABORTED") {
|
|
3118
|
+
logger?.debug?.(`[Qoder RPC] ${operation} was aborted`);
|
|
3119
|
+
return publicError("ABORTED", "Request aborted");
|
|
3120
|
+
}
|
|
3121
|
+
let code = "INTERNAL";
|
|
3122
|
+
if (error instanceof QoderLlmError) if (error.code === "MISSING_CREDENTIAL" || error.code === "NO_CREDENTIALS") code = "NO_CREDENTIALS";
|
|
3123
|
+
else if (error.code === "AUTH") code = "UNAUTHENTICATED";
|
|
3124
|
+
else if (error.code === "TIMEOUT") code = "TIMEOUT";
|
|
3125
|
+
else code = "UPSTREAM_ERROR";
|
|
3126
|
+
logger?.error?.(`[Qoder RPC] Failed to ${operation}`, logError(error));
|
|
3127
|
+
return publicError(code, error instanceof Error ? error.message : `Failed to ${operation}`);
|
|
2607
3128
|
}
|
|
2608
3129
|
};
|
|
2609
|
-
|
|
3130
|
+
if (endpoint === "models") return await executeRpc("discover Qoder models", () => discoverModels(signal));
|
|
3131
|
+
const force = typeof payload === "object" && payload !== null && "force" in payload ? payload.force === true : false;
|
|
3132
|
+
logger?.debug?.("[Qoder RPC] Reading subscriber account", { force });
|
|
3133
|
+
const outcome = await executeRpc("load Qoder account", () => activeTransport.readAccount({
|
|
3134
|
+
force,
|
|
3135
|
+
signal
|
|
3136
|
+
}));
|
|
3137
|
+
if (outcome.ok) logger?.debug?.("[Qoder RPC] Subscriber account resolved");
|
|
3138
|
+
return outcome;
|
|
3139
|
+
};
|
|
3140
|
+
try {
|
|
3141
|
+
ctx.effect(() => registerQoderRpc(ctx, handler), "provider-qoder: settings RPC routes");
|
|
3142
|
+
} catch (error) {
|
|
3143
|
+
logger?.error?.("[Qoder RPC] Failed to register the settings RPC routes", logError(error));
|
|
2610
3144
|
}
|
|
3145
|
+
ctx.inject(["web"], (webCtx) => {
|
|
3146
|
+
const searchProvider = new QoderSearchProvider({
|
|
3147
|
+
ctx,
|
|
3148
|
+
resolveTransport: () => activeTransport,
|
|
3149
|
+
getWebSearchMode: () => resolveConfig().webSearchMode ?? "auto"
|
|
3150
|
+
});
|
|
3151
|
+
webCtx.web.registerSearchProvider(searchProvider);
|
|
3152
|
+
if (typeof webCtx.web.search === "function") {
|
|
3153
|
+
const originalSearch = webCtx.web.search.bind(webCtx.web);
|
|
3154
|
+
webCtx.effect(() => {
|
|
3155
|
+
webCtx.web.search = async (request, signal) => {
|
|
3156
|
+
const mode = resolveConfig().webSearchMode ?? "auto";
|
|
3157
|
+
if (mode === "disabled") return originalSearch(request, signal);
|
|
3158
|
+
const agent = (ctx.get("agents") ?? ctx.agents)?.currentInitiator?.();
|
|
3159
|
+
const defaultModelService = ctx.get("agentDefaultModel");
|
|
3160
|
+
const isQoderActive = (agent?.options?.provider ?? defaultModelService?.get?.()?.provider) === providerQoder;
|
|
3161
|
+
if (mode === "always" || isQoderActive) return searchProvider.search(request, signal);
|
|
3162
|
+
return originalSearch(request, signal);
|
|
3163
|
+
};
|
|
3164
|
+
return () => {
|
|
3165
|
+
webCtx.web.search = originalSearch;
|
|
3166
|
+
};
|
|
3167
|
+
}, "provider-qoder: transparent web search router");
|
|
3168
|
+
}
|
|
3169
|
+
});
|
|
2611
3170
|
}
|
|
2612
3171
|
//#endregion
|
|
2613
|
-
export { Config, apply, inject, name };
|
|
3172
|
+
export { Config, QODER_SEARCH_PROVIDER_ID, QoderSearchClient, QoderSearchProvider, apply, getQoderWebSearchUrl, inject, name, qoderWebSearchPath };
|