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/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
- if (typeof rawExpires === "number" && rawExpires > 0) return new Date(rawExpires).toISOString();
1432
- if (typeof rawExpires === "string" && rawExpires.length > 0) {
1433
- const parsed = Date.parse(rawExpires);
1434
- if (!Number.isNaN(parsed) && parsed > 0) return new Date(parsed).toISOString();
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: data
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
- terminalKind = finishKind(choice.finish_reason) ?? terminalKind;
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
- ...qoderRequestId(response.headers) === void 0 ? {} : { requestId: qoderRequestId(response.headers) }
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
- yield* parseQoderSse(response.body, { onActivity: resetIdleTimer });
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), { cause: redactLogValue(error) });
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 ?? 3e4;
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
- preserveThinking: z.boolean().default(true)
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-official";
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
- listModels(provider) {
2386
- return Promise.resolve(this.catalogModels.map((model) => modelInfo(provider, model)));
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.catalogModels.find((model) => model.id === modelId);
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.catalogModels.find((candidate) => candidate.id === (options.model || "cmodel"));
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 = "qoder-official";
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: "internal",
2966
+ code,
2465
2967
  message,
2466
- details: { issues: [] }
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
- preserveThinking: config.preserveThinking ?? true
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
- preserveThinking: value.preserveThinking ?? true
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)) : activeTransport).discoverModels(signal);
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
- if (ctx.connection?.rpc) {
2597
- const handler = async (endpoint, payload, signal) => {
2598
- if (endpoint !== "account" && endpoint !== "models") return publicError(`Unknown endpoint: ${endpoint}`);
2599
- signal.throwIfAborted();
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: account
3114
+ value: await task()
2621
3115
  };
2622
3116
  } catch (error) {
2623
- if (signal.aborted || error instanceof QoderLlmError && error.code === "ABORTED") throw error;
2624
- logger?.error?.("[Qoder RPC] Failed to read subscriber account", logError(error));
2625
- return publicError(error instanceof Error ? error.message : "Failed to load Qoder account");
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
- ctx.effect(() => ctx.connection.rpc.handle(qoderChannel, handler), "provider-qoder: loopback account RPC");
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 };