@omnicross/daemon 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/dist/cli.js CHANGED
@@ -1139,8 +1139,16 @@ ${turn.responseBody}`);
1139
1139
  import { parseArgs as parseArgs2 } from "util";
1140
1140
  import {
1141
1141
  DEFAULT_IMAGES_SERVER_CONFIG as DEFAULT_IMAGES_SERVER_CONFIG2,
1142
- loadServerConfig as loadServerConfig3
1142
+ loadServerConfig as loadServerConfig4
1143
1143
  } from "@omnicross/core/outbound-api";
1144
+ import {
1145
+ apiSearchContributions as apiSearchContributions3
1146
+ } from "@omnicross/core/search/api";
1147
+ import { builtinHttpSearchContributions as builtinHttpSearchContributions4 } from "@omnicross/core/search/http";
1148
+ import {
1149
+ DEFAULT_SEARCH_FRONTEND_MODES,
1150
+ SEARCH_FRONTEND_NAMES
1151
+ } from "@omnicross/core/search";
1144
1152
 
1145
1153
  // src/bootstrap.ts
1146
1154
  import { accessSync, constants as fsConstants, existsSync as existsSync29, mkdirSync as mkdirSync9 } from "fs";
@@ -1155,7 +1163,8 @@ import {
1155
1163
  DEFAULT_ACCOUNT_PROBE,
1156
1164
  DEFAULT_OUTBOUND_PORT,
1157
1165
  getOutboundApiServer,
1158
- normalizeServerConfig as normalizeServerConfig2
1166
+ normalizeServerConfig as normalizeServerConfig2,
1167
+ validateSearchServerConfig as validateSearchServerConfig2
1159
1168
  } from "@omnicross/core/outbound-api";
1160
1169
  import { setSubscriptionRegistryForOutbound } from "@omnicross/core/outbound-api/subscriptionRegistryPort";
1161
1170
  import { getSharedAccountHealth as getSharedAccountHealth4 } from "@omnicross/core/pipeline/SubscriptionAccountHealth";
@@ -2012,14 +2021,16 @@ import http from "http";
2012
2021
  import {
2013
2022
  createNamedKey,
2014
2023
  DEFAULT_IMAGES_SERVER_CONFIG,
2024
+ DEFAULT_SEARCH_SERVER_CONFIG as DEFAULT_SEARCH_SERVER_CONFIG2,
2015
2025
  effectiveOutboundPermissions as effectiveOutboundPermissions2,
2016
2026
  gatewayBindingToEndpointConfig,
2017
2027
  isKindMappedEndpoint,
2018
- loadServerConfig as loadServerConfig2,
2028
+ loadServerConfig as loadServerConfig3,
2019
2029
  mergeServerConfig,
2020
2030
  normalizeProxyConfig,
2021
2031
  saveServerConfig,
2022
- validateOutboundPermissions
2032
+ validateOutboundPermissions,
2033
+ validateSearchServerConfig
2023
2034
  } from "@omnicross/core/outbound-api";
2024
2035
  import {
2025
2036
  IMAGE_CAPABILITY_UNAVAILABLE_REASONS
@@ -4585,6 +4596,510 @@ async function handleDashboard(deps) {
4585
4596
  return { status: 200, body: summary };
4586
4597
  }
4587
4598
 
4599
+ // src/admin/searchAdminApi.ts
4600
+ import { DEFAULT_SEARCH_SERVER_CONFIG, loadServerConfig } from "@omnicross/core/outbound-api";
4601
+ import { apiSearchContributions as apiSearchContributions2 } from "@omnicross/core/search/api";
4602
+ import { builtinHttpSearchContributions as builtinHttpSearchContributions3, createSearchHttpTransport as createSearchHttpTransport2 } from "@omnicross/core/search/http";
4603
+ import { createSearchRuntime as createSearchRuntime2 } from "@omnicross/core/search";
4604
+
4605
+ // src/search/searchDoctorProjection.ts
4606
+ import { toSearchErrorShape } from "@omnicross/contracts/search-types";
4607
+ import {
4608
+ JINA_CAPABILITIES,
4609
+ SEARXNG_CAPABILITIES,
4610
+ TAVILY_CAPABILITIES,
4611
+ ZHIPU_CAPABILITIES
4612
+ } from "@omnicross/core/search/api";
4613
+ import { builtinHttpSearchContributions } from "@omnicross/core/search/http";
4614
+ var API_DOCTOR_PROVIDERS = [
4615
+ {
4616
+ id: "tavily",
4617
+ capabilities: TAVILY_CAPABILITIES,
4618
+ configured: (configs) => configs.tavily !== void 0,
4619
+ missingReason: "no API key configured"
4620
+ },
4621
+ {
4622
+ id: "jina",
4623
+ capabilities: JINA_CAPABILITIES,
4624
+ configured: (configs) => configs.jina !== void 0,
4625
+ // Honest about the asymmetry: Jina CAN run keyless, but a provider nobody
4626
+ // asked for is still not enabled.
4627
+ missingReason: "not configured (Jina can run without a key, but must be enabled explicitly)"
4628
+ },
4629
+ {
4630
+ id: "searxng",
4631
+ capabilities: SEARXNG_CAPABILITIES,
4632
+ configured: (configs) => configs.searxng !== void 0,
4633
+ missingReason: "no API host configured"
4634
+ },
4635
+ {
4636
+ id: "zhipu",
4637
+ capabilities: ZHIPU_CAPABILITIES,
4638
+ configured: (configs) => configs.zhipu !== void 0,
4639
+ missingReason: "no API key configured"
4640
+ },
4641
+ {
4642
+ id: "z.ai",
4643
+ capabilities: ZHIPU_CAPABILITIES,
4644
+ configured: (configs) => configs["z.ai"] !== void 0,
4645
+ missingReason: "no API key configured"
4646
+ }
4647
+ ];
4648
+ function buildSearchDoctorSnapshot(contributions = builtinHttpSearchContributions(), apiConfigs) {
4649
+ const rows = contributions.map((contribution) => ({
4650
+ providerId: contribution.id,
4651
+ source: contribution.source,
4652
+ kind: contribution.kind,
4653
+ capabilities: contribution.capabilities
4654
+ }));
4655
+ if (apiConfigs === void 0) return rows;
4656
+ for (const provider of API_DOCTOR_PROVIDERS) {
4657
+ if (provider.configured(apiConfigs)) continue;
4658
+ rows.push({
4659
+ providerId: provider.id,
4660
+ source: "builtin",
4661
+ kind: "api",
4662
+ capabilities: provider.capabilities,
4663
+ status: "unconfigured",
4664
+ reason: provider.missingReason
4665
+ });
4666
+ }
4667
+ return rows;
4668
+ }
4669
+ var SEARCH_DOCTOR_QUERY = "MDN HTTP headers documentation";
4670
+ function classifyLiveSearchOutcome(providerId, outcome, checkedAt) {
4671
+ if (outcome.kind === "results") {
4672
+ if (outcome.count > 0) return { providerId, status: "healthy", checkedAt };
4673
+ return {
4674
+ providerId,
4675
+ status: "degraded",
4676
+ checkedAt,
4677
+ reason: "reachable, but the engine returned no usable results (possible partial drift)"
4678
+ };
4679
+ }
4680
+ const error = toSearchErrorShape(outcome.error);
4681
+ const stage = error.details?.stage;
4682
+ const { status, reason } = classifySearchFailure(stage, error.code);
4683
+ return { providerId, status, checkedAt, reason, error };
4684
+ }
4685
+ function classifySearchFailure(stage, code) {
4686
+ if (stage === "challenge") {
4687
+ return { status: "blocked", reason: "the engine served a bot challenge instead of results" };
4688
+ }
4689
+ if (stage === "trust") {
4690
+ return {
4691
+ status: "blocked",
4692
+ reason: "the engine served a page that failed the anti-decoy trust check"
4693
+ };
4694
+ }
4695
+ if (code === "policy_denied") {
4696
+ return {
4697
+ status: "blocked",
4698
+ reason: "the egress policy refused the request target"
4699
+ };
4700
+ }
4701
+ if (code === "parse_failed") {
4702
+ return {
4703
+ status: "failed",
4704
+ reason: "the response was not recognizable as a search result page (parser drift suspected)"
4705
+ };
4706
+ }
4707
+ if (code === "timeout") {
4708
+ return { status: "failed", reason: "the request exceeded its time budget" };
4709
+ }
4710
+ return { status: "failed", reason: `the request failed (${code})` };
4711
+ }
4712
+ async function runSearchLiveChecks(contributions, now = () => (/* @__PURE__ */ new Date()).toISOString()) {
4713
+ const diagnostics = [];
4714
+ for (const contribution of contributions) {
4715
+ try {
4716
+ const results = await contribution.provider.search(SEARCH_DOCTOR_QUERY, { maxResults: 5 });
4717
+ diagnostics.push(
4718
+ classifyLiveSearchOutcome(contribution.id, { kind: "results", count: results.length }, now())
4719
+ );
4720
+ } catch (error) {
4721
+ diagnostics.push(classifyLiveSearchOutcome(contribution.id, { kind: "failure", error }, now()));
4722
+ }
4723
+ }
4724
+ return diagnostics;
4725
+ }
4726
+
4727
+ // src/search/SearchAssembly.ts
4728
+ import { resolveUpstreamDispatcher } from "@omnicross/core/pipeline/upstreamFetch";
4729
+ import { createSearchRuntime } from "@omnicross/core/search";
4730
+ import { apiSearchContributions } from "@omnicross/core/search/api";
4731
+ import {
4732
+ builtinHttpSearchContributions as builtinHttpSearchContributions2,
4733
+ createSearchHttpTransport
4734
+ } from "@omnicross/core/search/http";
4735
+ function searchEgressPolicyFrom(config) {
4736
+ const hosts = config.egress.allowedPrivateHosts;
4737
+ return hosts.length > 0 ? { allowedPrivateHosts: [...hosts] } : {};
4738
+ }
4739
+ function searchPolicyFrom(config) {
4740
+ const { preferred, allowed, fallbackEnabled, maxAttempts } = config.policy;
4741
+ return {
4742
+ ...preferred !== void 0 ? { preferred } : {},
4743
+ ...allowed !== void 0 ? { allowed: [...allowed] } : {},
4744
+ fallbackEnabled,
4745
+ ...maxAttempts !== void 0 ? { maxAttempts } : {}
4746
+ };
4747
+ }
4748
+ function resolveSearchUpstreamDispatcher(url) {
4749
+ return resolveUpstreamDispatcher({ url });
4750
+ }
4751
+ var searchUpstreamProxyConfig = createUpstreamProxyResolver();
4752
+ function resolveSearchUpstreamProxyConfig(url) {
4753
+ return searchUpstreamProxyConfig({ url });
4754
+ }
4755
+ function searchContributionsFrom(config) {
4756
+ return [
4757
+ ...builtinHttpSearchContributions2(
4758
+ createSearchHttpTransport({
4759
+ resolveProxyDispatcher: resolveSearchUpstreamDispatcher,
4760
+ resolveProxyConfig: resolveSearchUpstreamProxyConfig
4761
+ })
4762
+ ),
4763
+ ...apiSearchContributions(config.providers, {
4764
+ egressPolicy: searchEgressPolicyFrom(config),
4765
+ resolveProxyDispatcher: resolveSearchUpstreamDispatcher
4766
+ })
4767
+ ];
4768
+ }
4769
+ function buildSearchRuntime(config, options = {}) {
4770
+ const logger = options.logger ?? null;
4771
+ return createSearchRuntime({
4772
+ contributions: options.contributions ?? searchContributionsFrom(config),
4773
+ policy: searchPolicyFrom(config),
4774
+ ...logger ? {
4775
+ onEvent: (event) => {
4776
+ logger.debug(`[search] ${formatSearchEvent(event)}`);
4777
+ }
4778
+ } : {}
4779
+ });
4780
+ }
4781
+ function formatSearchEvent(event) {
4782
+ const parts = [
4783
+ `type=${event.type}`,
4784
+ `request=${event.requestId}`,
4785
+ `queryHash=${event.queryHash}`,
4786
+ `durationMs=${event.durationMs}`
4787
+ ];
4788
+ if ("providerId" in event && event.providerId !== void 0) {
4789
+ parts.push(`provider=${event.providerId}`);
4790
+ }
4791
+ if ("outcome" in event && event.outcome !== void 0) parts.push(`outcome=${event.outcome}`);
4792
+ if ("errorCode" in event && event.errorCode !== void 0) parts.push(`error=${event.errorCode}`);
4793
+ if ("resultCount" in event && event.resultCount !== void 0) {
4794
+ parts.push(`results=${event.resultCount}`);
4795
+ }
4796
+ if ("fallbackCount" in event && event.fallbackCount !== void 0) {
4797
+ parts.push(`fallbacks=${event.fallbackCount}`);
4798
+ }
4799
+ return parts.join(" ");
4800
+ }
4801
+
4802
+ // src/admin/searchAdminApi.ts
4803
+ var KEYLESS_HTTP_PROVIDER_IDS = /* @__PURE__ */ new Set(["http-bing", "http-duckduckgo"]);
4804
+ var API_PROVIDER_IDS = /* @__PURE__ */ new Set([
4805
+ "tavily",
4806
+ "jina",
4807
+ "searxng",
4808
+ "zhipu",
4809
+ "z.ai"
4810
+ ]);
4811
+ var SEARCH_QUERY_MAX_CODE_UNITS = 256;
4812
+ var QUERY_CONTROL_CHARS = /[\u0000-\u001f\u007f]/u;
4813
+ var SEARCH_RESULT_FIELD_CAPS = { title: 512, url: 2048, content: 1024 };
4814
+ var SEARCH_QUERY_MAX_RESULTS = 5;
4815
+ function sanitizeResultField(value, cap) {
4816
+ const text = typeof value === "string" ? value : value === null || value === void 0 ? "" : String(value);
4817
+ return text.replace(/[\u0000-\u001f\u007f]/gu, "").slice(0, cap);
4818
+ }
4819
+ function writeJson(res, status, body) {
4820
+ res.writeHead(status, { "Content-Type": "application/json" });
4821
+ res.end(JSON.stringify(body));
4822
+ }
4823
+ function writeErr(res, status, message) {
4824
+ writeJson(res, status, { error: { type: "admin_api_error", message } });
4825
+ }
4826
+ var SEARCH_MAX_BODY_BYTES = 64 * 1024;
4827
+ var SearchBodyTooLargeError = class extends Error {
4828
+ };
4829
+ async function readJsonBody2(req) {
4830
+ const chunks = [];
4831
+ let bytes = 0;
4832
+ for await (const chunk of req) {
4833
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
4834
+ bytes += buffer.length;
4835
+ if (bytes > SEARCH_MAX_BODY_BYTES) throw new SearchBodyTooLargeError("request body is too large");
4836
+ chunks.push(buffer);
4837
+ }
4838
+ const raw = Buffer.concat(chunks).toString("utf8");
4839
+ if (!raw.trim()) return {};
4840
+ try {
4841
+ const parsed = JSON.parse(raw);
4842
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
4843
+ } catch {
4844
+ return {};
4845
+ }
4846
+ }
4847
+ async function readBodyOrReject(req, res) {
4848
+ try {
4849
+ return await readJsonBody2(req);
4850
+ } catch (error) {
4851
+ if (error instanceof SearchBodyTooLargeError) {
4852
+ writeErr(res, 400, error.message);
4853
+ return void 0;
4854
+ }
4855
+ throw error;
4856
+ }
4857
+ }
4858
+ async function handleSearchAdmin(req, res, method, rest, deps) {
4859
+ if (rest.length === 1 && rest[0] === "diagnostics") {
4860
+ if (!deps.searchStatus) {
4861
+ return writeErr(res, 501, "Search status is not available in this build");
4862
+ }
4863
+ if (method !== "GET") {
4864
+ return writeErr(res, 405, `method ${method} not allowed on search diagnostics`);
4865
+ }
4866
+ return handleSearchDiagnostics(res, deps);
4867
+ }
4868
+ if (rest.length === 1 && rest[0] === "test") {
4869
+ if (!deps.searchStatus) {
4870
+ return writeErr(res, 501, "Search status is not available in this build");
4871
+ }
4872
+ if (method !== "POST") {
4873
+ return writeErr(res, 405, `method ${method} not allowed on search test`);
4874
+ }
4875
+ return handleSearchTest(req, res, deps);
4876
+ }
4877
+ if (rest.length === 1 && rest[0] === "query") {
4878
+ if (!deps.searchStatus) {
4879
+ return writeErr(res, 501, "Search status is not available in this build");
4880
+ }
4881
+ if (method !== "POST") {
4882
+ return writeErr(res, 405, `method ${method} not allowed on search query`);
4883
+ }
4884
+ return handleSearchQuery(req, res, deps);
4885
+ }
4886
+ return writeErr(res, 404, `unknown search route '/${rest.join("/")}'`);
4887
+ }
4888
+ async function handleSearchDiagnostics(res, deps) {
4889
+ const status = deps.searchStatus;
4890
+ const persisted = await loadServerConfig(deps.settingsStore);
4891
+ const search = persisted.search ?? DEFAULT_SEARCH_SERVER_CONFIG;
4892
+ const rows = buildSearchDoctorSnapshot(
4893
+ status.runtime.listProviders(),
4894
+ search.providers
4895
+ );
4896
+ const snapshot = {
4897
+ rows,
4898
+ modes: {
4899
+ // codex is read from the LIVE config per request — an admin PUT has
4900
+ // already applied. responses/anthropic were captured at bootstrap.
4901
+ codex: search.modes.codex,
4902
+ responses: status.modes.responses,
4903
+ anthropic: status.modes.anthropic
4904
+ },
4905
+ applySemantics: { codex: "immediate", rest: "restart" }
4906
+ };
4907
+ return writeJson(res, 200, { diagnostics: snapshot });
4908
+ }
4909
+ function persistedSearchContributions(search, fetchImpl) {
4910
+ if (fetchImpl) {
4911
+ const egressPolicy = searchEgressPolicyFrom(search);
4912
+ return [
4913
+ ...builtinHttpSearchContributions3(
4914
+ createSearchHttpTransport2({ fetch: fetchImpl, egressPolicy })
4915
+ ),
4916
+ ...apiSearchContributions2(search.providers, { egressPolicy, fetchImpl })
4917
+ ];
4918
+ }
4919
+ return searchContributionsFrom(search);
4920
+ }
4921
+ async function handleSearchTest(req, res, deps) {
4922
+ const status = deps.searchStatus;
4923
+ const body = await readBodyOrReject(req, res);
4924
+ if (body === void 0) return;
4925
+ const providerId = body["providerId"];
4926
+ if (typeof providerId !== "string" || providerId.length === 0) {
4927
+ return writeErr(res, 400, "providerId must be a non-empty string");
4928
+ }
4929
+ if (!KEYLESS_HTTP_PROVIDER_IDS.has(providerId) && !API_PROVIDER_IDS.has(providerId)) {
4930
+ return writeErr(res, 404, `unknown search provider '${providerId}'`);
4931
+ }
4932
+ const persisted = await loadServerConfig(deps.settingsStore);
4933
+ const search = persisted.search ?? DEFAULT_SEARCH_SERVER_CONFIG;
4934
+ const providers = search.providers;
4935
+ if (!KEYLESS_HTTP_PROVIDER_IDS.has(providerId) && providers[providerId] === void 0) {
4936
+ return writeErr(res, 400, `search provider '${providerId}' is not configured`);
4937
+ }
4938
+ const fetchImpl = status.testFetch;
4939
+ const contributions = persistedSearchContributions(search, fetchImpl);
4940
+ const contribution = contributions.find((c) => c.id === providerId);
4941
+ if (!contribution) {
4942
+ return writeErr(res, 400, `search provider '${providerId}' is not configured`);
4943
+ }
4944
+ const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
4945
+ try {
4946
+ const results = await contribution.provider.search(SEARCH_DOCTOR_QUERY, { maxResults: 5 });
4947
+ const diagnostic = classifyLiveSearchOutcome(
4948
+ contribution.id,
4949
+ { kind: "results", count: results.length },
4950
+ checkedAt
4951
+ );
4952
+ const response = { diagnostic, resultCount: results.length };
4953
+ return writeJson(res, 200, { result: response });
4954
+ } catch (error) {
4955
+ const diagnostic = classifyLiveSearchOutcome(contribution.id, { kind: "failure", error }, checkedAt);
4956
+ const response = { diagnostic };
4957
+ return writeJson(res, 200, { result: response });
4958
+ }
4959
+ }
4960
+ async function handleSearchQuery(req, res, deps) {
4961
+ const status = deps.searchStatus;
4962
+ const body = await readBodyOrReject(req, res);
4963
+ if (body === void 0) return;
4964
+ const providerId = body["providerId"];
4965
+ if (typeof providerId !== "string" || providerId.length === 0) {
4966
+ return writeErr(res, 400, "providerId must be a non-empty string");
4967
+ }
4968
+ if (!KEYLESS_HTTP_PROVIDER_IDS.has(providerId) && !API_PROVIDER_IDS.has(providerId)) {
4969
+ return writeErr(res, 404, `unknown search provider '${providerId}'`);
4970
+ }
4971
+ const query2 = body["query"];
4972
+ if (typeof query2 !== "string" || query2.trim().length === 0) {
4973
+ return writeErr(res, 400, "query must be a non-empty string");
4974
+ }
4975
+ if (query2.length > SEARCH_QUERY_MAX_CODE_UNITS) {
4976
+ return writeErr(res, 400, `query must be at most ${SEARCH_QUERY_MAX_CODE_UNITS} characters`);
4977
+ }
4978
+ if (QUERY_CONTROL_CHARS.test(query2)) {
4979
+ return writeErr(res, 400, "query must not contain control characters");
4980
+ }
4981
+ const persisted = await loadServerConfig(deps.settingsStore);
4982
+ const search = persisted.search ?? DEFAULT_SEARCH_SERVER_CONFIG;
4983
+ const providers = search.providers;
4984
+ if (!KEYLESS_HTTP_PROVIDER_IDS.has(providerId) && providers[providerId] === void 0) {
4985
+ return writeErr(res, 400, `search provider '${providerId}' is not configured`);
4986
+ }
4987
+ const fetchImpl = status.testFetch;
4988
+ const runtime = createSearchRuntime2({
4989
+ contributions: persistedSearchContributions(search, fetchImpl),
4990
+ policy: {
4991
+ ...searchPolicyFrom(search),
4992
+ // The panel always walks: it answers "does a search WORK for this
4993
+ // operator", not "does this one provider behave" — that is `/test`'s
4994
+ // job. The persisted policy's allowlist still bounds the walk.
4995
+ fallbackEnabled: true,
4996
+ preferred: providerId
4997
+ }
4998
+ });
4999
+ const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
5000
+ try {
5001
+ const orchestrated = await runtime.search({ query: query2, options: { maxResults: 5 } });
5002
+ const results = orchestrated.results;
5003
+ const sanitized = results.slice(0, SEARCH_QUERY_MAX_RESULTS).map((result) => ({
5004
+ title: sanitizeResultField(result.title, SEARCH_RESULT_FIELD_CAPS.title),
5005
+ url: sanitizeResultField(result.url, SEARCH_RESULT_FIELD_CAPS.url),
5006
+ content: sanitizeResultField(result.content, SEARCH_RESULT_FIELD_CAPS.content)
5007
+ }));
5008
+ const diagnostic = sanitized.length === 0 ? { providerId: orchestrated.providerId, status: "healthy", checkedAt } : classifyLiveSearchOutcome(
5009
+ orchestrated.providerId,
5010
+ { kind: "results", count: sanitized.length },
5011
+ checkedAt
5012
+ );
5013
+ const response = {
5014
+ diagnostic,
5015
+ providerUsed: orchestrated.providerId,
5016
+ fallbackCount: orchestrated.fallbackCount,
5017
+ resultCount: sanitized.length,
5018
+ results: sanitized
5019
+ };
5020
+ return writeJson(res, 200, { result: response });
5021
+ } catch (error) {
5022
+ const diagnostic = classifyLiveSearchOutcome(providerId, { kind: "failure", error }, checkedAt);
5023
+ const response = { diagnostic };
5024
+ return writeJson(res, 200, { result: response });
5025
+ }
5026
+ }
5027
+
5028
+ // src/admin/searchAdminView.ts
5029
+ var API_KEY_PROVIDERS = /* @__PURE__ */ new Set(["tavily", "jina", "zhipu", "z.ai"]);
5030
+ var BASIC_AUTH_PROVIDERS = /* @__PURE__ */ new Set(["searxng"]);
5031
+ function isRecord(value) {
5032
+ return value !== null && typeof value === "object" && !Array.isArray(value);
5033
+ }
5034
+ function redactSearchServerConfig(search) {
5035
+ const providers = {};
5036
+ for (const [id, raw] of Object.entries(search.providers)) {
5037
+ const entry = raw;
5038
+ const view = {};
5039
+ if (entry.apiHost !== void 0) view.apiHost = entry.apiHost;
5040
+ if (entry.basicAuthUsername !== void 0) view.basicAuthUsername = entry.basicAuthUsername;
5041
+ if (API_KEY_PROVIDERS.has(id)) {
5042
+ view.apiKeyConfigured = typeof entry.apiKey === "string" && entry.apiKey.length > 0;
5043
+ }
5044
+ if (BASIC_AUTH_PROVIDERS.has(id)) {
5045
+ view.basicAuthPasswordConfigured = typeof entry.basicAuthPassword === "string" && entry.basicAuthPassword.length > 0;
5046
+ }
5047
+ providers[id] = view;
5048
+ }
5049
+ return {
5050
+ modes: search.modes,
5051
+ providers,
5052
+ egress: { allowedPrivateHosts: [...search.egress.allowedPrivateHosts] },
5053
+ policy: { ...search.policy, ...search.policy.allowed ? { allowed: [...search.policy.allowed] } : {} }
5054
+ };
5055
+ }
5056
+ function storedSecrets(current, id) {
5057
+ const entry = current.providers[id];
5058
+ if (!entry) return {};
5059
+ const out = {};
5060
+ if (typeof entry.apiKey === "string" && entry.apiKey.length > 0) out.apiKey = entry.apiKey;
5061
+ if (typeof entry.basicAuthPassword === "string" && entry.basicAuthPassword.length > 0) {
5062
+ out.basicAuthPassword = entry.basicAuthPassword;
5063
+ }
5064
+ return out;
5065
+ }
5066
+ function resolveSecretField(entry, field, stored) {
5067
+ if (!(field in entry)) {
5068
+ if (stored !== void 0) entry[field] = stored;
5069
+ return;
5070
+ }
5071
+ const value = entry[field];
5072
+ if (value === null) {
5073
+ delete entry[field];
5074
+ return;
5075
+ }
5076
+ if (typeof value === "string" && value.trim().length > 0) return;
5077
+ if (stored !== void 0) entry[field] = stored;
5078
+ else delete entry[field];
5079
+ }
5080
+ function preserveSearchSecrets(incoming, current) {
5081
+ if (!isRecord(incoming)) return incoming;
5082
+ const section = { ...incoming };
5083
+ const providersValue = section["providers"];
5084
+ if (!isRecord(providersValue)) return section;
5085
+ const providers = {};
5086
+ for (const [id, entryValue] of Object.entries(providersValue)) {
5087
+ if (!isRecord(entryValue)) {
5088
+ providers[id] = entryValue;
5089
+ continue;
5090
+ }
5091
+ const entry = { ...entryValue };
5092
+ delete entry["apiKeyConfigured"];
5093
+ delete entry["basicAuthPasswordConfigured"];
5094
+ const stored = storedSecrets(current, id);
5095
+ resolveSecretField(entry, "apiKey", stored.apiKey);
5096
+ resolveSecretField(entry, "basicAuthPassword", stored.basicAuthPassword);
5097
+ providers[id] = entry;
5098
+ }
5099
+ section["providers"] = providers;
5100
+ return section;
5101
+ }
5102
+
4588
5103
  // src/admin/keyPolicyBody.ts
4589
5104
  function parseKeyPolicyBody(body) {
4590
5105
  const policy = {};
@@ -4647,7 +5162,7 @@ function parseKeyPolicyBody(body) {
4647
5162
  var ENDPOINTS = /* @__PURE__ */ new Set(["chat", "responses", "messages", "gemini"]);
4648
5163
  var TARGET_KINDS = /* @__PURE__ */ new Set(["account", "account-group", "account-pool", "provider"]);
4649
5164
  var FALLBACKS = /* @__PURE__ */ new Set(["next", "fail", "global"]);
4650
- function isRecord(value) {
5165
+ function isRecord2(value) {
4651
5166
  return !!value && typeof value === "object" && !Array.isArray(value);
4652
5167
  }
4653
5168
  function nonBlank(value) {
@@ -4667,7 +5182,7 @@ function validateGatewayBindingsSegment(patch) {
4667
5182
  const ids = /* @__PURE__ */ new Set();
4668
5183
  raw.forEach((entry, index) => {
4669
5184
  const path2 = `bindings[${index}]`;
4670
- if (!isRecord(entry)) {
5185
+ if (!isRecord2(entry)) {
4671
5186
  errors.push(`${path2} must be an object`);
4672
5187
  return;
4673
5188
  }
@@ -4696,12 +5211,12 @@ function validateGatewayBindingsSegment(patch) {
4696
5211
  } else if (entry.modelMappings.length > 100) {
4697
5212
  errors.push(`${path2}.modelMappings cannot contain more than 100 entries`);
4698
5213
  } else if (entry.modelMappings.some(
4699
- (mapping) => !isRecord(mapping) || !nonBlank(mapping.source) || !nonBlank(mapping.target)
5214
+ (mapping) => !isRecord2(mapping) || !nonBlank(mapping.source) || !nonBlank(mapping.target)
4700
5215
  )) {
4701
5216
  errors.push(`${path2}.modelMappings must contain non-empty source and target strings`);
4702
5217
  }
4703
5218
  }
4704
- if (!isRecord(entry.target) || !TARGET_KINDS.has(String(entry.target.kind))) {
5219
+ if (!isRecord2(entry.target) || !TARGET_KINDS.has(String(entry.target.kind))) {
4705
5220
  errors.push(`${path2}.target is invalid`);
4706
5221
  } else {
4707
5222
  if (!nonBlank(entry.target.providerId)) errors.push(`${path2}.target.providerId is required`);
@@ -4716,7 +5231,7 @@ function validateGatewayBindingsSegment(patch) {
4716
5231
  }
4717
5232
  }
4718
5233
  if (entry.modelMap !== void 0) {
4719
- if (!isRecord(entry.modelMap) || Object.values(entry.modelMap).some((value) => typeof value !== "string")) {
5234
+ if (!isRecord2(entry.modelMap) || Object.values(entry.modelMap).some((value) => typeof value !== "string")) {
4720
5235
  errors.push(`${path2}.modelMap must contain string values`);
4721
5236
  }
4722
5237
  }
@@ -4737,19 +5252,19 @@ function validateGatewayBindingsSegment(patch) {
4737
5252
  import {
4738
5253
  generateVoucherCode,
4739
5254
  hashVoucherCode,
4740
- loadServerConfig,
5255
+ loadServerConfig as loadServerConfig2,
4741
5256
  newVoucherId,
4742
5257
  toVoucherInfo,
4743
5258
  voucherCodePrefix
4744
5259
  } from "@omnicross/core/outbound-api";
4745
- function writeJson(res, status, body) {
5260
+ function writeJson2(res, status, body) {
4746
5261
  res.writeHead(status, { "Content-Type": "application/json" });
4747
5262
  res.end(JSON.stringify(body));
4748
5263
  }
4749
- function writeErr(res, status, message) {
4750
- writeJson(res, status, { error: { type: "voucher_error", message } });
5264
+ function writeErr2(res, status, message) {
5265
+ writeJson2(res, status, { error: { type: "voucher_error", message } });
4751
5266
  }
4752
- function readJsonBody2(req) {
5267
+ function readJsonBody3(req) {
4753
5268
  return new Promise((resolve11, reject) => {
4754
5269
  const chunks = [];
4755
5270
  req.on("data", (c) => chunks.push(c));
@@ -4800,26 +5315,26 @@ function parseVoucherCreateBody(body) {
4800
5315
  return { ok: true, input };
4801
5316
  }
4802
5317
  async function voucherEnabled(deps) {
4803
- const config = await loadServerConfig(deps.settingsStore);
5318
+ const config = await loadServerConfig2(deps.settingsStore);
4804
5319
  return config.voucher?.enabled === true;
4805
5320
  }
4806
5321
  async function handleVoucher(req, res, method, rest, deps) {
4807
5322
  const voucherDb = deps.voucherDb;
4808
- if (!voucherDb) return writeErr(res, 501, "Voucher feature is not available");
5323
+ if (!voucherDb) return writeErr2(res, 501, "Voucher feature is not available");
4809
5324
  if (method === "GET" && rest.length === 0) {
4810
5325
  const rows = await voucherDb.voucherList();
4811
- return writeJson(res, 200, { vouchers: rows.map(toVoucherInfo) });
5326
+ return writeJson2(res, 200, { vouchers: rows.map(toVoucherInfo) });
4812
5327
  }
4813
5328
  if (method === "POST" && rest.length === 0) {
4814
- if (!await voucherEnabled(deps)) return writeErr(res, 403, "Voucher feature is disabled");
5329
+ if (!await voucherEnabled(deps)) return writeErr2(res, 403, "Voucher feature is disabled");
4815
5330
  let body;
4816
5331
  try {
4817
- body = await readJsonBody2(req);
5332
+ body = await readJsonBody3(req);
4818
5333
  } catch {
4819
- return writeErr(res, 400, "Invalid JSON in request body");
5334
+ return writeErr2(res, 400, "Invalid JSON in request body");
4820
5335
  }
4821
5336
  const parsed = parseVoucherCreateBody(body);
4822
- if (!parsed.ok) return writeErr(res, 400, parsed.message);
5337
+ if (!parsed.ok) return writeErr2(res, 400, parsed.message);
4823
5338
  const code = generateVoucherCode();
4824
5339
  const created = await voucherDb.voucherCreate({
4825
5340
  id: newVoucherId(),
@@ -4827,7 +5342,7 @@ async function handleVoucher(req, res, method, rest, deps) {
4827
5342
  codePrefix: voucherCodePrefix(code),
4828
5343
  ...parsed.input
4829
5344
  });
4830
- return writeJson(res, 201, {
5345
+ return writeJson2(res, 201, {
4831
5346
  id: created.id,
4832
5347
  codePrefix: created.codePrefix,
4833
5348
  type: created.type,
@@ -4838,11 +5353,11 @@ async function handleVoucher(req, res, method, rest, deps) {
4838
5353
  }
4839
5354
  const id = rest[0];
4840
5355
  if (method === "POST" && id && rest[1] === "revoke") {
4841
- if (!await voucherEnabled(deps)) return writeErr(res, 403, "Voucher feature is disabled");
5356
+ if (!await voucherEnabled(deps)) return writeErr2(res, 403, "Voucher feature is disabled");
4842
5357
  const ok = await voucherDb.voucherRevokeCas(id, Date.now());
4843
- return writeJson(res, ok ? 200 : 409, { ok });
5358
+ return writeJson2(res, ok ? 200 : 409, { ok });
4844
5359
  }
4845
- return writeErr(res, 405, `method ${method} not allowed on voucher`);
5360
+ return writeErr2(res, 405, `method ${method} not allowed on voucher`);
4846
5361
  }
4847
5362
 
4848
5363
  // src/admin/webhookConfigBody.ts
@@ -5756,12 +6271,12 @@ async function handlePricingResolveConflicts(body, deps) {
5756
6271
  }
5757
6272
 
5758
6273
  // src/admin/accountAllowanceApi.ts
5759
- function writeJson2(res, status, body) {
6274
+ function writeJson3(res, status, body) {
5760
6275
  res.writeHead(status, { "Content-Type": "application/json" });
5761
6276
  res.end(JSON.stringify(body));
5762
6277
  }
5763
6278
  function writeError2(res, status, message) {
5764
- writeJson2(res, status, { error: { type: "account_allowance_error", message } });
6279
+ writeJson3(res, status, { error: { type: "account_allowance_error", message } });
5765
6280
  }
5766
6281
  function readJson2(req) {
5767
6282
  return new Promise((resolve11, reject) => {
@@ -5794,7 +6309,7 @@ async function handleAccountAllowanceApi(req, res, method, rest, service) {
5794
6309
  if (!service.getSchedulingStatus) {
5795
6310
  return writeError2(res, 501, "allowance scheduling diagnostics are not available");
5796
6311
  }
5797
- return writeJson2(res, 200, { scheduling: service.getSchedulingStatus() });
6312
+ return writeJson3(res, 200, { scheduling: service.getSchedulingStatus() });
5798
6313
  }
5799
6314
  if (method === "GET") {
5800
6315
  const params = query(req);
@@ -5803,7 +6318,7 @@ async function handleAccountAllowanceApi(req, res, method, rest, service) {
5803
6318
  if (providerId === null) return writeError2(res, 400, "providerId must be claude or codex");
5804
6319
  const accountId = rest.length >= 2 ? rest[1] : params.get("accountId") ?? void 0;
5805
6320
  const allowances = await service.list({ providerId, accountId });
5806
- return writeJson2(res, 200, { allowances });
6321
+ return writeJson3(res, 200, { allowances });
5807
6322
  }
5808
6323
  if (method === "POST" && rest[0] === "refresh") {
5809
6324
  const body = await readJson2(req);
@@ -5818,7 +6333,7 @@ async function handleAccountAllowanceApi(req, res, method, rest, service) {
5818
6333
  if (accountId && allowances.length === 0) {
5819
6334
  return writeError2(res, 404, `Claude account '${accountId}' not found`);
5820
6335
  }
5821
- return writeJson2(res, 200, { allowances });
6336
+ return writeJson3(res, 200, { allowances });
5822
6337
  }
5823
6338
  return writeError2(res, 405, `method ${method} not allowed on account allowances`);
5824
6339
  }
@@ -5837,7 +6352,7 @@ function readBody(req) {
5837
6352
  req.on("error", reject);
5838
6353
  });
5839
6354
  }
5840
- async function readJsonBody3(req) {
6355
+ async function readJsonBody4(req) {
5841
6356
  const raw = await readBody(req);
5842
6357
  if (!raw.trim()) return {};
5843
6358
  try {
@@ -5847,12 +6362,12 @@ async function readJsonBody3(req) {
5847
6362
  return {};
5848
6363
  }
5849
6364
  }
5850
- function writeJson3(res, status, body) {
6365
+ function writeJson4(res, status, body) {
5851
6366
  res.writeHead(status, { "Content-Type": "application/json" });
5852
6367
  res.end(JSON.stringify(body));
5853
6368
  }
5854
6369
  function writeJsonError(res, status, message) {
5855
- writeJson3(res, status, { error: { type: "admin_api_error", message } });
6370
+ writeJson4(res, status, { error: { type: "admin_api_error", message } });
5856
6371
  }
5857
6372
  function maskProviderApiKey(apiKey) {
5858
6373
  if (!apiKey) return "";
@@ -5959,6 +6474,8 @@ async function handleAdminApi(req, res, path2, deps) {
5959
6474
  return await handleServer(req, res, method, deps);
5960
6475
  case "images":
5961
6476
  return await handleImages(res, method, rest, deps);
6477
+ case "search":
6478
+ return await handleSearchAdmin(req, res, method, rest, deps);
5962
6479
  case "accounts":
5963
6480
  return await handleAccounts(req, res, method, rest, deps);
5964
6481
  case "cli":
@@ -5992,7 +6509,7 @@ function requestQuery(req) {
5992
6509
  return new URLSearchParams(qIdx >= 0 ? raw.slice(qIdx + 1) : "");
5993
6510
  }
5994
6511
  function writeResult(res, result) {
5995
- writeJson3(res, result.status, result.body);
6512
+ writeJson4(res, result.status, result.body);
5996
6513
  }
5997
6514
  async function handleUsage(req, res, method, rest, deps) {
5998
6515
  if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on usage`);
