dsh-provider-qoder 0.2.0 → 0.3.0

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