dsh-provider-qoder 0.2.1 → 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 +112 -2
- package/lib/index.js +602 -62
- 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;
|
|
@@ -1426,13 +1436,20 @@ function normalizeQuota(raw) {
|
|
|
1426
1436
|
unit
|
|
1427
1437
|
};
|
|
1428
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
|
+
*/
|
|
1429
1446
|
function normalizeExpiresAt(rawExpires) {
|
|
1430
1447
|
if (rawExpires === void 0 || rawExpires === null) return void 0;
|
|
1431
|
-
|
|
1432
|
-
if (
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
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();
|
|
1436
1453
|
}
|
|
1437
1454
|
function asString(value) {
|
|
1438
1455
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
@@ -1447,6 +1464,76 @@ function asNumber(value) {
|
|
|
1447
1464
|
if (Number.isFinite(num)) return num;
|
|
1448
1465
|
}
|
|
1449
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
|
+
}
|
|
1450
1537
|
function normalizeOrganization(raw) {
|
|
1451
1538
|
if (!raw || typeof raw !== "object") return void 0;
|
|
1452
1539
|
const obj = raw;
|
|
@@ -1569,6 +1656,14 @@ var QoderUsageReader = class {
|
|
|
1569
1656
|
if (pat) this.cache.delete(`${this.region}:${opaqueCredentialKey(pat)}`);
|
|
1570
1657
|
else this.cache.clear();
|
|
1571
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
|
+
*/
|
|
1572
1667
|
async fetchUsage(jobToken, signal) {
|
|
1573
1668
|
const data = await openApiJsonRequest(this.fetchImpl, {
|
|
1574
1669
|
url: getQoderUsageUrl(this.region),
|
|
@@ -1579,13 +1674,17 @@ var QoderUsageReader = class {
|
|
|
1579
1674
|
operation: "Usage",
|
|
1580
1675
|
logCategory: "account.usage"
|
|
1581
1676
|
});
|
|
1677
|
+
const dedicatedResourcePackages = normalizeResourcePackages(data.dedicatedResourcePackages);
|
|
1678
|
+
const rawUsage = { ...data };
|
|
1679
|
+
delete rawUsage.dedicatedResourcePackages;
|
|
1582
1680
|
return {
|
|
1583
1681
|
userQuota: normalizeQuota(data.userQuota),
|
|
1584
1682
|
orgResourcePackage: normalizeQuota(data.orgResourcePackage),
|
|
1683
|
+
...dedicatedResourcePackages !== void 0 ? { dedicatedResourcePackages } : {},
|
|
1585
1684
|
totalUsagePercentage: typeof data.totalUsagePercentage === "number" ? data.totalUsagePercentage : void 0,
|
|
1586
1685
|
isQuotaExceeded: typeof data.isQuotaExceeded === "boolean" ? data.isQuotaExceeded : false,
|
|
1587
1686
|
expiresAt: normalizeExpiresAt(data.expiresAt),
|
|
1588
|
-
raw:
|
|
1687
|
+
raw: rawUsage
|
|
1589
1688
|
};
|
|
1590
1689
|
}
|
|
1591
1690
|
async fetchPlan(jobToken, signal) {
|
|
@@ -1659,6 +1758,163 @@ function qoderEncodeBody(plaintext) {
|
|
|
1659
1758
|
}
|
|
1660
1759
|
return out;
|
|
1661
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
|
+
};
|
|
1662
1918
|
//#endregion
|
|
1663
1919
|
//#region src/qoder/transport/wire/thinking.ts
|
|
1664
1920
|
const thinkingTags = [{
|
|
@@ -1866,10 +2122,18 @@ function finishKind(value) {
|
|
|
1866
2122
|
if (value === void 0 || value === null || value === "") return void 0;
|
|
1867
2123
|
if (value === "stop") return "stop";
|
|
1868
2124
|
if (value === "length") return "max-tokens";
|
|
1869
|
-
if (value === "tool_calls" || value === "toolUse") return "tool-calls";
|
|
2125
|
+
if (value === "tool_calls" || value === "toolUse" || value === "function_call") return "tool-calls";
|
|
1870
2126
|
if (value === "content_filter") throw new QoderLlmError("Qoder blocked the response through its content filter.", "PROVIDER_ERROR");
|
|
1871
2127
|
throw malformed(`Qoder returned unknown finish reason "${value}".`);
|
|
1872
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
|
+
}
|
|
1873
2137
|
async function* parseQoderSse(stream, options = {}) {
|
|
1874
2138
|
const reader = stream.getReader();
|
|
1875
2139
|
const decoder = new TextDecoder();
|
|
@@ -1882,6 +2146,7 @@ async function* parseQoderSse(stream, options = {}) {
|
|
|
1882
2146
|
let activeTextual;
|
|
1883
2147
|
let pendingUsage;
|
|
1884
2148
|
let terminalKind = "stop";
|
|
2149
|
+
let sawFinishReason = false;
|
|
1885
2150
|
let sawContent = false;
|
|
1886
2151
|
const maxBufferChars = options.maxBufferChars ?? 2097152;
|
|
1887
2152
|
const closeTextual = () => {
|
|
@@ -1985,7 +2250,11 @@ async function* parseQoderSse(stream, options = {}) {
|
|
|
1985
2250
|
const innerChunk = parseInner(body);
|
|
1986
2251
|
pendingUsage = tokenUsage(innerChunk) ?? pendingUsage;
|
|
1987
2252
|
for (const choice of innerChunk.choices ?? []) {
|
|
1988
|
-
|
|
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;
|
|
1989
2258
|
const delta = choice.delta;
|
|
1990
2259
|
if (!delta) continue;
|
|
1991
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;
|
|
@@ -2055,7 +2324,7 @@ async function* parseQoderSse(stream, options = {}) {
|
|
|
2055
2324
|
await reader.cancel().catch(() => void 0);
|
|
2056
2325
|
reader.releaseLock();
|
|
2057
2326
|
}
|
|
2058
|
-
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");
|
|
2059
2328
|
for (const segment of thinkingParser.finish()) for (const chunk of appendSegment(segment)) yield chunk;
|
|
2060
2329
|
for (const chunk of closeTextual()) yield chunk;
|
|
2061
2330
|
for (const [, state] of [...toolCalls.entries()].sort(([left], [right]) => left - right)) {
|
|
@@ -2123,7 +2392,10 @@ async function* streamQoderChat(options, model, credentials, messages, dependenc
|
|
|
2123
2392
|
let headerTimedOut = false;
|
|
2124
2393
|
let idleTimedOut = false;
|
|
2125
2394
|
let idleTimer;
|
|
2395
|
+
let chunkCount = 0;
|
|
2396
|
+
let reqId;
|
|
2126
2397
|
const startedAt = performance.now();
|
|
2398
|
+
let lastActivityAt = startedAt;
|
|
2127
2399
|
const onCallerAbort = () => requestController.abort(options.signal?.reason);
|
|
2128
2400
|
options.signal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
2129
2401
|
const headerTimer = setTimeout(() => {
|
|
@@ -2131,6 +2403,7 @@ async function* streamQoderChat(options, model, credentials, messages, dependenc
|
|
|
2131
2403
|
requestController.abort("response header timeout");
|
|
2132
2404
|
}, dependencies.responseHeaderTimeoutMs);
|
|
2133
2405
|
const resetIdleTimer = () => {
|
|
2406
|
+
lastActivityAt = performance.now();
|
|
2134
2407
|
if (idleTimer !== void 0) clearTimeout(idleTimer);
|
|
2135
2408
|
idleTimer = setTimeout(() => {
|
|
2136
2409
|
idleTimedOut = true;
|
|
@@ -2154,34 +2427,79 @@ async function* streamQoderChat(options, model, credentials, messages, dependenc
|
|
|
2154
2427
|
signal: requestController.signal
|
|
2155
2428
|
});
|
|
2156
2429
|
clearTimeout(headerTimer);
|
|
2430
|
+
reqId = qoderRequestId(response.headers);
|
|
2157
2431
|
dependencies.logger?.debug?.("[Qoder Stream] Response headers received", {
|
|
2158
2432
|
region: dependencies.region,
|
|
2159
2433
|
status: response.status,
|
|
2160
2434
|
durationMs: Math.round(performance.now() - startedAt),
|
|
2161
|
-
...
|
|
2435
|
+
...reqId === void 0 ? {} : { requestId: reqId }
|
|
2162
2436
|
});
|
|
2163
2437
|
resetIdleTimer();
|
|
2164
2438
|
if (!response.ok) throw qoderHttpError(`Qoder upstream service returned HTTP ${response.status}.`, response);
|
|
2165
2439
|
if (!response.body) throw new QoderLlmError("Qoder response contains no readable body stream.", "EMPTY_RESPONSE");
|
|
2166
|
-
|
|
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
|
+
});
|
|
2167
2465
|
} catch (error) {
|
|
2168
2466
|
if (options.signal?.aborted) throw aborted$1("Request was aborted.");
|
|
2467
|
+
const elapsedMs = Math.round(performance.now() - startedAt);
|
|
2169
2468
|
if (headerTimedOut) {
|
|
2170
2469
|
const failure = new QoderLlmError("Qoder model request exceeded its response header timeout.", "TIMEOUT");
|
|
2171
|
-
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
|
+
});
|
|
2172
2475
|
throw failure;
|
|
2173
2476
|
}
|
|
2174
2477
|
if (idleTimedOut) {
|
|
2175
2478
|
const failure = new QoderLlmError("Qoder model stream exceeded its idle timeout.", "TIMEOUT");
|
|
2176
|
-
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
|
+
});
|
|
2177
2486
|
throw failure;
|
|
2178
2487
|
}
|
|
2179
2488
|
if (error instanceof QoderLlmError) {
|
|
2180
|
-
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
|
+
});
|
|
2181
2494
|
throw error;
|
|
2182
2495
|
}
|
|
2183
2496
|
const failure = new QoderLlmError("Qoder transport request failed.", "TRANSPORT", { cause: error });
|
|
2184
|
-
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
|
+
});
|
|
2185
2503
|
throw failure;
|
|
2186
2504
|
} finally {
|
|
2187
2505
|
clearTimeout(headerTimer);
|
|
@@ -2209,6 +2527,7 @@ var DefaultQoderTransport = class {
|
|
|
2209
2527
|
modelFlights = new SingleFlight();
|
|
2210
2528
|
attachments;
|
|
2211
2529
|
imageUploader;
|
|
2530
|
+
searchClient;
|
|
2212
2531
|
preserveThinking;
|
|
2213
2532
|
constructor(options) {
|
|
2214
2533
|
this.region = options.region;
|
|
@@ -2216,7 +2535,7 @@ var DefaultQoderTransport = class {
|
|
|
2216
2535
|
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
2217
2536
|
this.logger = options.logger;
|
|
2218
2537
|
this.streamIdleTimeoutMs = options.streamIdleTimeoutMs ?? 3e5;
|
|
2219
|
-
this.responseHeaderTimeoutMs = options.responseHeaderTimeoutMs ??
|
|
2538
|
+
this.responseHeaderTimeoutMs = options.responseHeaderTimeoutMs ?? 6e4;
|
|
2220
2539
|
this.metadataTimeoutMs = options.metadataTimeoutMs;
|
|
2221
2540
|
this.attachments = options.attachments;
|
|
2222
2541
|
this.preserveThinking = options.preserveThinking;
|
|
@@ -2245,6 +2564,20 @@ var DefaultQoderTransport = class {
|
|
|
2245
2564
|
return this.auth.getCredentials(pat, signal);
|
|
2246
2565
|
}
|
|
2247
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
|
+
});
|
|
2248
2581
|
}
|
|
2249
2582
|
stream(options, model) {
|
|
2250
2583
|
return this.generate(options, model);
|
|
@@ -2270,6 +2603,9 @@ var DefaultQoderTransport = class {
|
|
|
2270
2603
|
signal: options?.signal
|
|
2271
2604
|
});
|
|
2272
2605
|
}
|
|
2606
|
+
async searchWeb(request, signal) {
|
|
2607
|
+
return this.searchClient.search(request, signal);
|
|
2608
|
+
}
|
|
2273
2609
|
async requirePat(signal) {
|
|
2274
2610
|
if (signal?.aborted) throw aborted("Qoder request was aborted.");
|
|
2275
2611
|
const pat = (await this.resolvePat()).trim();
|
|
@@ -2333,7 +2669,13 @@ const Config = z.object({
|
|
|
2333
2669
|
modelsByRegion: modelsByRegionSchema.default({}),
|
|
2334
2670
|
models: z.array(catalogModel),
|
|
2335
2671
|
streamIdleTimeoutMs: z.number().step(1).min(1).default(defaultStreamIdleTimeoutMs),
|
|
2336
|
-
|
|
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")
|
|
2337
2679
|
});
|
|
2338
2680
|
function resolveModels(models) {
|
|
2339
2681
|
const resolved = models ?? defaultModels;
|
|
@@ -2353,6 +2695,10 @@ function modelsFor(config, region, legacyModelsRegion = config.region ?? "global
|
|
|
2353
2695
|
return resolveModels(defaultModels);
|
|
2354
2696
|
}
|
|
2355
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
|
|
2356
2702
|
//#region src/dsh/adapter.ts
|
|
2357
2703
|
/** Thin DSH adapter over the Qoder transport seam. */
|
|
2358
2704
|
function modelInfo(provider, model) {
|
|
@@ -2369,11 +2715,12 @@ var QoderAdapter = class extends LlmAdapter {
|
|
|
2369
2715
|
catalogModels;
|
|
2370
2716
|
providerId;
|
|
2371
2717
|
providerName;
|
|
2718
|
+
discoveries = /* @__PURE__ */ new WeakMap();
|
|
2372
2719
|
constructor(options) {
|
|
2373
2720
|
super();
|
|
2374
2721
|
this.resolveTransport = options.resolveTransport;
|
|
2375
2722
|
this.catalogModels = options.models && options.models.length > 0 ? options.models : defaultModels;
|
|
2376
|
-
this.providerId = options.providerId ?? "qoder
|
|
2723
|
+
this.providerId = options.providerId ?? "dsh-provider-qoder";
|
|
2377
2724
|
this.providerName = options.providerName ?? "Qoder";
|
|
2378
2725
|
}
|
|
2379
2726
|
providerInfo(provider) {
|
|
@@ -2382,15 +2729,41 @@ var QoderAdapter = class extends LlmAdapter {
|
|
|
2382
2729
|
name: this.providerName
|
|
2383
2730
|
};
|
|
2384
2731
|
}
|
|
2385
|
-
|
|
2386
|
-
|
|
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 ?? []);
|
|
2387
2753
|
}
|
|
2388
2754
|
replaceModels(models) {
|
|
2389
2755
|
this.catalogModels = models;
|
|
2390
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
|
+
}
|
|
2391
2764
|
resolveModel(provider, modelId, signal) {
|
|
2392
2765
|
if (signal?.aborted) return Promise.reject(new QoderLlmError("Qoder model resolution was aborted.", "ABORTED"));
|
|
2393
|
-
const configured = this.
|
|
2766
|
+
const configured = this.effectiveModels().find((model) => model.id === modelId);
|
|
2394
2767
|
if (configured === void 0) return Promise.resolve({
|
|
2395
2768
|
provider,
|
|
2396
2769
|
id: modelId,
|
|
@@ -2413,11 +2786,64 @@ var QoderAdapter = class extends LlmAdapter {
|
|
|
2413
2786
|
}
|
|
2414
2787
|
stream(options) {
|
|
2415
2788
|
if (options.provider !== this.providerId) throw new QoderLlmError(`Qoder adapter does not own provider "${options.provider}".`, "INVALID_PROVIDER");
|
|
2416
|
-
const model = this.
|
|
2789
|
+
const model = this.effectiveModels().find((candidate) => candidate.id === (options.model || "cmodel"));
|
|
2417
2790
|
return this.resolveTransport().stream(options, model);
|
|
2418
2791
|
}
|
|
2419
2792
|
};
|
|
2420
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
|
|
2421
2847
|
//#region src/dsh/credential-contract.ts
|
|
2422
2848
|
/** Shared DSH host/browser contract for the one managed Qoder credential. */
|
|
2423
2849
|
const qoderCredentialRef = "QODER_MANAGED_CREDENTIAL";
|
|
@@ -2435,6 +2861,83 @@ async function resolveManagedQoderPat(credentials) {
|
|
|
2435
2861
|
if (hit === void 0 || isEnvironmentCredentialSource(hit.source)) return "";
|
|
2436
2862
|
return hit.value.trim();
|
|
2437
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
|
+
}
|
|
2438
2941
|
//#endregion
|
|
2439
2942
|
//#region src/dsh/plugin.ts
|
|
2440
2943
|
const name = "provider-qoder";
|
|
@@ -2444,9 +2947,8 @@ const inject = [
|
|
|
2444
2947
|
"connection",
|
|
2445
2948
|
"attachments"
|
|
2446
2949
|
];
|
|
2447
|
-
const providerQoder =
|
|
2950
|
+
const providerQoder = QODER_PROVIDER_ID;
|
|
2448
2951
|
const settingsNamespace = "provider-qoder";
|
|
2449
|
-
const qoderChannel = "/qoder-subscription";
|
|
2450
2952
|
const fiberDisposed = 4;
|
|
2451
2953
|
const fiberUnloading = 5;
|
|
2452
2954
|
function logError(error) {
|
|
@@ -2457,13 +2959,13 @@ function logError(error) {
|
|
|
2457
2959
|
..."code" in error ? { code: error.code } : {}
|
|
2458
2960
|
};
|
|
2459
2961
|
}
|
|
2460
|
-
function publicError(message) {
|
|
2962
|
+
function publicError(code, message, details = { issues: [] }) {
|
|
2461
2963
|
return {
|
|
2462
2964
|
ok: false,
|
|
2463
2965
|
error: {
|
|
2464
|
-
code
|
|
2966
|
+
code,
|
|
2465
2967
|
message,
|
|
2466
|
-
details
|
|
2968
|
+
details
|
|
2467
2969
|
}
|
|
2468
2970
|
};
|
|
2469
2971
|
}
|
|
@@ -2476,7 +2978,9 @@ function apply(ctx, config = {}) {
|
|
|
2476
2978
|
modelsByRegion: { ...config.modelsByRegion },
|
|
2477
2979
|
...hasConfiguredModels ? { models: resolveModels(config.models) } : {},
|
|
2478
2980
|
streamIdleTimeoutMs: config.streamIdleTimeoutMs ?? 3e5,
|
|
2479
|
-
|
|
2981
|
+
responseHeaderTimeoutMs: config.responseHeaderTimeoutMs ?? 6e4,
|
|
2982
|
+
preserveThinking: config.preserveThinking ?? true,
|
|
2983
|
+
webSearchMode: config.webSearchMode ?? "auto"
|
|
2480
2984
|
};
|
|
2481
2985
|
let current = () => baseConfig;
|
|
2482
2986
|
let legacyModelsRegion = initialRegion;
|
|
@@ -2485,11 +2989,12 @@ function apply(ctx, config = {}) {
|
|
|
2485
2989
|
global: [],
|
|
2486
2990
|
china: []
|
|
2487
2991
|
};
|
|
2488
|
-
const createTransport = (region, streamIdleTimeoutMs, preserveThinking, resolvePat = () => resolveManagedQoderPat(ctx.credentials)) => createQoderTransport({
|
|
2992
|
+
const createTransport = (region, streamIdleTimeoutMs, responseHeaderTimeoutMs, preserveThinking, resolvePat = () => resolveManagedQoderPat(ctx.credentials)) => createQoderTransport({
|
|
2489
2993
|
region,
|
|
2490
2994
|
resolvePat,
|
|
2491
2995
|
logger,
|
|
2492
2996
|
streamIdleTimeoutMs,
|
|
2997
|
+
responseHeaderTimeoutMs,
|
|
2493
2998
|
attachments: ctx.attachments,
|
|
2494
2999
|
preserveThinking
|
|
2495
3000
|
});
|
|
@@ -2500,14 +3005,17 @@ function apply(ctx, config = {}) {
|
|
|
2500
3005
|
region,
|
|
2501
3006
|
models: mergeQoderDiscoveryMetadata(modelsFor(value, region, legacyModelsRegion), discoveredCatalogs[region]),
|
|
2502
3007
|
streamIdleTimeoutMs: value.streamIdleTimeoutMs ?? 3e5,
|
|
2503
|
-
|
|
3008
|
+
responseHeaderTimeoutMs: value.responseHeaderTimeoutMs ?? 6e4,
|
|
3009
|
+
preserveThinking: value.preserveThinking ?? true,
|
|
3010
|
+
webSearchMode: value.webSearchMode ?? "auto"
|
|
2504
3011
|
};
|
|
2505
3012
|
};
|
|
2506
3013
|
const initial = resolveConfig();
|
|
2507
|
-
let activeTransport = createTransport(initial.region, initial.streamIdleTimeoutMs, initial.preserveThinking);
|
|
3014
|
+
let activeTransport = createTransport(initial.region, initial.streamIdleTimeoutMs, initial.responseHeaderTimeoutMs, initial.preserveThinking);
|
|
2508
3015
|
let activeTransportConfig = {
|
|
2509
3016
|
region: initial.region,
|
|
2510
3017
|
streamIdleTimeoutMs: initial.streamIdleTimeoutMs,
|
|
3018
|
+
responseHeaderTimeoutMs: initial.responseHeaderTimeoutMs,
|
|
2511
3019
|
preserveThinking: initial.preserveThinking
|
|
2512
3020
|
};
|
|
2513
3021
|
const adapter = new QoderAdapter({
|
|
@@ -2519,11 +3027,12 @@ function apply(ctx, config = {}) {
|
|
|
2519
3027
|
const registration = ctx.llm.registerAdapter([providerQoder], adapter);
|
|
2520
3028
|
const refreshAdapter = () => {
|
|
2521
3029
|
const next = resolveConfig();
|
|
2522
|
-
if (next.region !== activeTransportConfig.region || next.streamIdleTimeoutMs !== activeTransportConfig.streamIdleTimeoutMs || next.preserveThinking !== activeTransportConfig.preserveThinking) {
|
|
2523
|
-
activeTransport = createTransport(next.region, next.streamIdleTimeoutMs, next.preserveThinking);
|
|
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);
|
|
2524
3032
|
activeTransportConfig = {
|
|
2525
3033
|
region: next.region,
|
|
2526
3034
|
streamIdleTimeoutMs: next.streamIdleTimeoutMs,
|
|
3035
|
+
responseHeaderTimeoutMs: next.responseHeaderTimeoutMs,
|
|
2527
3036
|
preserveThinking: next.preserveThinking
|
|
2528
3037
|
};
|
|
2529
3038
|
}
|
|
@@ -2577,8 +3086,10 @@ function apply(ctx, config = {}) {
|
|
|
2577
3086
|
});
|
|
2578
3087
|
const discoverModels = async (signal, suppliedPat) => {
|
|
2579
3088
|
const snapshot = resolveConfig();
|
|
3089
|
+
const snapshotTransport = activeTransport;
|
|
2580
3090
|
const normalizedPat = suppliedPat?.trim();
|
|
2581
|
-
const models = await (normalizedPat ? createTransport(snapshot.region, snapshot.streamIdleTimeoutMs, snapshot.preserveThinking, () => 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);
|
|
2582
3093
|
discoveredCatalogs[snapshot.region] = models;
|
|
2583
3094
|
refreshAdapter();
|
|
2584
3095
|
await persistDiscoveredModels(snapshot.region);
|
|
@@ -2593,40 +3104,69 @@ function apply(ctx, config = {}) {
|
|
|
2593
3104
|
maxTokens: model.maxTokens
|
|
2594
3105
|
}));
|
|
2595
3106
|
});
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
if (endpoint === "models") try {
|
|
2601
|
-
return {
|
|
2602
|
-
ok: true,
|
|
2603
|
-
value: await discoverModels(signal)
|
|
2604
|
-
};
|
|
2605
|
-
} catch (error) {
|
|
2606
|
-
if (signal.aborted || error instanceof QoderLlmError && error.code === "ABORTED") throw error;
|
|
2607
|
-
logger?.error?.("[Qoder RPC] Failed to discover models", logError(error));
|
|
2608
|
-
return publicError(error instanceof Error ? error.message : "Failed to discover Qoder models");
|
|
2609
|
-
}
|
|
2610
|
-
const force = typeof payload === "object" && payload !== null && "force" in payload ? payload.force === true : false;
|
|
2611
|
-
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) => {
|
|
2612
3111
|
try {
|
|
2613
|
-
const account = await activeTransport.readAccount({
|
|
2614
|
-
force,
|
|
2615
|
-
signal
|
|
2616
|
-
});
|
|
2617
|
-
logger?.debug?.("[Qoder RPC] Subscriber account resolved");
|
|
2618
3112
|
return {
|
|
2619
3113
|
ok: true,
|
|
2620
|
-
value:
|
|
3114
|
+
value: await task()
|
|
2621
3115
|
};
|
|
2622
3116
|
} catch (error) {
|
|
2623
|
-
if (signal.aborted || error instanceof QoderLlmError && error.code === "ABORTED")
|
|
2624
|
-
|
|
2625
|
-
|
|
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}`);
|
|
2626
3128
|
}
|
|
2627
3129
|
};
|
|
2628
|
-
|
|
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));
|
|
2629
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
|
+
});
|
|
2630
3170
|
}
|
|
2631
3171
|
//#endregion
|
|
2632
|
-
export { Config, apply, inject, name };
|
|
3172
|
+
export { Config, QODER_SEARCH_PROVIDER_ID, QoderSearchClient, QoderSearchProvider, apply, getQoderWebSearchUrl, inject, name, qoderWebSearchPath };
|