@@ -6001,13 +6518,13 @@ async function handleUsage(req, res, method, rest, deps) {
6001
6518
  async function handleDashboardRoute(res, method, deps) {
6002
6519
  if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on dashboard`);
6003
6520
  const result = await handleDashboard(deps);
6004
- return writeJson3(res, result.status, result.body);
6521
+ return writeJson4(res, result.status, result.body);
6005
6522
  }
6006
6523
  async function handlePricing(req, res, method, rest, deps) {
6007
6524
  if (rest.length === 0) {
6008
6525
  if (method === "GET") return writeResult(res, await handlePricingList(deps));
6009
6526
  if (method === "PUT") {
6010
- return writeResult(res, await handlePricingUpsert(await readJsonBody3(req), deps));
6527
+ return writeResult(res, await handlePricingUpsert(await readJsonBody4(req), deps));
6011
6528
  }
6012
6529
  if (method === "DELETE") {
6013
6530
  return writeResult(res, await handlePricingDelete(requestQuery(req), deps));
@@ -6018,7 +6535,7 @@ async function handlePricing(req, res, method, rest, deps) {
6018
6535
  return writeResult(res, await handlePricingFetchLatest(deps));
6019
6536
  }
6020
6537
  if (method === "POST" && rest.length === 1 && rest[0] === "resolve-conflicts") {
6021
- return writeResult(res, await handlePricingResolveConflicts(await readJsonBody3(req), deps));
6538
+ return writeResult(res, await handlePricingResolveConflicts(await readJsonBody4(req), deps));
6022
6539
  }
6023
6540
  return writeJsonError(res, 404, `unknown pricing route '${rest.join("/")}'`);
6024
6541
  }
@@ -6032,15 +6549,15 @@ function migrationDeps(deps) {
6032
6549
  }
6033
6550
  async function handleMigrationExport(req, res, method, deps) {
6034
6551
  if (method !== "POST") return writeJsonError(res, 405, `method ${method} not allowed on export`);
6035
- const body = await readJsonBody3(req);
6552
+ const body = await readJsonBody4(req);
6036
6553
  const result = await handleExport(body, migrationDeps(deps));
6037
- return writeJson3(res, result.status, result.body);
6554
+ return writeJson4(res, result.status, result.body);
6038
6555
  }
6039
6556
  async function handleMigrationImport(req, res, method, deps) {
6040
6557
  if (method !== "POST") return writeJsonError(res, 405, `method ${method} not allowed on import`);
6041
- const body = await readJsonBody3(req);
6558
+ const body = await readJsonBody4(req);
6042
6559
  const result = await handleImport(body, migrationDeps(deps));
6043
- return writeJson3(res, result.status, result.body);
6560
+ return writeJson4(res, result.status, result.body);
6044
6561
  }
6045
6562
  async function handleProviders(req, res, method, rest, deps) {
6046
6563
  const cfg = loadConfig(deps.configPath);
@@ -6071,13 +6588,13 @@ async function handleProviders(req, res, method, rest, deps) {
6071
6588
  if (method === "GET" && rest.length === 2 && rest[1] === "reveal-key") {
6072
6589
  const row = cfg.providers.find((p) => p.id === rest[0]);
6073
6590
  if (!row) return writeJsonError(res, 404, `provider '${rest[0]}' not found`);
6074
- return writeJson3(res, 200, { apiKey: row.apiKey ?? "" });
6591
+ return writeJson4(res, 200, { apiKey: row.apiKey ?? "" });
6075
6592
  }
6076
6593
  if (method === "GET") {
6077
- return writeJson3(res, 200, { providers: cfg.providers.map(toProviderView) });
6594
+ return writeJson4(res, 200, { providers: cfg.providers.map(toProviderView) });
6078
6595
  }
6079
6596
  if (method === "POST") {
6080
- const body = await readJsonBody3(req);
6597
+ const body = await readJsonBody4(req);
6081
6598
  const provider = parseProviderInput(body, void 0);
6082
6599
  if (!provider) return writeJsonError(res, 400, "invalid provider (id, apiFormat, baseUrl required)");
6083
6600
  if (cfg.providers.some((p) => p.id === provider.id)) {
@@ -6085,25 +6602,25 @@ async function handleProviders(req, res, method, rest, deps) {
6085
6602
  }
6086
6603
  cfg.providers.push(provider);
6087
6604
  persistProviders(cfg, deps);
6088
- return writeJson3(res, 201, { provider: toProviderView(provider) });
6605
+ return writeJson4(res, 201, { provider: toProviderView(provider) });
6089
6606
  }
6090
6607
  const id = rest[0];
6091
6608
  if (!id) return writeJsonError(res, 400, "provider id required in path");
6092
6609
  const idx = cfg.providers.findIndex((p) => p.id === id);
6093
6610
  if (idx < 0) return writeJsonError(res, 404, `provider '${id}' not found`);
6094
6611
  if (method === "PUT") {
6095
- const body = await readJsonBody3(req);
6612
+ const body = await readJsonBody4(req);
6096
6613
  const existing = cfg.providers[idx];
6097
6614
  const updated = parseProviderInput(body, existing);
6098
6615
  if (!updated) return writeJsonError(res, 400, "invalid provider (apiFormat, baseUrl required)");
6099
6616
  cfg.providers[idx] = updated;
6100
6617
  persistProviders(cfg, deps);
6101
- return writeJson3(res, 200, { provider: toProviderView(updated) });
6618
+ return writeJson4(res, 200, { provider: toProviderView(updated) });
6102
6619
  }
6103
6620
  if (method === "DELETE") {
6104
6621
  cfg.providers.splice(idx, 1);
6105
6622
  persistProviders(cfg, deps);
6106
- return writeJson3(res, 200, { ok: true });
6623
+ return writeJson4(res, 200, { ok: true });
6107
6624
  }
6108
6625
  return writeJsonError(res, 405, `method ${method} not allowed on providers`);
6109
6626
  }
@@ -6112,7 +6629,7 @@ function persistProviders(cfg, deps) {
6112
6629
  deps.llmConfig.reload(cfg);
6113
6630
  }
6114
6631
  async function handleProviderReorder(req, res, cfg, deps) {
6115
- const body = await readJsonBody3(req);
6632
+ const body = await readJsonBody4(req);
6116
6633
  const rawOrder = body["order"];
6117
6634
  if (!Array.isArray(rawOrder)) {
6118
6635
  return writeJsonError(res, 400, "reorder requires { order: string[] }");
@@ -6136,14 +6653,14 @@ async function handleProviderReorder(req, res, cfg, deps) {
6136
6653
  }
6137
6654
  cfg.providers = reordered;
6138
6655
  persistProviders(cfg, deps);
6139
- return writeJson3(res, 200, { ok: true, providers: cfg.providers.map(toProviderView) });
6656
+ return writeJson4(res, 200, { ok: true, providers: cfg.providers.map(toProviderView) });
6140
6657
  }
6141
6658
  async function handleDiscoverModels(res, id, cfg) {
6142
6659
  if (!id) return writeJsonError(res, 400, "provider id required in path");
6143
6660
  const row = cfg.providers.find((p) => p.id === id);
6144
6661
  if (!row) return writeJsonError(res, 404, `provider '${id}' not found`);
6145
6662
  if (row.apiFormat !== "openai" && row.apiFormat !== "openai-response") {
6146
- return writeJson3(res, 200, { models: [], unsupportedFormat: true });
6663
+ return writeJson4(res, 200, { models: [], unsupportedFormat: true });
6147
6664
  }
6148
6665
  const resolvedKey = resolveEnvKey(row.apiKey);
6149
6666
  const base = row.baseUrl.replace(/\/+$/, "");
@@ -6160,32 +6677,32 @@ async function handleDiscoverModels(res, id, cfg) {
6160
6677
  message = parsed?.error?.message || parsed?.message || message;
6161
6678
  } catch {
6162
6679
  }
6163
- return writeJson3(res, 200, {
6680
+ return writeJson4(res, 200, {
6164
6681
  models: [],
6165
6682
  error: `discovery failed (${response.status})${message ? `: ${message}` : ""}`
6166
6683
  });
6167
6684
  }
6168
6685
  const data = await response.json();
6169
6686
  const models = Array.isArray(data?.data) ? data.data.map((m) => typeof m?.id === "string" ? m.id : "").filter((m) => m.length > 0) : [];
6170
- return writeJson3(res, 200, { models });
6687
+ return writeJson4(res, 200, { models });
6171
6688
  } catch (err5) {
6172
6689
  const message = err5 instanceof Error ? err5.message : String(err5);
6173
- return writeJson3(res, 200, { models: [], error: `discovery failed: ${message}` });
6690
+ return writeJson4(res, 200, { models: [], error: `discovery failed: ${message}` });
6174
6691
  }
6175
6692
  }
6176
6693
  async function handleTestModel(req, res, id, cfg) {
6177
6694
  if (!id) return writeJsonError(res, 400, "provider id required in path");
6178
6695
  const row = cfg.providers.find((p) => p.id === id);
6179
6696
  if (!row) return writeJsonError(res, 404, `provider '${id}' not found`);
6180
- const body = await readJsonBody3(req);
6697
+ const body = await readJsonBody4(req);
6181
6698
  const model = typeof body["model"] === "string" ? body["model"].trim() : "";
6182
6699
  if (!model) return writeJsonError(res, 400, "test requires a { model } string");
6183
6700
  if (row.apiFormat === "gemini") {
6184
- return writeJson3(res, 200, { ok: false, unsupportedFormat: true });
6701
+ return writeJson4(res, 200, { ok: false, unsupportedFormat: true });
6185
6702
  }
6186
6703
  const resolvedKey = resolveEnvKey(row.apiKey);
6187
6704
  if (!resolvedKey) {
6188
- return writeJson3(res, 200, { ok: false, message: "no API key configured for this provider" });
6705
+ return writeJson4(res, 200, { ok: false, message: "no API key configured for this provider" });
6189
6706
  }
6190
6707
  let url = row.baseUrl.replace(/\/+$/, "");
6191
6708
  const prompt = "Reply with the single word: OK.";
@@ -6224,9 +6741,9 @@ async function handleTestModel(req, res, id, cfg) {
6224
6741
  message = parsed?.error?.message || parsed?.message || message;
6225
6742
  } catch {
6226
6743
  }
6227
- return writeJson3(res, 200, { ok: false, status: response.status, latencyMs, message });
6744
+ return writeJson4(res, 200, { ok: false, status: response.status, latencyMs, message });
6228
6745
  }
6229
- return writeJson3(res, 200, {
6746
+ return writeJson4(res, 200, {
6230
6747
  ok: true,
6231
6748
  status: response.status,
6232
6749
  latencyMs,
@@ -6234,7 +6751,7 @@ async function handleTestModel(req, res, id, cfg) {
6234
6751
  });
6235
6752
  } catch (err5) {
6236
6753
  const message = err5 instanceof Error ? err5.message : String(err5);
6237
- return writeJson3(res, 200, { ok: false, latencyMs: Date.now() - startedAt, message });
6754
+ return writeJson4(res, 200, { ok: false, latencyMs: Date.now() - startedAt, message });
6238
6755
  }
6239
6756
  }
6240
6757
  function extractSampleText(text, apiFormat) {
@@ -6275,7 +6792,7 @@ async function handleProviderKeys(res, id, cfg, deps) {
6275
6792
  const row = cfg.providers.find((p) => p.id === id);
6276
6793
  if (!row) return writeJsonError(res, 404, `provider '${id}' not found`);
6277
6794
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
6278
- return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
6795
+ return writeJson4(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
6279
6796
  }
6280
6797
  function parsePoolKeyInput(body, existing) {
6281
6798
  const out = {};
@@ -6294,7 +6811,7 @@ async function handleAddProviderKey(req, res, id, cfg, deps) {
6294
6811
  if (!id) return writeJsonError(res, 400, "provider id required in path");
6295
6812
  const idx = cfg.providers.findIndex((p) => p.id === id);
6296
6813
  if (idx < 0) return writeJsonError(res, 404, `provider '${id}' not found`);
6297
- const body = await readJsonBody3(req);
6814
+ const body = await readJsonBody4(req);
6298
6815
  const parsed = parsePoolKeyInput(body);
6299
6816
  if (!parsed.apiKey) return writeJsonError(res, 400, "add requires a non-empty apiKey");
6300
6817
  const keyId = `key-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
@@ -6306,7 +6823,7 @@ async function handleAddProviderKey(req, res, id, cfg, deps) {
6306
6823
  row.apiKeys = [...row.apiKeys ?? [], entry];
6307
6824
  persistProviders(cfg, deps);
6308
6825
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
6309
- return writeJson3(res, 201, { keys: toPoolKeyView(row, cooldown, deps) });
6826
+ return writeJson4(res, 201, { keys: toPoolKeyView(row, cooldown, deps) });
6310
6827
  }
6311
6828
  async function handleUpdateProviderKey(req, res, id, keyId, cfg, deps) {
6312
6829
  if (!id) return writeJsonError(res, 400, "provider id required in path");
@@ -6316,7 +6833,7 @@ async function handleUpdateProviderKey(req, res, id, keyId, cfg, deps) {
6316
6833
  const row = cfg.providers[idx];
6317
6834
  const keyIdx = (row.apiKeys ?? []).findIndex((k) => k.id === keyId);
6318
6835
  if (keyIdx < 0) return writeJsonError(res, 404, `pool key '${keyId}' not found`);
6319
- const body = await readJsonBody3(req);
6836
+ const body = await readJsonBody4(req);
6320
6837
  const existing = row.apiKeys[keyIdx];
6321
6838
  const parsed = parsePoolKeyInput(body, existing);
6322
6839
  const entry = { id: keyId, apiKey: parsed.apiKey || existing.apiKey };
@@ -6326,7 +6843,7 @@ async function handleUpdateProviderKey(req, res, id, keyId, cfg, deps) {
6326
6843
  row.apiKeys[keyIdx] = entry;
6327
6844
  persistProviders(cfg, deps);
6328
6845
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
6329
- return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
6846
+ return writeJson4(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
6330
6847
  }
6331
6848
  async function handleDeleteProviderKey(res, id, keyId, cfg, deps) {
6332
6849
  if (!id) return writeJsonError(res, 400, "provider id required in path");
@@ -6340,7 +6857,7 @@ async function handleDeleteProviderKey(res, id, keyId, cfg, deps) {
6340
6857
  if (row.apiKeys.length === 0) row.apiKeys = void 0;
6341
6858
  persistProviders(cfg, deps);
6342
6859
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
6343
- return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
6860
+ return writeJson4(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
6344
6861
  }
6345
6862
  async function handleToggleProviderKey(req, res, id, keyId, cfg, deps) {
6346
6863
  if (!id) return writeJsonError(res, 400, "provider id required in path");
@@ -6350,11 +6867,11 @@ async function handleToggleProviderKey(req, res, id, keyId, cfg, deps) {
6350
6867
  const row = cfg.providers[idx];
6351
6868
  const keyIdx = (row.apiKeys ?? []).findIndex((k) => k.id === keyId);
6352
6869
  if (keyIdx < 0) return writeJsonError(res, 404, `pool key '${keyId}' not found`);
6353
- const body = await readJsonBody3(req);
6870
+ const body = await readJsonBody4(req);
6354
6871
  row.apiKeys[keyIdx].enabled = Boolean(body["enabled"]);
6355
6872
  persistProviders(cfg, deps);
6356
6873
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
6357
- return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
6874
+ return writeJson4(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
6358
6875
  }
6359
6876
  function parseApiKeysInput(raw, existing) {
6360
6877
  if (!Array.isArray(raw)) return existing;
@@ -6540,13 +7057,13 @@ function handlePresets(res, method) {
6540
7057
  website: p.website,
6541
7058
  modelsEndpoint: p.modelsEndpoint
6542
7059
  }));
6543
- return writeJson3(res, 200, { presets, excluded });
7060
+ return writeJson4(res, 200, { presets, excluded });
6544
7061
  }
6545
7062
  async function handleKeys(req, res, method, rest, deps) {
6546
7063
  if (method === "GET" && rest.length === 0) {
6547
7064
  const rows = await deps.keyDb.outboundApiKeysList();
6548
7065
  const reader = deps.keySpendReader;
6549
- if (!reader) return writeJson3(res, 200, { keys: rows.map(toKeyInfo) });
7066
+ if (!reader) return writeJson4(res, 200, { keys: rows.map(toKeyInfo) });
6550
7067
  const now = Date.now();
6551
7068
  const keys = await Promise.all(
6552
7069
  rows.map(async (row) => {
@@ -6558,13 +7075,13 @@ async function handleKeys(req, res, method, rest, deps) {
6558
7075
  return info;
6559
7076
  })
6560
7077
  );
6561
- return writeJson3(res, 200, { keys });
7078
+ return writeJson4(res, 200, { keys });
6562
7079
  }
6563
7080
  if (method === "POST" && rest.length === 0) {
6564
- const body = await readJsonBody3(req);
7081
+ const body = await readJsonBody4(req);
6565
7082
  const name = typeof body["name"] === "string" && body["name"].trim() ? body["name"].trim() : "key";
6566
7083
  const created = await createNamedKey(deps.keyDb, name);
6567
- return writeJson3(res, 201, {
7084
+ return writeJson4(res, 201, {
6568
7085
  id: created.id,
6569
7086
  name: created.name,
6570
7087
  keyPrefix: created.keyPrefix,
@@ -6575,7 +7092,7 @@ async function handleKeys(req, res, method, rest, deps) {
6575
7092
  }
6576
7093
  if (method === "GET" && rest.length === 2 && rest[1] === "reveal") {
6577
7094
  const revealed = await deps.keyDb.outboundApiKeysReveal(rest[0]);
6578
- if (revealed !== null) return writeJson3(res, 200, { key: revealed });
7095
+ if (revealed !== null) return writeJson4(res, 200, { key: revealed });
6579
7096
  const exists = (await deps.keyDb.outboundApiKeysList()).some((r) => r.id === rest[0]);
6580
7097
  if (!exists) return writeJsonError(res, 404, `key '${rest[0]}' not found`);
6581
7098
  return writeJsonError(
@@ -6590,26 +7107,26 @@ async function handleKeys(req, res, method, rest, deps) {
6590
7107
  const bound = await integrationKeyRequirement(deps, id);
6591
7108
  if (bound) return writeJsonError(res, 409, bound);
6592
7109
  const ok = await deps.keyDb.outboundApiKeysRevoke(id);
6593
- return writeJson3(res, ok ? 200 : 404, { ok });
7110
+ return writeJson4(res, ok ? 200 : 404, { ok });
6594
7111
  }
6595
7112
  if (method === "DELETE" && id && !action) {
6596
7113
  const bound = await integrationKeyRequirement(deps, id);
6597
7114
  if (bound) return writeJsonError(res, 409, bound);
6598
7115
  const ok = await deps.keyDb.outboundApiKeysDelete(id);
6599
- return writeJson3(res, ok ? 200 : 404, { ok });
7116
+ return writeJson4(res, ok ? 200 : 404, { ok });
6600
7117
  }
6601
7118
  if (method === "POST" && id && action === "enabled") {
6602
- const body = await readJsonBody3(req);
7119
+ const body = await readJsonBody4(req);
6603
7120
  const enabled = body["enabled"] === true;
6604
7121
  if (!enabled) {
6605
7122
  const bound = await integrationKeyRequirement(deps, id);
6606
7123
  if (bound) return writeJsonError(res, 409, bound);
6607
7124
  }
6608
7125
  const ok = await deps.keyDb.outboundApiKeysSetEnabled(id, enabled);
6609
- return writeJson3(res, ok ? 200 : 404, { ok, enabled });
7126
+ return writeJson4(res, ok ? 200 : 404, { ok, enabled });
6610
7127
  }
6611
7128
  if (method === "POST" && id && action === "permissions") {
6612
- const body = await readJsonBody3(req);
7129
+ const body = await readJsonBody4(req);
6613
7130
  if (Object.keys(body).length !== 1 || !Object.prototype.hasOwnProperty.call(body, "permissions")) {
6614
7131
  return writeJsonError(res, 400, "body must contain only permissions");
6615
7132
  }
@@ -6624,19 +7141,19 @@ async function handleKeys(req, res, method, rest, deps) {
6624
7141
  );
6625
7142
  }
6626
7143
  const before = (await deps.keyDb.outboundApiKeysList()).find((row) => row.id === id);
6627
- if (!before) return writeJson3(res, 404, { ok: false });
6628
- if (before.revokedAt !== null) return writeJson3(res, 409, { ok: false });
7144
+ if (!before) return writeJson4(res, 404, { ok: false });
7145
+ if (before.revokedAt !== null) return writeJson4(res, 409, { ok: false });
6629
7146
  const required = await integrationKeyRequirement(deps, id, permissions);
6630
7147
  if (required) return writeJsonError(res, 409, required);
6631
7148
  const ok = await deps.keyDb.outboundApiKeysSetPermissions(id, permissions);
6632
7149
  if (!ok) {
6633
7150
  const current = (await deps.keyDb.outboundApiKeysList()).find((row) => row.id === id);
6634
- return writeJson3(res, current?.revokedAt !== null ? 409 : 404, { ok: false });
7151
+ return writeJson4(res, current?.revokedAt !== null ? 409 : 404, { ok: false });
6635
7152
  }
6636
- return writeJson3(res, 200, { ok: true, allowedEndpoints: permissions });
7153
+ return writeJson4(res, 200, { ok: true, allowedEndpoints: permissions });
6637
7154
  }
6638
7155
  if (method === "POST" && id && action === "max-concurrency") {
6639
- const body = await readJsonBody3(req);
7156
+ const body = await readJsonBody4(req);
6640
7157
  const raw = body["maxConcurrency"];
6641
7158
  let value;
6642
7159
  if (raw === null) {
@@ -6651,14 +7168,14 @@ async function handleKeys(req, res, method, rest, deps) {
6651
7168
  );
6652
7169
  }
6653
7170
  const ok = await deps.keyDb.outboundApiKeysSetMaxConcurrency(id, value);
6654
- return writeJson3(res, ok ? 200 : 404, { ok, maxConcurrency: value });
7171
+ return writeJson4(res, ok ? 200 : 404, { ok, maxConcurrency: value });
6655
7172
  }
6656
7173
  if (method === "POST" && id && action === "policy") {
6657
- const body = await readJsonBody3(req);
7174
+ const body = await readJsonBody4(req);
6658
7175
  const parsed = parseKeyPolicyBody(body);
6659
7176
  if (!parsed.ok) return writeJsonError(res, 400, parsed.message);
6660
7177
  const ok = await deps.keyDb.outboundApiKeysSetPolicy(id, parsed.policy);
6661
- return writeJson3(res, ok ? 200 : 404, { ok });
7178
+ return writeJson4(res, ok ? 200 : 404, { ok });
6662
7179
  }
6663
7180
  return writeJsonError(res, 405, `method ${method} not allowed on keys`);
6664
7181
  }
@@ -6749,7 +7266,8 @@ function outboundServerConfigInput(config) {
6749
7266
  userMessageQueue: config.userMessageQueue,
6750
7267
  concurrencyQueue: config.concurrencyQueue,
6751
7268
  voucher: config.voucher,
6752
- anthropic: config.anthropic
7269
+ anthropic: config.anthropic,
7270
+ search: config.search
6753
7271
  };
6754
7272
  }
6755
7273
  function projectImagesConfigForAdmin(config) {
@@ -6812,15 +7330,21 @@ function currentImageGenerationId(deps) {
6812
7330
  }
6813
7331
  async function handleServer(req, res, method, deps) {
6814
7332
  if (method === "GET") {
6815
- const config = await loadServerConfig2(deps.settingsStore);
7333
+ const config = await loadServerConfig3(deps.settingsStore);
6816
7334
  let server = config;
6817
7335
  if (config.proxy) server = { ...server, proxy: redactOutboundProxy(config.proxy) };
6818
7336
  if (config.webhook) server = { ...server, webhook: redactWebhookConfig(config.webhook) };
6819
7337
  if (config.billing) server = { ...server, billing: redactBillingConfig(config.billing) };
6820
- return writeJson3(res, 200, { server: projectImagesConfigForAdmin(server) });
7338
+ if (config.search) {
7339
+ server = {
7340
+ ...server,
7341
+ search: redactSearchServerConfig(config.search)
7342
+ };
7343
+ }
7344
+ return writeJson4(res, 200, { server: projectImagesConfigForAdmin(server) });
6821
7345
  }
6822
7346
  if (method === "PUT") {
6823
- const patch = await readJsonBody3(req);
7347
+ const patch = await readJsonBody4(req);
6824
7348
  const queueErrors = validateQueueSegments(patch);
6825
7349
  if (queueErrors.length > 0) {
6826
7350
  return writeJsonError(res, 400, `invalid queue config: ${queueErrors.join("; ")}`);
@@ -6849,7 +7373,7 @@ async function handleServer(req, res, method, deps) {
6849
7373
  if (billingErrors.length > 0) {
6850
7374
  return writeJsonError(res, 400, `invalid billing config: ${billingErrors.join("; ")}`);
6851
7375
  }
6852
- const current = await loadServerConfig2(deps.settingsStore);
7376
+ const current = await loadServerConfig3(deps.settingsStore);
6853
7377
  let effectivePatch = patch;
6854
7378
  if (patch.proxy) {
6855
7379
  effectivePatch = { ...effectivePatch, proxy: preserveOutboundProxySecrets(patch.proxy, current.proxy) };
@@ -6870,6 +7394,20 @@ async function handleServer(req, res, method, deps) {
6870
7394
  }
6871
7395
  effectivePatch = { ...effectivePatch, images };
6872
7396
  }
7397
+ if (patch.search !== void 0) {
7398
+ const searchPatch = preserveSearchSecrets(
7399
+ patch.search,
7400
+ current.search ?? DEFAULT_SEARCH_SERVER_CONFIG2
7401
+ );
7402
+ const searchErrors = validateSearchServerConfig(searchPatch);
7403
+ if (searchErrors.length > 0) {
7404
+ return writeJsonError(res, 400, `invalid search config: ${searchErrors.join("; ")}`);
7405
+ }
7406
+ effectivePatch = {
7407
+ ...effectivePatch,
7408
+ search: searchPatch
7409
+ };
7410
+ }
6873
7411
  const merged = mergeServerConfig(current, effectivePatch);
6874
7412
  const priorImageGenerationId = currentImageGenerationId(deps);
6875
7413
  try {
@@ -6922,7 +7460,11 @@ async function handleServer(req, res, method, deps) {
6922
7460
  } catch {
6923
7461
  }
6924
7462
  }
6925
- return writeJson3(res, 200, { server: projectImagesConfigForAdmin(merged) });
7463
+ const mergedForAdmin = merged.search ? {
7464
+ ...merged,
7465
+ search: redactSearchServerConfig(merged.search)
7466
+ } : merged;
7467
+ return writeJson4(res, 200, { server: projectImagesConfigForAdmin(mergedForAdmin) });
6926
7468
  }
6927
7469
  return writeJsonError(res, 405, `method ${method} not allowed on server`);
6928
7470
  }
@@ -6939,7 +7481,7 @@ async function handleAccounts(req, res, method, rest, deps) {
6939
7481
  sessionKey: query2.get("sessionKey") ?? void 0,
6940
7482
  limit: Number.isFinite(parsedLimit) ? parsedLimit : 100
6941
7483
  });
6942
- return writeJson3(res, 200, {
7484
+ return writeJson4(res, 200, {
6943
7485
  available: true,
6944
7486
  records,
6945
7487
  capacity: ACCOUNT_ROUTE_ACTIVITY_LIMIT,
@@ -6955,7 +7497,7 @@ async function handleAccounts(req, res, method, rest, deps) {
6955
7497
  providerId: query2.get("providerId") ?? void 0,
6956
7498
  accountId: query2.get("accountId") ?? void 0
6957
7499
  });
6958
- return writeJson3(res, 200, {
7500
+ return writeJson4(res, 200, {
6959
7501
  available: true,
6960
7502
  entries,
6961
7503
  collectedAt: Date.now()
@@ -6974,10 +7516,10 @@ async function handleAccounts(req, res, method, rest, deps) {
6974
7516
  const accounts = await deps.subscriptionAccounts.listAll();
6975
7517
  const providerAccounts = await deps.subscriptionTokenWriter.listSanitizedAccounts();
6976
7518
  const externalCli = await deps.subscriptionTokenWriter.listExternalCliAvailability();
6977
- return writeJson3(res, 200, { accounts, providerAccounts, externalCli });
7519
+ return writeJson4(res, 200, { accounts, providerAccounts, externalCli });
6978
7520
  }
6979
7521
  if (method === "POST" && rest[0] === "batch" && rest.length === 1) {
6980
- const body = await readJsonBody3(req);
7522
+ const body = await readJsonBody4(req);
6981
7523
  const parsed = validateAccountBatchBody(body);
6982
7524
  if (!parsed) return writeJsonError(res, 400, "invalid account batch request");
6983
7525
  const result = await deps.subscriptionTokenWriter.batchManageAccounts(parsed.refs, parsed.mutation);
@@ -6993,15 +7535,15 @@ async function handleAccounts(req, res, method, rest, deps) {
6993
7535
  deps.accountAllowanceService?.removeAccountSnapshot?.(ref.providerId, ref.accountId);
6994
7536
  }
6995
7537
  }
6996
- return writeJson3(res, 200, { ok: true, affected: result.affected });
7538
+ return writeJson4(res, 200, { ok: true, affected: result.affected });
6997
7539
  }
6998
7540
  if (method === "GET" && rest[0] === "codex" && rest[1] === "oauth" && rest[3] === "status") {
6999
7541
  const result = handleCodexOAuthStatus(rest[2], deps);
7000
- return writeJson3(res, result.status, result.body);
7542
+ return writeJson4(res, result.status, result.body);
7001
7543
  }
7002
7544
  if (method === "DELETE" && rest[0] === "codex" && rest[1] === "oauth" && rest[2]) {
7003
7545
  const result = handleCodexOAuthCancel(rest[2], deps);
7004
- return writeJson3(res, result.status, result.body);
7546
+ return writeJson4(res, result.status, result.body);
7005
7547
  }
7006
7548
  if (method === "GET" && rest.length === 3 && rest[2] === "diagnostics") {
7007
7549
  const providerId = asSubscriptionProviderId(rest[0]);
@@ -7023,7 +7565,7 @@ async function handleAccounts(req, res, method, rest, deps) {
7023
7565
  resumeAt: entry.resumeAt
7024
7566
  })) ?? [];
7025
7567
  const diagnostics = [...health2, ...allowance].sort((left, right) => right.at - left.at).slice(0, 200);
7026
- return writeJson3(res, 200, { diagnostics });
7568
+ return writeJson4(res, 200, { diagnostics });
7027
7569
  }
7028
7570
  if (method === "GET" && rest.length === 3 && rest[2] === "events") {
7029
7571
  const providerId = asSubscriptionProviderId(rest[0]);
@@ -7035,17 +7577,17 @@ async function handleAccounts(req, res, method, rest, deps) {
7035
7577
  }
7036
7578
  const snapshot = deps.accountProbeService?.getAllHistory().find((entry) => entry.providerId === providerId && entry.accountId === accountId);
7037
7579
  const diagnostics = getSharedAccountHealth().getDiagnostics({ providerId, accountId });
7038
- return writeJson3(res, 200, { events: snapshot?.records ?? [], diagnostics });
7580
+ return writeJson4(res, 200, { events: snapshot?.records ?? [], diagnostics });
7039
7581
  }
7040
7582
  if (method === "PATCH" && rest.length === 2) {
7041
7583
  const providerId = asSubscriptionProviderId(rest[0]);
7042
7584
  if (!providerId) return writeJsonError(res, 400, `unknown subscription provider '${rest[0] ?? ""}'`);
7043
- const body = await readJsonBody3(req);
7585
+ const body = await readJsonBody4(req);
7044
7586
  const patch = validateAccountMetadataPatch(body);
7045
7587
  if (!patch) return writeJsonError(res, 400, "invalid account metadata patch");
7046
7588
  const result = await deps.subscriptionTokenWriter.patchAccountMetadata(providerId, rest[1], patch);
7047
7589
  if (!result.ok) return writeJsonError(res, 404, `account '${rest[1]}' not found`);
7048
- return writeJson3(res, 200, { ok: true });
7590
+ return writeJson4(res, 200, { ok: true });
7049
7591
  }
7050
7592
  if (method === "PUT" || method === "POST" || method === "DELETE") {
7051
7593
  const providerId = asSubscriptionProviderId(rest[0]);
@@ -7054,15 +7596,15 @@ async function handleAccounts(req, res, method, rest, deps) {
7054
7596
  }
7055
7597
  if (method === "POST" && rest[1] === "oauth" && rest[2] === "start") {
7056
7598
  const result = providerId === "codex" ? handleCodexOAuthStart(deps) : handleOAuthStart(providerId, deps);
7057
- return writeJson3(res, result.status, result.body);
7599
+ return writeJson4(res, result.status, result.body);
7058
7600
  }
7059
7601
  if (method === "POST" && rest[1] === "oauth" && rest[2] === "complete") {
7060
- const body2 = await readJsonBody3(req);
7602
+ const body2 = await readJsonBody4(req);
7061
7603
  const result = await handleOAuthComplete(providerId, body2, deps);
7062
- return writeJson3(res, result.status, result.body);
7604
+ return writeJson4(res, result.status, result.body);
7063
7605
  }
7064
7606
  if (method === "POST" && rest[1] === "accounts") {
7065
- const body2 = await readJsonBody3(req);
7607
+ const body2 = await readJsonBody4(req);
7066
7608
  const block = validateTokenBody(providerId, body2);
7067
7609
  if (!block) {
7068
7610
  return writeJsonError(res, 400, `malformed token body for provider '${providerId}'`);
@@ -7070,20 +7612,20 @@ async function handleAccounts(req, res, method, rest, deps) {
7070
7612
  const label = typeof body2["label"] === "string" && body2["label"].trim() ? body2["label"].trim() : void 0;
7071
7613
  await deps.subscriptionAccountAppender.appendProviderAccount(providerId, block, label);
7072
7614
  const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
7073
- return writeJson3(res, 200, status2 ? { account: status2 } : { ok: true });
7615
+ return writeJson4(res, 200, status2 ? { account: status2 } : { ok: true });
7074
7616
  }
7075
7617
  if (method === "POST" && rest[1] === "import-external") {
7076
7618
  if (providerId !== "claude" && providerId !== "codex") {
7077
7619
  return writeJsonError(res, 400, `provider '${providerId}' has no external CLI store`);
7078
7620
  }
7079
- const body2 = await readJsonBody3(req);
7621
+ const body2 = await readJsonBody4(req);
7080
7622
  const label = typeof body2["label"] === "string" && body2["label"].trim() ? body2["label"].trim() : void 0;
7081
7623
  const result = await deps.subscriptionTokenWriter.importExternalCliAccount(providerId, label);
7082
7624
  if (!result.ok) {
7083
7625
  return writeJsonError(res, 409, `no usable external ${providerId} CLI credential found`);
7084
7626
  }
7085
7627
  const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
7086
- return writeJson3(res, 200, {
7628
+ return writeJson4(res, 200, {
7087
7629
  ok: true,
7088
7630
  account: status2 ?? void 0,
7089
7631
  nativeCredentialMode: result.nativeCredentialMode,
@@ -7098,7 +7640,7 @@ async function handleAccounts(req, res, method, rest, deps) {
7098
7640
  const writer2 = deps.subscriptionTokenWriter;
7099
7641
  const ok = providerId === "claude" ? await writer2.refreshClaudeToken() : providerId === "codex" ? await writer2.refreshCodexToken() : await writer2.refreshGeminiToken();
7100
7642
  const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
7101
- return writeJson3(res, 200, { ok, account: status2 ?? void 0 });
7643
+ return writeJson4(res, 200, { ok, account: status2 ?? void 0 });
7102
7644
  }
7103
7645
  if (method === "POST" && rest.length === 3 && rest[2] === "test") {
7104
7646
  const accountId = rest[1];
@@ -7108,7 +7650,7 @@ async function handleAccounts(req, res, method, rest, deps) {
7108
7650
  return writeJsonError(res, 404, `account '${accountId}' not found`);
7109
7651
  }
7110
7652
  const result = await deps.accountProbeService.testAccountConnection(providerId, accountId);
7111
- return writeJson3(res, 200, {
7653
+ return writeJson4(res, 200, {
7112
7654
  ok: result.ok,
7113
7655
  marked: result.marked,
7114
7656
  tier: result.tier,
@@ -7117,15 +7659,15 @@ async function handleAccounts(req, res, method, rest, deps) {
7117
7659
  }
7118
7660
  if (method === "POST" && rest[2] === "label") {
7119
7661
  const accountId = rest[1];
7120
- const body2 = await readJsonBody3(req);
7662
+ const body2 = await readJsonBody4(req);
7121
7663
  const label = typeof body2["label"] === "string" ? body2["label"].trim() : "";
7122
7664
  const result = await deps.subscriptionTokenWriter.renameAccount(providerId, accountId, label);
7123
7665
  if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
7124
- return writeJson3(res, 200, { ok: true });
7666
+ return writeJson4(res, 200, { ok: true });
7125
7667
  }
7126
7668
  if (method === "POST" && rest[2] === "priority") {
7127
7669
  const accountId = rest[1];
7128
- const body2 = await readJsonBody3(req);
7670
+ const body2 = await readJsonBody4(req);
7129
7671
  const raw = body2["priority"];
7130
7672
  const priority = typeof raw === "number" ? raw : Number(raw);
7131
7673
  if (!Number.isFinite(priority)) {
@@ -7133,11 +7675,11 @@ async function handleAccounts(req, res, method, rest, deps) {
7133
7675
  }
7134
7676
  const result = await deps.subscriptionTokenWriter.setAccountPriority(providerId, accountId, priority);
7135
7677
  if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
7136
- return writeJson3(res, 200, { ok: true });
7678
+ return writeJson4(res, 200, { ok: true });
7137
7679
  }
7138
7680
  if (method === "POST" && rest[2] === "proxy") {
7139
7681
  const accountId = rest[1];
7140
- const body2 = await readJsonBody3(req);
7682
+ const body2 = await readJsonBody4(req);
7141
7683
  const rawProxy = body2["proxy"];
7142
7684
  let proxy;
7143
7685
  if (rawProxy !== null && rawProxy !== void 0) {
@@ -7146,63 +7688,63 @@ async function handleAccounts(req, res, method, rest, deps) {
7146
7688
  }
7147
7689
  const result = await deps.subscriptionTokenWriter.setAccountProxy(providerId, accountId, proxy);
7148
7690
  if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
7149
- return writeJson3(res, 200, { ok: true });
7691
+ return writeJson4(res, 200, { ok: true });
7150
7692
  }
7151
7693
  if (method === "POST" && rest[2] === "supported-models") {
7152
7694
  const accountId = rest[1];
7153
- const body2 = await readJsonBody3(req);
7695
+ const body2 = await readJsonBody4(req);
7154
7696
  const parsed = validateSupportedModelsBody(body2["supportedModels"]);
7155
7697
  if (!parsed.ok) return writeJsonError(res, 400, "invalid supportedModels");
7156
7698
  const result = await deps.subscriptionTokenWriter.setAccountSupportedModels(providerId, accountId, parsed.value);
7157
7699
  if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
7158
- return writeJson3(res, 200, { ok: true });
7700
+ return writeJson4(res, 200, { ok: true });
7159
7701
  }
7160
7702
  if (method === "PUT" && rest[1] === "active") {
7161
- const body2 = await readJsonBody3(req);
7703
+ const body2 = await readJsonBody4(req);
7162
7704
  const id = typeof body2["id"] === "string" ? body2["id"] : "";
7163
7705
  if (!id) return writeJsonError(res, 400, "active switch requires { id }");
7164
7706
  const result = await deps.subscriptionTokenWriter.setActiveAccount(providerId, id);
7165
7707
  if (!result.ok) return writeJsonError(res, 404, `account '${id}' not found`);
7166
- return writeJson3(res, 200, { ok: true });
7708
+ return writeJson4(res, 200, { ok: true });
7167
7709
  }
7168
7710
  if (method === "DELETE" && rest.length === 2) {
7169
7711
  const accountId = rest[1];
7170
7712
  const result = await deps.subscriptionTokenWriter.removeAccount(providerId, accountId);
7171
7713
  if (!result.removed) return writeJsonError(res, 404, `account '${accountId}' not found`);
7172
7714
  deps.accountAllowanceService?.removeAccountSnapshot?.(providerId, accountId);
7173
- return writeJson3(res, 200, { ok: true });
7715
+ return writeJson4(res, 200, { ok: true });
7174
7716
  }
7175
7717
  if (method === "DELETE" && rest.length === 1) {
7176
7718
  await deps.subscriptionTokenWriter.clearProvider(providerId);
7177
7719
  deps.accountAllowanceService?.removeProviderSnapshots?.(providerId);
7178
- return writeJson3(res, 200, { ok: true });
7720
+ return writeJson4(res, 200, { ok: true });
7179
7721
  }
7180
7722
  if (method === "DELETE") {
7181
7723
  return writeJsonError(res, 405, "method DELETE not allowed on this accounts path");
7182
7724
  }
7183
- const body = await readJsonBody3(req);
7725
+ const body = await readJsonBody4(req);
7184
7726
  const config = validateTokenBody(providerId, body);
7185
7727
  if (!config) {
7186
7728
  return writeJsonError(res, 400, `malformed token body for provider '${providerId}'`);
7187
7729
  }
7188
7730
  await deps.subscriptionTokenWriter.writeProviderTokens(providerId, config);
7189
7731
  const status = await statusEntryFor(deps.subscriptionAccounts, providerId);
7190
- return writeJson3(res, 200, status ? { account: status } : { ok: true });
7732
+ return writeJson4(res, 200, status ? { account: status } : { ok: true });
7191
7733
  }
7192
7734
  return writeJsonError(res, 405, `method ${method} not allowed on accounts`);
7193
7735
  }
7194
7736
  async function handleCli(req, res, method, rest, deps) {
7195
7737
  if (method === "GET" && rest.length === 0) {
7196
7738
  const result = handleCliList(process.platform, deps.cliPathProbe);
7197
- return writeJson3(res, result.status, result.body);
7739
+ return writeJson4(res, result.status, result.body);
7198
7740
  }
7199
7741
  if (method === "GET" && rest[0] === "sessions") {
7200
7742
  const result = handleCliSessions();
7201
- return writeJson3(res, result.status, result.body);
7743
+ return writeJson4(res, result.status, result.body);
7202
7744
  }
7203
7745
  if (method === "DELETE" && rest[0] === "sessions" && rest[1]) {
7204
7746
  const result = handleCliStop(rest[1]);
7205
- return writeJson3(res, result.status, result.body);
7747
+ return writeJson4(res, result.status, result.body);
7206
7748
  }
7207
7749
  if (method === "POST" && rest[1] === "install") {
7208
7750
  const cli = rest[0];
@@ -7210,14 +7752,14 @@ async function handleCli(req, res, method, rest, deps) {
7210
7752
  return writeJsonError(res, 400, `unknown cli '${cli ?? ""}'`);
7211
7753
  }
7212
7754
  const result = await handleCliInstall(cli, deps.cliCommandRunner);
7213
- return writeJson3(res, result.status, result.body);
7755
+ return writeJson4(res, result.status, result.body);
7214
7756
  }
7215
7757
  if (method === "POST" && rest[1] === "launch") {
7216
7758
  const cli = rest[0];
7217
7759
  if (!isLaunchCliId(cli)) {
7218
7760
  return writeJsonError(res, 400, `unknown cli '${cli ?? ""}'`);
7219
7761
  }
7220
- const body = await readJsonBody3(req);
7762
+ const body = await readJsonBody4(req);
7221
7763
  const providers = loadConfig(deps.configPath).providers ?? [];
7222
7764
  const result = await handleCliLaunch(cli, body, {
7223
7765
  llmConfig: deps.llmConfig,
@@ -7226,7 +7768,7 @@ async function handleCli(req, res, method, rest, deps) {
7226
7768
  opener: deps.cliTerminalOpener,
7227
7769
  probe: deps.cliPathProbe
7228
7770
  });
7229
- return writeJson3(res, result.status, result.body);
7771
+ return writeJson4(res, result.status, result.body);
7230
7772
  }
7231
7773
  return writeJsonError(res, 405, `method ${method} not allowed on cli`);
7232
7774
  }
@@ -7236,52 +7778,52 @@ async function handleIntegrations(req, res, method, rest, deps) {
7236
7778
  const manager = factory();
7237
7779
  try {
7238
7780
  if (method === "GET" && rest.length === 0) {
7239
- return writeJson3(res, 200, {
7781
+ return writeJson4(res, 200, {
7240
7782
  integrations: await manager.listStatus(),
7241
7783
  gateway: deps.outboundApiServer.getStatus()
7242
7784
  });
7243
7785
  }
7244
7786
  if (method === "POST" && rest.length === 1 && rest[0] === "rotate") {
7245
7787
  await manager.rotateGatewayKey();
7246
- return writeJson3(res, 200, { ok: true, integrations: await manager.listStatus() });
7788
+ return writeJson4(res, 200, { ok: true, integrations: await manager.listStatus() });
7247
7789
  }
7248
7790
  const client = rest[0];
7249
7791
  if (!isIntegrationClient(client)) {
7250
7792
  return writeJsonError(res, 400, `unknown integration client '${client ?? ""}'`);
7251
7793
  }
7252
7794
  if (method === "POST" && rest[1] === "key") {
7253
- const body = await readJsonBody3(req);
7795
+ const body = await readJsonBody4(req);
7254
7796
  if (Object.keys(body).length !== 1 || typeof body.keyId !== "string" || !body.keyId.trim()) {
7255
7797
  return writeJsonError(res, 400, "body must contain a non-empty keyId string");
7256
7798
  }
7257
7799
  const status = await manager.bindIntegrationKey(client, body.keyId.trim());
7258
- return writeJson3(res, 200, { integration: status });
7800
+ return writeJson4(res, 200, { integration: status });
7259
7801
  }
7260
7802
  if (method === "POST" && rest[1] === "plan") {
7261
- const body = await readJsonBody3(req);
7803
+ const body = await readJsonBody4(req);
7262
7804
  const configPath = body.configPath;
7263
7805
  if (configPath !== void 0 && typeof configPath !== "string") {
7264
7806
  return writeJsonError(res, 400, "configPath must be a string");
7265
7807
  }
7266
7808
  const plan = await manager.plan(client, configPath);
7267
- return writeJson3(res, 200, { plan });
7809
+ return writeJson4(res, 200, { plan });
7268
7810
  }
7269
7811
  if (method === "POST" && (rest[1] === "install" || rest[1] === "apply")) {
7270
- const body = await readJsonBody3(req);
7812
+ const body = await readJsonBody4(req);
7271
7813
  const configPath = body.configPath;
7272
7814
  if (configPath !== void 0 && typeof configPath !== "string") {
7273
7815
  return writeJsonError(res, 400, "configPath must be a string");
7274
7816
  }
7275
7817
  const status = await manager.install(client, configPath);
7276
- return writeJson3(res, 200, { integration: status });
7818
+ return writeJson4(res, 200, { integration: status });
7277
7819
  }
7278
7820
  if (method === "POST" && rest[1] === "repair") {
7279
7821
  const status = await manager.repair(client);
7280
- return writeJson3(res, 200, { integration: status });
7822
+ return writeJson4(res, 200, { integration: status });
7281
7823
  }
7282
7824
  if (method === "DELETE" && rest.length === 1 || method === "POST" && rest[1] === "remove") {
7283
7825
  const status = await manager.remove(client);
7284
- return writeJson3(res, 200, { integration: status });
7826
+ return writeJson4(res, 200, { integration: status });
7285
7827
  }
7286
7828
  return writeJsonError(res, 405, `method ${method} not allowed on integrations`);
7287
7829
  } catch (error) {
@@ -7424,7 +7966,7 @@ async function handleImages(res, method, rest, deps) {
7424
7966
  }
7425
7967
  const reader = deps.imageRuntimeStatus;
7426
7968
  if (!reader) return writeJsonError(res, 501, "Images runtime status is not available");
7427
- const serverConfig = await loadServerConfig2(deps.settingsStore);
7969
+ const serverConfig = await loadServerConfig3(deps.settingsStore);
7428
7970
  const images = serverConfig.images ?? DEFAULT_IMAGES_SERVER_CONFIG;
7429
7971
  const lifecycle = reader.status();
7430
7972
  const capability = await reader.inspectCapability(IMAGE_ADMIN_STATUS_TENANT);
@@ -7444,7 +7986,7 @@ async function handleImages(res, method, rest, deps) {
7444
7986
  httpLeases: safeStatusCount(generation.httpLeases),
7445
7987
  hostedLeases: safeStatusCount(generation.hostedLeases)
7446
7988
  }));
7447
- return writeJson3(res, 200, {
7989
+ return writeJson4(res, 200, {
7448
7990
  configured: {
7449
7991
  enabled: images.enabled,
7450
7992
  provider: images.provider,
@@ -7472,7 +8014,7 @@ async function handleImages(res, method, rest, deps) {
7472
8014
  async function handleStatus(res, method, deps) {
7473
8015
  if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on status`);
7474
8016
  const status = deps.outboundApiServer.getStatus();
7475
- const serverConfig = await loadServerConfig2(deps.settingsStore);
8017
+ const serverConfig = await loadServerConfig3(deps.settingsStore);
7476
8018
  const endpoints = ["chat", "responses", "messages", "gemini"].map((endpoint) => {
7477
8019
  const routes = (serverConfig.bindings ?? []).filter((binding) => binding.enabled && binding.endpoint === endpoint).map((binding) => gatewayBindingToEndpointConfig(binding));
7478
8020
  const useSubscription = routes.some((route) => route.useSubscription);
@@ -7510,14 +8052,14 @@ async function handleStatus(res, method, deps) {
7510
8052
  })() : void 0;
7511
8053
  if (status.running) {
7512
8054
  const queueStatus = deps.outboundApiServer.getQueueStatus();
7513
- return writeJson3(res, 200, {
8055
+ return writeJson4(res, 200, {
7514
8056
  ...status,
7515
8057
  endpoints,
7516
8058
  queueStatus,
7517
8059
  ...imageRuntime ? { imageRuntime } : {}
7518
8060
  });
7519
8061
  }
7520
- return writeJson3(res, 200, {
8062
+ return writeJson4(res, 200, {
7521
8063
  ...status,
7522
8064
  endpoints,
7523
8065
  ...imageRuntime ? { imageRuntime } : {}
@@ -7541,18 +8083,18 @@ function resolvePlaygroundPath(endpoint, body) {
7541
8083
  }
7542
8084
  async function handlePlayground(req, res, method, deps) {
7543
8085
  if (method !== "POST") return writeJsonError(res, 405, `method ${method} not allowed on playground`);
7544
- const body = await readJsonBody3(req);
8086
+ const body = await readJsonBody4(req);
7545
8087
  const endpoint = typeof body["endpoint"] === "string" ? body["endpoint"] : "";
7546
8088
  const key = typeof body["key"] === "string" ? body["key"] : "";
7547
8089
  const payload = body["body"];
7548
8090
  const status = deps.outboundApiServer.getStatus();
7549
8091
  if (!status.running || !status.port) return writeJsonError(res, 503, "outbound server not running");
7550
- const path2 = resolvePlaygroundPath(endpoint, isRecord2(payload) ? payload : {});
8092
+ const path2 = resolvePlaygroundPath(endpoint, isRecord3(payload) ? payload : {});
7551
8093
  if (!path2) return writeJsonError(res, 400, `unknown endpoint '${endpoint}'`);
7552
8094
  const upstreamBody = typeof payload === "string" ? payload : JSON.stringify(payload ?? {});
7553
8095
  await proxyToOutbound(res, status.port, path2, key, upstreamBody);
7554
8096
  }
7555
- function isRecord2(v) {
8097
+ function isRecord3(v) {
7556
8098
  return !!v && typeof v === "object" && !Array.isArray(v);
7557
8099
  }
7558
8100
  function proxyToOutbound(res, outboundPort, path2, key, body) {
@@ -7687,7 +8229,7 @@ async function handleUiStatic(req, res, urlPath, uiDist) {
7687
8229
  }
7688
8230
 
7689
8231
  // src/admin/version.ts
7690
- var DAEMON_VERSION = true ? "0.2.0" : "0.0.0-dev";
8232
+ var DAEMON_VERSION = true ? "0.3.0" : "0.0.0-dev";
7691
8233
 
7692
8234
  // src/admin/AdminServer.ts
7693
8235
  var LOOPBACK_ADDR = "127.0.0.1";
@@ -17582,6 +18124,14 @@ function buildDaemon(config, paths) {
17582
18124
  onEvent: (row, at) => usageThroughput.record(row, at)
17583
18125
  });
17584
18126
  const initialImagesConfig = normalizeServerConfig2(decryptedConfig.server).images;
18127
+ const initialSearchConfig = normalizeServerConfig2(decryptedConfig.server).search;
18128
+ for (const issue of validateSearchServerConfig2(
18129
+ decryptedConfig.server?.search
18130
+ )) {
18131
+ logger.warn("[search] ignoring invalid config: " + issue);
18132
+ }
18133
+ const searchRuntime = buildSearchRuntime(initialSearchConfig, { logger });
18134
+ const searchFrontendModes = initialSearchConfig.modes;
17585
18135
  const imageObservability = new ImageObservability();
17586
18136
  const imageRuntimeObservability = Object.freeze({
17587
18137
  telemetrySink: imageObservability.telemetrySink,
@@ -17711,7 +18261,9 @@ function buildDaemon(config, paths) {
17711
18261
  apiKeyPool,
17712
18262
  usageRecorder,
17713
18263
  openAIOperationRegistry,
17714
- responsesHostedImageIngress
18264
+ responsesHostedImageIngress,
18265
+ searchRuntime,
18266
+ searchFrontendModes
17715
18267
  });
17716
18268
  if (providerProxy.getDeps().openAIOperationRegistry !== openAIOperationRegistry) {
17717
18269
  throw new Error(
@@ -17776,7 +18328,9 @@ function buildDaemon(config, paths) {
17776
18328
  keySpendTracker,
17777
18329
  // configurable-logging: route the server's OWN lifecycle + relay dispatch-error
17778
18330
  // lines through the injected logger (honors level/format/file sink).
17779
- logger
18331
+ logger,
18332
+ // plan 阶段5: the same instance the managed frontends hold.
18333
+ searchRuntime
17780
18334
  });
17781
18335
  const auditDir = defaultAuditDir(paths.configPath);
17782
18336
  const billingDir = defaultBillingDir(paths.configPath);
@@ -17798,6 +18352,10 @@ function buildDaemon(config, paths) {
17798
18352
  }),
17799
18353
  routeLeaseManager,
17800
18354
  subscriptionAccounts,
18355
+ // search-settings-ui D3: the daemon's ONE search runtime + its
18356
+ // bootstrap-captured modes, for `GET /admin/api/search/diagnostics` and
18357
+ // `POST /admin/api/search/test` (501 when a light embedder omits it).
18358
+ searchStatus: { runtime: searchRuntime, modes: searchFrontendModes },
17801
18359
  accountAllowanceService,
17802
18360
  allowanceRefreshScheduler: claudeAllowanceRefreshScheduler,
17803
18361
  accountProbeService: accountHealthProbeScheduler,
@@ -17934,6 +18492,8 @@ function buildDaemon(config, paths) {
17934
18492
  keyDb,
17935
18493
  settingsStore,
17936
18494
  openAIOperationRegistry,
18495
+ searchRuntime,
18496
+ searchFrontendModes,
17937
18497
  imageRuntimeManager,
17938
18498
  imageObservability,
17939
18499
  imageCleanupService,
@@ -18080,6 +18640,116 @@ async function runImagesLiveDoctor(config, doctor, signal = new AbortController(
18080
18640
  );
18081
18641
  return true;
18082
18642
  }
18643
+ function readSearchApiConfigFromEnv(env = process.env) {
18644
+ const configs = {};
18645
+ const read = (name) => {
18646
+ const value = env[`OMNICROSS_SEARCH_${name}`]?.trim();
18647
+ return value ? value : void 0;
18648
+ };
18649
+ const tavilyKey = read("TAVILY_API_KEY");
18650
+ if (tavilyKey) {
18651
+ configs.tavily = { apiKey: tavilyKey, ...optionalHost(read("TAVILY_API_HOST")) };
18652
+ }
18653
+ const jinaKey = read("JINA_API_KEY");
18654
+ const jinaHost = read("JINA_API_HOST");
18655
+ if (jinaKey || jinaHost) {
18656
+ configs.jina = { ...jinaKey ? { apiKey: jinaKey } : {}, ...optionalHost(jinaHost) };
18657
+ }
18658
+ const searxngHost = read("SEARXNG_API_HOST");
18659
+ if (searxngHost) {
18660
+ const username = read("SEARXNG_BASIC_AUTH_USERNAME");
18661
+ const password = read("SEARXNG_BASIC_AUTH_PASSWORD");
18662
+ configs.searxng = {
18663
+ apiHost: searxngHost,
18664
+ ...username ? { basicAuthUsername: username } : {},
18665
+ ...password ? { basicAuthPassword: password } : {}
18666
+ };
18667
+ }
18668
+ const zhipuKey = read("ZHIPU_API_KEY");
18669
+ if (zhipuKey) {
18670
+ configs.zhipu = { apiKey: zhipuKey, ...optionalHost(read("ZHIPU_API_HOST")) };
18671
+ }
18672
+ const zaiKey = read("Z_AI_API_KEY");
18673
+ if (zaiKey) {
18674
+ configs["z.ai"] = { apiKey: zaiKey, ...optionalHost(read("Z_AI_API_HOST")) };
18675
+ }
18676
+ return configs;
18677
+ }
18678
+ function optionalHost(apiHost) {
18679
+ return apiHost ? { apiHost } : {};
18680
+ }
18681
+ function resolveSearchApiConfigs(configured, env = process.env) {
18682
+ const fromEnv = readSearchApiConfigFromEnv(env);
18683
+ const resolved = {};
18684
+ const tavily = configured?.tavily ?? fromEnv.tavily;
18685
+ if (tavily) resolved.tavily = tavily;
18686
+ const jina = configured?.jina ?? fromEnv.jina;
18687
+ if (jina) resolved.jina = jina;
18688
+ const searxng = configured?.searxng ?? fromEnv.searxng;
18689
+ if (searxng) resolved.searxng = searxng;
18690
+ const zhipu = configured?.zhipu ?? fromEnv.zhipu;
18691
+ if (zhipu) resolved.zhipu = zhipu;
18692
+ const zai = configured?.["z.ai"] ?? fromEnv["z.ai"];
18693
+ if (zai) resolved["z.ai"] = zai;
18694
+ return resolved;
18695
+ }
18696
+ function formatCapabilities(capabilities) {
18697
+ return [
18698
+ `apiKey=${capabilities.requiresApiKey}`,
18699
+ `cancellation=${capabilities.supportsCancellation}`,
18700
+ `urlRead=${capabilities.supportsUrlRead}`,
18701
+ `region=${capabilities.supportsRegion}`,
18702
+ `language=${capabilities.supportsLanguage}`,
18703
+ `timeRange=${capabilities.supportsTimeRange}`,
18704
+ `maxResults=${capabilities.maxResults ?? "unbounded"}`
18705
+ ].join(", ");
18706
+ }
18707
+ function formatDiagnostic(diagnostic) {
18708
+ const parts = [diagnostic.status];
18709
+ if (diagnostic.reason) parts.push(diagnostic.reason);
18710
+ if (diagnostic.error) {
18711
+ const { code, details } = diagnostic.error;
18712
+ parts.push(
18713
+ `code=${code}, transport=${details?.transport ?? "unknown"}, stage=${details?.stage ?? "unknown"}`
18714
+ );
18715
+ }
18716
+ return parts.join(" \u2014 ");
18717
+ }
18718
+ async function runSearchDoctor(live, env = process.env, source = {}) {
18719
+ const apiConfigs = resolveSearchApiConfigs(source.config?.providers, env);
18720
+ const egressPolicy = source.config && source.config.egress.allowedPrivateHosts.length > 0 ? { allowedPrivateHosts: [...source.config.egress.allowedPrivateHosts] } : void 0;
18721
+ const contributions = [
18722
+ ...builtinHttpSearchContributions4(),
18723
+ ...apiSearchContributions3(apiConfigs, { ...egressPolicy ? { egressPolicy } : {} })
18724
+ ];
18725
+ const declarations = source.runtime?.listProviders() ?? contributions;
18726
+ console.info("omnicross doctor search \u2014 builtin search contributions (offline, no network)");
18727
+ const modes = source.config?.modes ?? DEFAULT_SEARCH_FRONTEND_MODES;
18728
+ console.info(
18729
+ ` [i] frontend modes: ${SEARCH_FRONTEND_NAMES.map((name) => `${name}=${modes[name]}`).join(", ")}`
18730
+ );
18731
+ console.info(
18732
+ " [i] codex mode applies immediately; responses/anthropic modes, provider config, egress allowlist and policy apply on daemon restart"
18733
+ );
18734
+ for (const row of buildSearchDoctorSnapshot(declarations, apiConfigs)) {
18735
+ const mark = row.status === "unconfigured" ? "\u2013" : "\u2713";
18736
+ const suffix = row.status ? ` \u2014 ${row.status}: ${row.reason ?? ""}` : "";
18737
+ console.info(
18738
+ ` [${mark}] ${row.providerId}: source=${row.source}, kind=${row.kind}, ${formatCapabilities(row.capabilities)}${suffix}`
18739
+ );
18740
+ }
18741
+ if (!live) return 0;
18742
+ console.info(
18743
+ ` [\u26A0] live search sends ONE fixed public query per provider ("${SEARCH_DOCTOR_QUERY}") to the engine itself`
18744
+ );
18745
+ let hardFailure = false;
18746
+ for (const diagnostic of await runSearchLiveChecks(contributions)) {
18747
+ const mark = diagnostic.status === "healthy" ? "\u2713" : diagnostic.status === "failed" ? "\u2717" : "\u26A0";
18748
+ if (diagnostic.status === "failed") hardFailure = true;
18749
+ console.info(` [${mark}] ${diagnostic.providerId}: ${formatDiagnostic(diagnostic)}`);
18750
+ }
18751
+ return hardFailure ? 1 : 0;
18752
+ }
18083
18753
  async function runLiveProbe(url, key, fetchImpl = fetch) {
18084
18754
  try {
18085
18755
  const res = await fetchImpl(`${url.replace(/\/+$/, "")}/v1/messages/count_tokens`, {
@@ -18116,11 +18786,12 @@ async function runDoctor(argv, fetchImpl = fetch) {
18116
18786
  allowPositionals: true
18117
18787
  });
18118
18788
  const subject = positionals[0] ?? "claude";
18119
- if (subject !== "claude" && subject !== "images") {
18120
- throw new Error(`doctor: unknown subject '${subject}' (supported: 'claude', 'images')`);
18789
+ if (subject !== "claude" && subject !== "images" && subject !== "search") {
18790
+ throw new Error(`doctor: unknown subject '${subject}' (supported: 'claude', 'images', 'search')`);
18121
18791
  }
18122
18792
  const configPath = values.config;
18123
18793
  if (!configPath) {
18794
+ if (subject === "search") return runSearchDoctor(values.live === true);
18124
18795
  throw new Error("doctor: --config <path> is required (the same config `omnicross start` uses)");
18125
18796
  }
18126
18797
  const config = loadConfig(configPath);
@@ -18132,7 +18803,13 @@ async function runDoctor(argv, fetchImpl = fetch) {
18132
18803
  };
18133
18804
  const daemon = await buildDaemon(config, paths);
18134
18805
  try {
18135
- const serverConfig = await loadServerConfig3(daemon.settingsStore);
18806
+ const serverConfig = await loadServerConfig4(daemon.settingsStore);
18807
+ if (subject === "search") {
18808
+ return await runSearchDoctor(values.live === true, process.env, {
18809
+ ...serverConfig.search ? { config: serverConfig.search } : {},
18810
+ runtime: daemon.searchRuntime
18811
+ });
18812
+ }
18136
18813
  const checks = subject === "images" ? buildImagesDoctorChecks(await daemon.imageDoctor.inspectLocal(
18137
18814
  serverConfig.images ?? DEFAULT_IMAGES_SERVER_CONFIG2
18138
18815
  )) : buildClaudeDoctorChecks(serverConfig);
@@ -19209,7 +19886,7 @@ function tokensSuffix(configPath) {
19209
19886
 
19210
19887
  // src/commands/start.ts
19211
19888
  import { parseArgs as parseArgs10 } from "util";
19212
- import { loadServerConfig as loadServerConfig4 } from "@omnicross/core/outbound-api";
19889
+ import { loadServerConfig as loadServerConfig5 } from "@omnicross/core/outbound-api";
19213
19890
  import { getSharedAccountHealth as getSharedAccountHealth5 } from "@omnicross/core/pipeline/SubscriptionAccountHealth";
19214
19891
  import { getSharedAccountAllowanceScheduling as getSharedAccountAllowanceScheduling6 } from "@omnicross/core/pipeline/AccountAllowanceScheduling";
19215
19892
 
@@ -19271,7 +19948,7 @@ async function runStart(argv) {
19271
19948
  await daemon.llmConfig.ready();
19272
19949
  await daemon.migrateUsageStore();
19273
19950
  await daemon.providerProxy.start();
19274
- const serverConfig = await loadServerConfig4(daemon.settingsStore);
19951
+ const serverConfig = await loadServerConfig5(daemon.settingsStore);
19275
19952
  await daemon.imageCleanupService.runOnce();
19276
19953
  daemon.imageCleanupService.start();
19277
19954
  getSharedAccountHealth5().configure({
@@ -19294,7 +19971,9 @@ async function runStart(argv) {
19294
19971
  voucher: serverConfig.voucher,
19295
19972
  // claude-api-protocol-fidelity (§10): count_tokens strategy/budget,
19296
19973
  // /v1/models shape, synthetic-ping heartbeat (hot-applied inside applyConfig).
19297
- anthropic: serverConfig.anthropic
19974
+ anthropic: serverConfig.anthropic,
19975
+ // plan 阶段5: the Codex frontend mode is read live per request from here.
19976
+ search: serverConfig.search
19298
19977
  });
19299
19978
  let dashboardUrl = null;
19300
19979
  if (!values["no-dashboard"]) {
@@ -19409,6 +20088,16 @@ Usage:
19409
20088
  Check local Images config, roots, stores, permissions,
19410
20089
  account and cached evidence. --live warns, then consumes
19411
20090
  at most one minimal subscription image request.
20091
+ omnicross doctor search [--live] List the builtin search providers and their declared
20092
+ capabilities (offline, no config needed). --live sends ONE
20093
+ fixed public query per provider and reports healthy /
20094
+ degraded / blocked / failed.
20095
+ Keyed API providers show as unconfigured unless enabled
20096
+ through the DIAGNOSTIC-ONLY environment variables
20097
+ OMNICROSS_SEARCH_{TAVILY,JINA,SEARXNG,ZHIPU,Z_AI}_API_KEY
20098
+ / _API_HOST (plus SEARXNG_BASIC_AUTH_USERNAME/_PASSWORD).
20099
+ These are read only by this command and are not the
20100
+ configuration system.
19412
20101
  `;
19413
20102
  async function main() {
19414
20103
  const [, , subcommand, ...rest] = process.argv;