@omnicross/daemon 0.2.0 → 0.2.1

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,487 @@ 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 } from "@omnicross/core/search/http";
4603
+
4604
+ // src/search/searchDoctorProjection.ts
4605
+ import { toSearchErrorShape } from "@omnicross/contracts/search-types";
4606
+ import {
4607
+ JINA_CAPABILITIES,
4608
+ SEARXNG_CAPABILITIES,
4609
+ TAVILY_CAPABILITIES,
4610
+ ZHIPU_CAPABILITIES
4611
+ } from "@omnicross/core/search/api";
4612
+ import { builtinHttpSearchContributions } from "@omnicross/core/search/http";
4613
+ var API_DOCTOR_PROVIDERS = [
4614
+ {
4615
+ id: "tavily",
4616
+ capabilities: TAVILY_CAPABILITIES,
4617
+ configured: (configs) => configs.tavily !== void 0,
4618
+ missingReason: "no API key configured"
4619
+ },
4620
+ {
4621
+ id: "jina",
4622
+ capabilities: JINA_CAPABILITIES,
4623
+ configured: (configs) => configs.jina !== void 0,
4624
+ // Honest about the asymmetry: Jina CAN run keyless, but a provider nobody
4625
+ // asked for is still not enabled.
4626
+ missingReason: "not configured (Jina can run without a key, but must be enabled explicitly)"
4627
+ },
4628
+ {
4629
+ id: "searxng",
4630
+ capabilities: SEARXNG_CAPABILITIES,
4631
+ configured: (configs) => configs.searxng !== void 0,
4632
+ missingReason: "no API host configured"
4633
+ },
4634
+ {
4635
+ id: "zhipu",
4636
+ capabilities: ZHIPU_CAPABILITIES,
4637
+ configured: (configs) => configs.zhipu !== void 0,
4638
+ missingReason: "no API key configured"
4639
+ },
4640
+ {
4641
+ id: "z.ai",
4642
+ capabilities: ZHIPU_CAPABILITIES,
4643
+ configured: (configs) => configs["z.ai"] !== void 0,
4644
+ missingReason: "no API key configured"
4645
+ }
4646
+ ];
4647
+ function buildSearchDoctorSnapshot(contributions = builtinHttpSearchContributions(), apiConfigs) {
4648
+ const rows = contributions.map((contribution) => ({
4649
+ providerId: contribution.id,
4650
+ source: contribution.source,
4651
+ kind: contribution.kind,
4652
+ capabilities: contribution.capabilities
4653
+ }));
4654
+ if (apiConfigs === void 0) return rows;
4655
+ for (const provider of API_DOCTOR_PROVIDERS) {
4656
+ if (provider.configured(apiConfigs)) continue;
4657
+ rows.push({
4658
+ providerId: provider.id,
4659
+ source: "builtin",
4660
+ kind: "api",
4661
+ capabilities: provider.capabilities,
4662
+ status: "unconfigured",
4663
+ reason: provider.missingReason
4664
+ });
4665
+ }
4666
+ return rows;
4667
+ }
4668
+ var SEARCH_DOCTOR_QUERY = "mozilla developer network http headers";
4669
+ function classifyLiveSearchOutcome(providerId, outcome, checkedAt) {
4670
+ if (outcome.kind === "results") {
4671
+ if (outcome.count > 0) return { providerId, status: "healthy", checkedAt };
4672
+ return {
4673
+ providerId,
4674
+ status: "degraded",
4675
+ checkedAt,
4676
+ reason: "reachable, but the engine returned no usable results (possible partial drift)"
4677
+ };
4678
+ }
4679
+ const error = toSearchErrorShape(outcome.error);
4680
+ const stage = error.details?.stage;
4681
+ const { status, reason } = classifySearchFailure(stage, error.code);
4682
+ return { providerId, status, checkedAt, reason, error };
4683
+ }
4684
+ function classifySearchFailure(stage, code) {
4685
+ if (stage === "challenge") {
4686
+ return { status: "blocked", reason: "the engine served a bot challenge instead of results" };
4687
+ }
4688
+ if (stage === "trust") {
4689
+ return {
4690
+ status: "blocked",
4691
+ reason: "the engine served a page that failed the anti-decoy trust check"
4692
+ };
4693
+ }
4694
+ if (code === "policy_denied") {
4695
+ return {
4696
+ status: "blocked",
4697
+ reason: "the egress policy refused the request target"
4698
+ };
4699
+ }
4700
+ if (code === "parse_failed") {
4701
+ return {
4702
+ status: "failed",
4703
+ reason: "the response was not recognizable as a search result page (parser drift suspected)"
4704
+ };
4705
+ }
4706
+ if (code === "timeout") {
4707
+ return { status: "failed", reason: "the request exceeded its time budget" };
4708
+ }
4709
+ return { status: "failed", reason: `the request failed (${code})` };
4710
+ }
4711
+ async function runSearchLiveChecks(contributions, now = () => (/* @__PURE__ */ new Date()).toISOString()) {
4712
+ const diagnostics = [];
4713
+ for (const contribution of contributions) {
4714
+ try {
4715
+ const results = await contribution.provider.search(SEARCH_DOCTOR_QUERY, { maxResults: 5 });
4716
+ diagnostics.push(
4717
+ classifyLiveSearchOutcome(contribution.id, { kind: "results", count: results.length }, now())
4718
+ );
4719
+ } catch (error) {
4720
+ diagnostics.push(classifyLiveSearchOutcome(contribution.id, { kind: "failure", error }, now()));
4721
+ }
4722
+ }
4723
+ return diagnostics;
4724
+ }
4725
+
4726
+ // src/search/SearchAssembly.ts
4727
+ import { createSearchRuntime } from "@omnicross/core/search";
4728
+ import { apiSearchContributions } from "@omnicross/core/search/api";
4729
+ import { builtinHttpSearchContributions as builtinHttpSearchContributions2 } from "@omnicross/core/search/http";
4730
+ function searchEgressPolicyFrom(config) {
4731
+ const hosts = config.egress.allowedPrivateHosts;
4732
+ return hosts.length > 0 ? { allowedPrivateHosts: [...hosts] } : {};
4733
+ }
4734
+ function searchPolicyFrom(config) {
4735
+ const { preferred, allowed, fallbackEnabled, maxAttempts } = config.policy;
4736
+ return {
4737
+ ...preferred !== void 0 ? { preferred } : {},
4738
+ ...allowed !== void 0 ? { allowed: [...allowed] } : {},
4739
+ fallbackEnabled,
4740
+ ...maxAttempts !== void 0 ? { maxAttempts } : {}
4741
+ };
4742
+ }
4743
+ function searchContributionsFrom(config) {
4744
+ return [
4745
+ ...builtinHttpSearchContributions2(),
4746
+ ...apiSearchContributions(config.providers, {
4747
+ egressPolicy: searchEgressPolicyFrom(config)
4748
+ })
4749
+ ];
4750
+ }
4751
+ function buildSearchRuntime(config, options = {}) {
4752
+ const logger = options.logger ?? null;
4753
+ return createSearchRuntime({
4754
+ contributions: options.contributions ?? searchContributionsFrom(config),
4755
+ policy: searchPolicyFrom(config),
4756
+ ...logger ? {
4757
+ onEvent: (event) => {
4758
+ logger.debug(`[search] ${formatSearchEvent(event)}`);
4759
+ }
4760
+ } : {}
4761
+ });
4762
+ }
4763
+ function formatSearchEvent(event) {
4764
+ const parts = [
4765
+ `type=${event.type}`,
4766
+ `request=${event.requestId}`,
4767
+ `queryHash=${event.queryHash}`,
4768
+ `durationMs=${event.durationMs}`
4769
+ ];
4770
+ if ("providerId" in event && event.providerId !== void 0) {
4771
+ parts.push(`provider=${event.providerId}`);
4772
+ }
4773
+ if ("outcome" in event && event.outcome !== void 0) parts.push(`outcome=${event.outcome}`);
4774
+ if ("errorCode" in event && event.errorCode !== void 0) parts.push(`error=${event.errorCode}`);
4775
+ if ("resultCount" in event && event.resultCount !== void 0) {
4776
+ parts.push(`results=${event.resultCount}`);
4777
+ }
4778
+ if ("fallbackCount" in event && event.fallbackCount !== void 0) {
4779
+ parts.push(`fallbacks=${event.fallbackCount}`);
4780
+ }
4781
+ return parts.join(" ");
4782
+ }
4783
+
4784
+ // src/admin/searchAdminApi.ts
4785
+ var KEYLESS_HTTP_PROVIDER_IDS = /* @__PURE__ */ new Set(["http-bing", "http-duckduckgo"]);
4786
+ var API_PROVIDER_IDS = /* @__PURE__ */ new Set([
4787
+ "tavily",
4788
+ "jina",
4789
+ "searxng",
4790
+ "zhipu",
4791
+ "z.ai"
4792
+ ]);
4793
+ var SEARCH_QUERY_MAX_CODE_UNITS = 256;
4794
+ var QUERY_CONTROL_CHARS = /[\u0000-\u001f\u007f]/u;
4795
+ var SEARCH_RESULT_FIELD_CAPS = { title: 512, url: 2048, content: 1024 };
4796
+ var SEARCH_QUERY_MAX_RESULTS = 5;
4797
+ function sanitizeResultField(value, cap) {
4798
+ const text = typeof value === "string" ? value : value === null || value === void 0 ? "" : String(value);
4799
+ return text.replace(/[\u0000-\u001f\u007f]/gu, "").slice(0, cap);
4800
+ }
4801
+ function writeJson(res, status, body) {
4802
+ res.writeHead(status, { "Content-Type": "application/json" });
4803
+ res.end(JSON.stringify(body));
4804
+ }
4805
+ function writeErr(res, status, message) {
4806
+ writeJson(res, status, { error: { type: "admin_api_error", message } });
4807
+ }
4808
+ var SEARCH_MAX_BODY_BYTES = 64 * 1024;
4809
+ var SearchBodyTooLargeError = class extends Error {
4810
+ };
4811
+ async function readJsonBody2(req) {
4812
+ const chunks = [];
4813
+ let bytes = 0;
4814
+ for await (const chunk of req) {
4815
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
4816
+ bytes += buffer.length;
4817
+ if (bytes > SEARCH_MAX_BODY_BYTES) throw new SearchBodyTooLargeError("request body is too large");
4818
+ chunks.push(buffer);
4819
+ }
4820
+ const raw = Buffer.concat(chunks).toString("utf8");
4821
+ if (!raw.trim()) return {};
4822
+ try {
4823
+ const parsed = JSON.parse(raw);
4824
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
4825
+ } catch {
4826
+ return {};
4827
+ }
4828
+ }
4829
+ async function readBodyOrReject(req, res) {
4830
+ try {
4831
+ return await readJsonBody2(req);
4832
+ } catch (error) {
4833
+ if (error instanceof SearchBodyTooLargeError) {
4834
+ writeErr(res, 400, error.message);
4835
+ return void 0;
4836
+ }
4837
+ throw error;
4838
+ }
4839
+ }
4840
+ async function handleSearchAdmin(req, res, method, rest, deps) {
4841
+ if (rest.length === 1 && rest[0] === "diagnostics") {
4842
+ if (!deps.searchStatus) {
4843
+ return writeErr(res, 501, "Search status is not available in this build");
4844
+ }
4845
+ if (method !== "GET") {
4846
+ return writeErr(res, 405, `method ${method} not allowed on search diagnostics`);
4847
+ }
4848
+ return handleSearchDiagnostics(res, deps);
4849
+ }
4850
+ if (rest.length === 1 && rest[0] === "test") {
4851
+ if (!deps.searchStatus) {
4852
+ return writeErr(res, 501, "Search status is not available in this build");
4853
+ }
4854
+ if (method !== "POST") {
4855
+ return writeErr(res, 405, `method ${method} not allowed on search test`);
4856
+ }
4857
+ return handleSearchTest(req, res, deps);
4858
+ }
4859
+ if (rest.length === 1 && rest[0] === "query") {
4860
+ if (!deps.searchStatus) {
4861
+ return writeErr(res, 501, "Search status is not available in this build");
4862
+ }
4863
+ if (method !== "POST") {
4864
+ return writeErr(res, 405, `method ${method} not allowed on search query`);
4865
+ }
4866
+ return handleSearchQuery(req, res, deps);
4867
+ }
4868
+ return writeErr(res, 404, `unknown search route '/${rest.join("/")}'`);
4869
+ }
4870
+ async function handleSearchDiagnostics(res, deps) {
4871
+ const status = deps.searchStatus;
4872
+ const persisted = await loadServerConfig(deps.settingsStore);
4873
+ const search = persisted.search ?? DEFAULT_SEARCH_SERVER_CONFIG;
4874
+ const rows = buildSearchDoctorSnapshot(
4875
+ status.runtime.listProviders(),
4876
+ search.providers
4877
+ );
4878
+ const snapshot = {
4879
+ rows,
4880
+ modes: {
4881
+ // codex is read from the LIVE config per request — an admin PUT has
4882
+ // already applied. responses/anthropic were captured at bootstrap.
4883
+ codex: search.modes.codex,
4884
+ responses: status.modes.responses,
4885
+ anthropic: status.modes.anthropic
4886
+ },
4887
+ applySemantics: { codex: "immediate", rest: "restart" }
4888
+ };
4889
+ return writeJson(res, 200, { diagnostics: snapshot });
4890
+ }
4891
+ async function handleSearchTest(req, res, deps) {
4892
+ const status = deps.searchStatus;
4893
+ const body = await readBodyOrReject(req, res);
4894
+ if (body === void 0) return;
4895
+ const providerId = body["providerId"];
4896
+ if (typeof providerId !== "string" || providerId.length === 0) {
4897
+ return writeErr(res, 400, "providerId must be a non-empty string");
4898
+ }
4899
+ if (!KEYLESS_HTTP_PROVIDER_IDS.has(providerId) && !API_PROVIDER_IDS.has(providerId)) {
4900
+ return writeErr(res, 404, `unknown search provider '${providerId}'`);
4901
+ }
4902
+ const persisted = await loadServerConfig(deps.settingsStore);
4903
+ const search = persisted.search ?? DEFAULT_SEARCH_SERVER_CONFIG;
4904
+ const providers = search.providers;
4905
+ if (!KEYLESS_HTTP_PROVIDER_IDS.has(providerId) && providers[providerId] === void 0) {
4906
+ return writeErr(res, 400, `search provider '${providerId}' is not configured`);
4907
+ }
4908
+ const egressPolicy = searchEgressPolicyFrom(search);
4909
+ const fetchImpl = status.testFetch;
4910
+ const transport = fetchImpl ? createSearchHttpTransport({ fetch: fetchImpl, egressPolicy }) : void 0;
4911
+ const contributions = [
4912
+ ...builtinHttpSearchContributions3(transport),
4913
+ ...apiSearchContributions2(search.providers, {
4914
+ egressPolicy,
4915
+ ...fetchImpl ? { fetchImpl } : {}
4916
+ })
4917
+ ];
4918
+ const contribution = contributions.find((c) => c.id === providerId);
4919
+ if (!contribution) {
4920
+ return writeErr(res, 400, `search provider '${providerId}' is not configured`);
4921
+ }
4922
+ const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
4923
+ try {
4924
+ const results = await contribution.provider.search(SEARCH_DOCTOR_QUERY, { maxResults: 5 });
4925
+ const diagnostic = classifyLiveSearchOutcome(
4926
+ contribution.id,
4927
+ { kind: "results", count: results.length },
4928
+ checkedAt
4929
+ );
4930
+ const response = { diagnostic, resultCount: results.length };
4931
+ return writeJson(res, 200, { result: response });
4932
+ } catch (error) {
4933
+ const diagnostic = classifyLiveSearchOutcome(contribution.id, { kind: "failure", error }, checkedAt);
4934
+ const response = { diagnostic };
4935
+ return writeJson(res, 200, { result: response });
4936
+ }
4937
+ }
4938
+ async function handleSearchQuery(req, res, deps) {
4939
+ const status = deps.searchStatus;
4940
+ const body = await readBodyOrReject(req, res);
4941
+ if (body === void 0) return;
4942
+ const providerId = body["providerId"];
4943
+ if (typeof providerId !== "string" || providerId.length === 0) {
4944
+ return writeErr(res, 400, "providerId must be a non-empty string");
4945
+ }
4946
+ if (!KEYLESS_HTTP_PROVIDER_IDS.has(providerId) && !API_PROVIDER_IDS.has(providerId)) {
4947
+ return writeErr(res, 404, `unknown search provider '${providerId}'`);
4948
+ }
4949
+ const query2 = body["query"];
4950
+ if (typeof query2 !== "string" || query2.trim().length === 0) {
4951
+ return writeErr(res, 400, "query must be a non-empty string");
4952
+ }
4953
+ if (query2.length > SEARCH_QUERY_MAX_CODE_UNITS) {
4954
+ return writeErr(res, 400, `query must be at most ${SEARCH_QUERY_MAX_CODE_UNITS} characters`);
4955
+ }
4956
+ if (QUERY_CONTROL_CHARS.test(query2)) {
4957
+ return writeErr(res, 400, "query must not contain control characters");
4958
+ }
4959
+ const persisted = await loadServerConfig(deps.settingsStore);
4960
+ const search = persisted.search ?? DEFAULT_SEARCH_SERVER_CONFIG;
4961
+ const providers = search.providers;
4962
+ if (!KEYLESS_HTTP_PROVIDER_IDS.has(providerId) && providers[providerId] === void 0) {
4963
+ return writeErr(res, 400, `search provider '${providerId}' is not configured`);
4964
+ }
4965
+ const egressPolicy = searchEgressPolicyFrom(search);
4966
+ const fetchImpl = status.testFetch;
4967
+ const transport = fetchImpl ? createSearchHttpTransport({ fetch: fetchImpl, egressPolicy }) : void 0;
4968
+ const contributions = [
4969
+ ...builtinHttpSearchContributions3(transport),
4970
+ ...apiSearchContributions2(search.providers, {
4971
+ egressPolicy,
4972
+ ...fetchImpl ? { fetchImpl } : {}
4973
+ })
4974
+ ];
4975
+ const contribution = contributions.find((c) => c.id === providerId);
4976
+ if (!contribution) {
4977
+ return writeErr(res, 400, `search provider '${providerId}' is not configured`);
4978
+ }
4979
+ const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
4980
+ try {
4981
+ const results = await contribution.provider.search(query2, { maxResults: 5 });
4982
+ const sanitized = results.slice(0, SEARCH_QUERY_MAX_RESULTS).map((result) => ({
4983
+ title: sanitizeResultField(result.title, SEARCH_RESULT_FIELD_CAPS.title),
4984
+ url: sanitizeResultField(result.url, SEARCH_RESULT_FIELD_CAPS.url),
4985
+ content: sanitizeResultField(result.content, SEARCH_RESULT_FIELD_CAPS.content)
4986
+ }));
4987
+ const diagnostic = sanitized.length === 0 ? { providerId: contribution.id, status: "healthy", checkedAt } : classifyLiveSearchOutcome(
4988
+ contribution.id,
4989
+ { kind: "results", count: sanitized.length },
4990
+ checkedAt
4991
+ );
4992
+ const response = {
4993
+ diagnostic,
4994
+ resultCount: sanitized.length,
4995
+ results: sanitized
4996
+ };
4997
+ return writeJson(res, 200, { result: response });
4998
+ } catch (error) {
4999
+ const diagnostic = classifyLiveSearchOutcome(contribution.id, { kind: "failure", error }, checkedAt);
5000
+ const response = { diagnostic };
5001
+ return writeJson(res, 200, { result: response });
5002
+ }
5003
+ }
5004
+
5005
+ // src/admin/searchAdminView.ts
5006
+ var API_KEY_PROVIDERS = /* @__PURE__ */ new Set(["tavily", "jina", "zhipu", "z.ai"]);
5007
+ var BASIC_AUTH_PROVIDERS = /* @__PURE__ */ new Set(["searxng"]);
5008
+ function isRecord(value) {
5009
+ return value !== null && typeof value === "object" && !Array.isArray(value);
5010
+ }
5011
+ function redactSearchServerConfig(search) {
5012
+ const providers = {};
5013
+ for (const [id, raw] of Object.entries(search.providers)) {
5014
+ const entry = raw;
5015
+ const view = {};
5016
+ if (entry.apiHost !== void 0) view.apiHost = entry.apiHost;
5017
+ if (entry.basicAuthUsername !== void 0) view.basicAuthUsername = entry.basicAuthUsername;
5018
+ if (API_KEY_PROVIDERS.has(id)) {
5019
+ view.apiKeyConfigured = typeof entry.apiKey === "string" && entry.apiKey.length > 0;
5020
+ }
5021
+ if (BASIC_AUTH_PROVIDERS.has(id)) {
5022
+ view.basicAuthPasswordConfigured = typeof entry.basicAuthPassword === "string" && entry.basicAuthPassword.length > 0;
5023
+ }
5024
+ providers[id] = view;
5025
+ }
5026
+ return {
5027
+ modes: search.modes,
5028
+ providers,
5029
+ egress: { allowedPrivateHosts: [...search.egress.allowedPrivateHosts] },
5030
+ policy: { ...search.policy, ...search.policy.allowed ? { allowed: [...search.policy.allowed] } : {} }
5031
+ };
5032
+ }
5033
+ function storedSecrets(current, id) {
5034
+ const entry = current.providers[id];
5035
+ if (!entry) return {};
5036
+ const out = {};
5037
+ if (typeof entry.apiKey === "string" && entry.apiKey.length > 0) out.apiKey = entry.apiKey;
5038
+ if (typeof entry.basicAuthPassword === "string" && entry.basicAuthPassword.length > 0) {
5039
+ out.basicAuthPassword = entry.basicAuthPassword;
5040
+ }
5041
+ return out;
5042
+ }
5043
+ function resolveSecretField(entry, field, stored) {
5044
+ if (!(field in entry)) {
5045
+ if (stored !== void 0) entry[field] = stored;
5046
+ return;
5047
+ }
5048
+ const value = entry[field];
5049
+ if (value === null) {
5050
+ delete entry[field];
5051
+ return;
5052
+ }
5053
+ if (typeof value === "string" && value.trim().length > 0) return;
5054
+ if (stored !== void 0) entry[field] = stored;
5055
+ else delete entry[field];
5056
+ }
5057
+ function preserveSearchSecrets(incoming, current) {
5058
+ if (!isRecord(incoming)) return incoming;
5059
+ const section = { ...incoming };
5060
+ const providersValue = section["providers"];
5061
+ if (!isRecord(providersValue)) return section;
5062
+ const providers = {};
5063
+ for (const [id, entryValue] of Object.entries(providersValue)) {
5064
+ if (!isRecord(entryValue)) {
5065
+ providers[id] = entryValue;
5066
+ continue;
5067
+ }
5068
+ const entry = { ...entryValue };
5069
+ delete entry["apiKeyConfigured"];
5070
+ delete entry["basicAuthPasswordConfigured"];
5071
+ const stored = storedSecrets(current, id);
5072
+ resolveSecretField(entry, "apiKey", stored.apiKey);
5073
+ resolveSecretField(entry, "basicAuthPassword", stored.basicAuthPassword);
5074
+ providers[id] = entry;
5075
+ }
5076
+ section["providers"] = providers;
5077
+ return section;
5078
+ }
5079
+
4588
5080
  // src/admin/keyPolicyBody.ts
4589
5081
  function parseKeyPolicyBody(body) {
4590
5082
  const policy = {};
@@ -4647,7 +5139,7 @@ function parseKeyPolicyBody(body) {
4647
5139
  var ENDPOINTS = /* @__PURE__ */ new Set(["chat", "responses", "messages", "gemini"]);
4648
5140
  var TARGET_KINDS = /* @__PURE__ */ new Set(["account", "account-group", "account-pool", "provider"]);
4649
5141
  var FALLBACKS = /* @__PURE__ */ new Set(["next", "fail", "global"]);
4650
- function isRecord(value) {
5142
+ function isRecord2(value) {
4651
5143
  return !!value && typeof value === "object" && !Array.isArray(value);
4652
5144
  }
4653
5145
  function nonBlank(value) {
@@ -4667,7 +5159,7 @@ function validateGatewayBindingsSegment(patch) {
4667
5159
  const ids = /* @__PURE__ */ new Set();
4668
5160
  raw.forEach((entry, index) => {
4669
5161
  const path2 = `bindings[${index}]`;
4670
- if (!isRecord(entry)) {
5162
+ if (!isRecord2(entry)) {
4671
5163
  errors.push(`${path2} must be an object`);
4672
5164
  return;
4673
5165
  }
@@ -4696,12 +5188,12 @@ function validateGatewayBindingsSegment(patch) {
4696
5188
  } else if (entry.modelMappings.length > 100) {
4697
5189
  errors.push(`${path2}.modelMappings cannot contain more than 100 entries`);
4698
5190
  } else if (entry.modelMappings.some(
4699
- (mapping) => !isRecord(mapping) || !nonBlank(mapping.source) || !nonBlank(mapping.target)
5191
+ (mapping) => !isRecord2(mapping) || !nonBlank(mapping.source) || !nonBlank(mapping.target)
4700
5192
  )) {
4701
5193
  errors.push(`${path2}.modelMappings must contain non-empty source and target strings`);
4702
5194
  }
4703
5195
  }
4704
- if (!isRecord(entry.target) || !TARGET_KINDS.has(String(entry.target.kind))) {
5196
+ if (!isRecord2(entry.target) || !TARGET_KINDS.has(String(entry.target.kind))) {
4705
5197
  errors.push(`${path2}.target is invalid`);
4706
5198
  } else {
4707
5199
  if (!nonBlank(entry.target.providerId)) errors.push(`${path2}.target.providerId is required`);
@@ -4716,7 +5208,7 @@ function validateGatewayBindingsSegment(patch) {
4716
5208
  }
4717
5209
  }
4718
5210
  if (entry.modelMap !== void 0) {
4719
- if (!isRecord(entry.modelMap) || Object.values(entry.modelMap).some((value) => typeof value !== "string")) {
5211
+ if (!isRecord2(entry.modelMap) || Object.values(entry.modelMap).some((value) => typeof value !== "string")) {
4720
5212
  errors.push(`${path2}.modelMap must contain string values`);
4721
5213
  }
4722
5214
  }
@@ -4737,19 +5229,19 @@ function validateGatewayBindingsSegment(patch) {
4737
5229
  import {
4738
5230
  generateVoucherCode,
4739
5231
  hashVoucherCode,
4740
- loadServerConfig,
5232
+ loadServerConfig as loadServerConfig2,
4741
5233
  newVoucherId,
4742
5234
  toVoucherInfo,
4743
5235
  voucherCodePrefix
4744
5236
  } from "@omnicross/core/outbound-api";
4745
- function writeJson(res, status, body) {
5237
+ function writeJson2(res, status, body) {
4746
5238
  res.writeHead(status, { "Content-Type": "application/json" });
4747
5239
  res.end(JSON.stringify(body));
4748
5240
  }
4749
- function writeErr(res, status, message) {
4750
- writeJson(res, status, { error: { type: "voucher_error", message } });
5241
+ function writeErr2(res, status, message) {
5242
+ writeJson2(res, status, { error: { type: "voucher_error", message } });
4751
5243
  }
4752
- function readJsonBody2(req) {
5244
+ function readJsonBody3(req) {
4753
5245
  return new Promise((resolve11, reject) => {
4754
5246
  const chunks = [];
4755
5247
  req.on("data", (c) => chunks.push(c));
@@ -4800,26 +5292,26 @@ function parseVoucherCreateBody(body) {
4800
5292
  return { ok: true, input };
4801
5293
  }
4802
5294
  async function voucherEnabled(deps) {
4803
- const config = await loadServerConfig(deps.settingsStore);
5295
+ const config = await loadServerConfig2(deps.settingsStore);
4804
5296
  return config.voucher?.enabled === true;
4805
5297
  }
4806
5298
  async function handleVoucher(req, res, method, rest, deps) {
4807
5299
  const voucherDb = deps.voucherDb;
4808
- if (!voucherDb) return writeErr(res, 501, "Voucher feature is not available");
5300
+ if (!voucherDb) return writeErr2(res, 501, "Voucher feature is not available");
4809
5301
  if (method === "GET" && rest.length === 0) {
4810
5302
  const rows = await voucherDb.voucherList();
4811
- return writeJson(res, 200, { vouchers: rows.map(toVoucherInfo) });
5303
+ return writeJson2(res, 200, { vouchers: rows.map(toVoucherInfo) });
4812
5304
  }
4813
5305
  if (method === "POST" && rest.length === 0) {
4814
- if (!await voucherEnabled(deps)) return writeErr(res, 403, "Voucher feature is disabled");
5306
+ if (!await voucherEnabled(deps)) return writeErr2(res, 403, "Voucher feature is disabled");
4815
5307
  let body;
4816
5308
  try {
4817
- body = await readJsonBody2(req);
5309
+ body = await readJsonBody3(req);
4818
5310
  } catch {
4819
- return writeErr(res, 400, "Invalid JSON in request body");
5311
+ return writeErr2(res, 400, "Invalid JSON in request body");
4820
5312
  }
4821
5313
  const parsed = parseVoucherCreateBody(body);
4822
- if (!parsed.ok) return writeErr(res, 400, parsed.message);
5314
+ if (!parsed.ok) return writeErr2(res, 400, parsed.message);
4823
5315
  const code = generateVoucherCode();
4824
5316
  const created = await voucherDb.voucherCreate({
4825
5317
  id: newVoucherId(),
@@ -4827,7 +5319,7 @@ async function handleVoucher(req, res, method, rest, deps) {
4827
5319
  codePrefix: voucherCodePrefix(code),
4828
5320
  ...parsed.input
4829
5321
  });
4830
- return writeJson(res, 201, {
5322
+ return writeJson2(res, 201, {
4831
5323
  id: created.id,
4832
5324
  codePrefix: created.codePrefix,
4833
5325
  type: created.type,
@@ -4838,11 +5330,11 @@ async function handleVoucher(req, res, method, rest, deps) {
4838
5330
  }
4839
5331
  const id = rest[0];
4840
5332
  if (method === "POST" && id && rest[1] === "revoke") {
4841
- if (!await voucherEnabled(deps)) return writeErr(res, 403, "Voucher feature is disabled");
5333
+ if (!await voucherEnabled(deps)) return writeErr2(res, 403, "Voucher feature is disabled");
4842
5334
  const ok = await voucherDb.voucherRevokeCas(id, Date.now());
4843
- return writeJson(res, ok ? 200 : 409, { ok });
5335
+ return writeJson2(res, ok ? 200 : 409, { ok });
4844
5336
  }
4845
- return writeErr(res, 405, `method ${method} not allowed on voucher`);
5337
+ return writeErr2(res, 405, `method ${method} not allowed on voucher`);
4846
5338
  }
4847
5339
 
4848
5340
  // src/admin/webhookConfigBody.ts
@@ -5756,12 +6248,12 @@ async function handlePricingResolveConflicts(body, deps) {
5756
6248
  }
5757
6249
 
5758
6250
  // src/admin/accountAllowanceApi.ts
5759
- function writeJson2(res, status, body) {
6251
+ function writeJson3(res, status, body) {
5760
6252
  res.writeHead(status, { "Content-Type": "application/json" });
5761
6253
  res.end(JSON.stringify(body));
5762
6254
  }
5763
6255
  function writeError2(res, status, message) {
5764
- writeJson2(res, status, { error: { type: "account_allowance_error", message } });
6256
+ writeJson3(res, status, { error: { type: "account_allowance_error", message } });
5765
6257
  }
5766
6258
  function readJson2(req) {
5767
6259
  return new Promise((resolve11, reject) => {
@@ -5794,7 +6286,7 @@ async function handleAccountAllowanceApi(req, res, method, rest, service) {
5794
6286
  if (!service.getSchedulingStatus) {
5795
6287
  return writeError2(res, 501, "allowance scheduling diagnostics are not available");
5796
6288
  }
5797
- return writeJson2(res, 200, { scheduling: service.getSchedulingStatus() });
6289
+ return writeJson3(res, 200, { scheduling: service.getSchedulingStatus() });
5798
6290
  }
5799
6291
  if (method === "GET") {
5800
6292
  const params = query(req);
@@ -5803,7 +6295,7 @@ async function handleAccountAllowanceApi(req, res, method, rest, service) {
5803
6295
  if (providerId === null) return writeError2(res, 400, "providerId must be claude or codex");
5804
6296
  const accountId = rest.length >= 2 ? rest[1] : params.get("accountId") ?? void 0;
5805
6297
  const allowances = await service.list({ providerId, accountId });
5806
- return writeJson2(res, 200, { allowances });
6298
+ return writeJson3(res, 200, { allowances });
5807
6299
  }
5808
6300
  if (method === "POST" && rest[0] === "refresh") {
5809
6301
  const body = await readJson2(req);
@@ -5818,7 +6310,7 @@ async function handleAccountAllowanceApi(req, res, method, rest, service) {
5818
6310
  if (accountId && allowances.length === 0) {
5819
6311
  return writeError2(res, 404, `Claude account '${accountId}' not found`);
5820
6312
  }
5821
- return writeJson2(res, 200, { allowances });
6313
+ return writeJson3(res, 200, { allowances });
5822
6314
  }
5823
6315
  return writeError2(res, 405, `method ${method} not allowed on account allowances`);
5824
6316
  }
@@ -5837,7 +6329,7 @@ function readBody(req) {
5837
6329
  req.on("error", reject);
5838
6330
  });
5839
6331
  }
5840
- async function readJsonBody3(req) {
6332
+ async function readJsonBody4(req) {
5841
6333
  const raw = await readBody(req);
5842
6334
  if (!raw.trim()) return {};
5843
6335
  try {
@@ -5847,12 +6339,12 @@ async function readJsonBody3(req) {
5847
6339
  return {};
5848
6340
  }
5849
6341
  }
5850
- function writeJson3(res, status, body) {
6342
+ function writeJson4(res, status, body) {
5851
6343
  res.writeHead(status, { "Content-Type": "application/json" });
5852
6344
  res.end(JSON.stringify(body));
5853
6345
  }
5854
6346
  function writeJsonError(res, status, message) {
5855
- writeJson3(res, status, { error: { type: "admin_api_error", message } });
6347
+ writeJson4(res, status, { error: { type: "admin_api_error", message } });
5856
6348
  }
5857
6349
  function maskProviderApiKey(apiKey) {
5858
6350
  if (!apiKey) return "";
@@ -5959,6 +6451,8 @@ async function handleAdminApi(req, res, path2, deps) {
5959
6451
  return await handleServer(req, res, method, deps);
5960
6452
  case "images":
5961
6453
  return await handleImages(res, method, rest, deps);
6454
+ case "search":
6455
+ return await handleSearchAdmin(req, res, method, rest, deps);
5962
6456
  case "accounts":
5963
6457
  return await handleAccounts(req, res, method, rest, deps);
5964
6458
  case "cli":
@@ -5992,7 +6486,7 @@ function requestQuery(req) {
5992
6486
  return new URLSearchParams(qIdx >= 0 ? raw.slice(qIdx + 1) : "");
5993
6487
  }
5994
6488
  function writeResult(res, result) {
5995
- writeJson3(res, result.status, result.body);
6489
+ writeJson4(res, result.status, result.body);
5996
6490
  }
5997
6491
  async function handleUsage(req, res, method, rest, deps) {
5998
6492
  if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on usage`);
@@ -6001,13 +6495,13 @@ async function handleUsage(req, res, method, rest, deps) {
6001
6495
  async function handleDashboardRoute(res, method, deps) {
6002
6496
  if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on dashboard`);
6003
6497
  const result = await handleDashboard(deps);
6004
- return writeJson3(res, result.status, result.body);
6498
+ return writeJson4(res, result.status, result.body);
6005
6499
  }
6006
6500
  async function handlePricing(req, res, method, rest, deps) {
6007
6501
  if (rest.length === 0) {
6008
6502
  if (method === "GET") return writeResult(res, await handlePricingList(deps));
6009
6503
  if (method === "PUT") {
6010
- return writeResult(res, await handlePricingUpsert(await readJsonBody3(req), deps));
6504
+ return writeResult(res, await handlePricingUpsert(await readJsonBody4(req), deps));
6011
6505
  }
6012
6506
  if (method === "DELETE") {
6013
6507
  return writeResult(res, await handlePricingDelete(requestQuery(req), deps));
@@ -6018,7 +6512,7 @@ async function handlePricing(req, res, method, rest, deps) {
6018
6512
  return writeResult(res, await handlePricingFetchLatest(deps));
6019
6513
  }
6020
6514
  if (method === "POST" && rest.length === 1 && rest[0] === "resolve-conflicts") {
6021
- return writeResult(res, await handlePricingResolveConflicts(await readJsonBody3(req), deps));
6515
+ return writeResult(res, await handlePricingResolveConflicts(await readJsonBody4(req), deps));
6022
6516
  }
6023
6517
  return writeJsonError(res, 404, `unknown pricing route '${rest.join("/")}'`);
6024
6518
  }
@@ -6032,15 +6526,15 @@ function migrationDeps(deps) {
6032
6526
  }
6033
6527
  async function handleMigrationExport(req, res, method, deps) {
6034
6528
  if (method !== "POST") return writeJsonError(res, 405, `method ${method} not allowed on export`);
6035
- const body = await readJsonBody3(req);
6529
+ const body = await readJsonBody4(req);
6036
6530
  const result = await handleExport(body, migrationDeps(deps));
6037
- return writeJson3(res, result.status, result.body);
6531
+ return writeJson4(res, result.status, result.body);
6038
6532
  }
6039
6533
  async function handleMigrationImport(req, res, method, deps) {
6040
6534
  if (method !== "POST") return writeJsonError(res, 405, `method ${method} not allowed on import`);
6041
- const body = await readJsonBody3(req);
6535
+ const body = await readJsonBody4(req);
6042
6536
  const result = await handleImport(body, migrationDeps(deps));
6043
- return writeJson3(res, result.status, result.body);
6537
+ return writeJson4(res, result.status, result.body);
6044
6538
  }
6045
6539
  async function handleProviders(req, res, method, rest, deps) {
6046
6540
  const cfg = loadConfig(deps.configPath);
@@ -6071,13 +6565,13 @@ async function handleProviders(req, res, method, rest, deps) {
6071
6565
  if (method === "GET" && rest.length === 2 && rest[1] === "reveal-key") {
6072
6566
  const row = cfg.providers.find((p) => p.id === rest[0]);
6073
6567
  if (!row) return writeJsonError(res, 404, `provider '${rest[0]}' not found`);
6074
- return writeJson3(res, 200, { apiKey: row.apiKey ?? "" });
6568
+ return writeJson4(res, 200, { apiKey: row.apiKey ?? "" });
6075
6569
  }
6076
6570
  if (method === "GET") {
6077
- return writeJson3(res, 200, { providers: cfg.providers.map(toProviderView) });
6571
+ return writeJson4(res, 200, { providers: cfg.providers.map(toProviderView) });
6078
6572
  }
6079
6573
  if (method === "POST") {
6080
- const body = await readJsonBody3(req);
6574
+ const body = await readJsonBody4(req);
6081
6575
  const provider = parseProviderInput(body, void 0);
6082
6576
  if (!provider) return writeJsonError(res, 400, "invalid provider (id, apiFormat, baseUrl required)");
6083
6577
  if (cfg.providers.some((p) => p.id === provider.id)) {
@@ -6085,25 +6579,25 @@ async function handleProviders(req, res, method, rest, deps) {
6085
6579
  }
6086
6580
  cfg.providers.push(provider);
6087
6581
  persistProviders(cfg, deps);
6088
- return writeJson3(res, 201, { provider: toProviderView(provider) });
6582
+ return writeJson4(res, 201, { provider: toProviderView(provider) });
6089
6583
  }
6090
6584
  const id = rest[0];
6091
6585
  if (!id) return writeJsonError(res, 400, "provider id required in path");
6092
6586
  const idx = cfg.providers.findIndex((p) => p.id === id);
6093
6587
  if (idx < 0) return writeJsonError(res, 404, `provider '${id}' not found`);
6094
6588
  if (method === "PUT") {
6095
- const body = await readJsonBody3(req);
6589
+ const body = await readJsonBody4(req);
6096
6590
  const existing = cfg.providers[idx];
6097
6591
  const updated = parseProviderInput(body, existing);
6098
6592
  if (!updated) return writeJsonError(res, 400, "invalid provider (apiFormat, baseUrl required)");
6099
6593
  cfg.providers[idx] = updated;
6100
6594
  persistProviders(cfg, deps);
6101
- return writeJson3(res, 200, { provider: toProviderView(updated) });
6595
+ return writeJson4(res, 200, { provider: toProviderView(updated) });
6102
6596
  }
6103
6597
  if (method === "DELETE") {
6104
6598
  cfg.providers.splice(idx, 1);
6105
6599
  persistProviders(cfg, deps);
6106
- return writeJson3(res, 200, { ok: true });
6600
+ return writeJson4(res, 200, { ok: true });
6107
6601
  }
6108
6602
  return writeJsonError(res, 405, `method ${method} not allowed on providers`);
6109
6603
  }
@@ -6112,7 +6606,7 @@ function persistProviders(cfg, deps) {
6112
6606
  deps.llmConfig.reload(cfg);
6113
6607
  }
6114
6608
  async function handleProviderReorder(req, res, cfg, deps) {
6115
- const body = await readJsonBody3(req);
6609
+ const body = await readJsonBody4(req);
6116
6610
  const rawOrder = body["order"];
6117
6611
  if (!Array.isArray(rawOrder)) {
6118
6612
  return writeJsonError(res, 400, "reorder requires { order: string[] }");
@@ -6136,14 +6630,14 @@ async function handleProviderReorder(req, res, cfg, deps) {
6136
6630
  }
6137
6631
  cfg.providers = reordered;
6138
6632
  persistProviders(cfg, deps);
6139
- return writeJson3(res, 200, { ok: true, providers: cfg.providers.map(toProviderView) });
6633
+ return writeJson4(res, 200, { ok: true, providers: cfg.providers.map(toProviderView) });
6140
6634
  }
6141
6635
  async function handleDiscoverModels(res, id, cfg) {
6142
6636
  if (!id) return writeJsonError(res, 400, "provider id required in path");
6143
6637
  const row = cfg.providers.find((p) => p.id === id);
6144
6638
  if (!row) return writeJsonError(res, 404, `provider '${id}' not found`);
6145
6639
  if (row.apiFormat !== "openai" && row.apiFormat !== "openai-response") {
6146
- return writeJson3(res, 200, { models: [], unsupportedFormat: true });
6640
+ return writeJson4(res, 200, { models: [], unsupportedFormat: true });
6147
6641
  }
6148
6642
  const resolvedKey = resolveEnvKey(row.apiKey);
6149
6643
  const base = row.baseUrl.replace(/\/+$/, "");
@@ -6160,32 +6654,32 @@ async function handleDiscoverModels(res, id, cfg) {
6160
6654
  message = parsed?.error?.message || parsed?.message || message;
6161
6655
  } catch {
6162
6656
  }
6163
- return writeJson3(res, 200, {
6657
+ return writeJson4(res, 200, {
6164
6658
  models: [],
6165
6659
  error: `discovery failed (${response.status})${message ? `: ${message}` : ""}`
6166
6660
  });
6167
6661
  }
6168
6662
  const data = await response.json();
6169
6663
  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 });
6664
+ return writeJson4(res, 200, { models });
6171
6665
  } catch (err5) {
6172
6666
  const message = err5 instanceof Error ? err5.message : String(err5);
6173
- return writeJson3(res, 200, { models: [], error: `discovery failed: ${message}` });
6667
+ return writeJson4(res, 200, { models: [], error: `discovery failed: ${message}` });
6174
6668
  }
6175
6669
  }
6176
6670
  async function handleTestModel(req, res, id, cfg) {
6177
6671
  if (!id) return writeJsonError(res, 400, "provider id required in path");
6178
6672
  const row = cfg.providers.find((p) => p.id === id);
6179
6673
  if (!row) return writeJsonError(res, 404, `provider '${id}' not found`);
6180
- const body = await readJsonBody3(req);
6674
+ const body = await readJsonBody4(req);
6181
6675
  const model = typeof body["model"] === "string" ? body["model"].trim() : "";
6182
6676
  if (!model) return writeJsonError(res, 400, "test requires a { model } string");
6183
6677
  if (row.apiFormat === "gemini") {
6184
- return writeJson3(res, 200, { ok: false, unsupportedFormat: true });
6678
+ return writeJson4(res, 200, { ok: false, unsupportedFormat: true });
6185
6679
  }
6186
6680
  const resolvedKey = resolveEnvKey(row.apiKey);
6187
6681
  if (!resolvedKey) {
6188
- return writeJson3(res, 200, { ok: false, message: "no API key configured for this provider" });
6682
+ return writeJson4(res, 200, { ok: false, message: "no API key configured for this provider" });
6189
6683
  }
6190
6684
  let url = row.baseUrl.replace(/\/+$/, "");
6191
6685
  const prompt = "Reply with the single word: OK.";
@@ -6224,9 +6718,9 @@ async function handleTestModel(req, res, id, cfg) {
6224
6718
  message = parsed?.error?.message || parsed?.message || message;
6225
6719
  } catch {
6226
6720
  }
6227
- return writeJson3(res, 200, { ok: false, status: response.status, latencyMs, message });
6721
+ return writeJson4(res, 200, { ok: false, status: response.status, latencyMs, message });
6228
6722
  }
6229
- return writeJson3(res, 200, {
6723
+ return writeJson4(res, 200, {
6230
6724
  ok: true,
6231
6725
  status: response.status,
6232
6726
  latencyMs,
@@ -6234,7 +6728,7 @@ async function handleTestModel(req, res, id, cfg) {
6234
6728
  });
6235
6729
  } catch (err5) {
6236
6730
  const message = err5 instanceof Error ? err5.message : String(err5);
6237
- return writeJson3(res, 200, { ok: false, latencyMs: Date.now() - startedAt, message });
6731
+ return writeJson4(res, 200, { ok: false, latencyMs: Date.now() - startedAt, message });
6238
6732
  }
6239
6733
  }
6240
6734
  function extractSampleText(text, apiFormat) {
@@ -6275,7 +6769,7 @@ async function handleProviderKeys(res, id, cfg, deps) {
6275
6769
  const row = cfg.providers.find((p) => p.id === id);
6276
6770
  if (!row) return writeJsonError(res, 404, `provider '${id}' not found`);
6277
6771
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
6278
- return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
6772
+ return writeJson4(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
6279
6773
  }
6280
6774
  function parsePoolKeyInput(body, existing) {
6281
6775
  const out = {};
@@ -6294,7 +6788,7 @@ async function handleAddProviderKey(req, res, id, cfg, deps) {
6294
6788
  if (!id) return writeJsonError(res, 400, "provider id required in path");
6295
6789
  const idx = cfg.providers.findIndex((p) => p.id === id);
6296
6790
  if (idx < 0) return writeJsonError(res, 404, `provider '${id}' not found`);
6297
- const body = await readJsonBody3(req);
6791
+ const body = await readJsonBody4(req);
6298
6792
  const parsed = parsePoolKeyInput(body);
6299
6793
  if (!parsed.apiKey) return writeJsonError(res, 400, "add requires a non-empty apiKey");
6300
6794
  const keyId = `key-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
@@ -6306,7 +6800,7 @@ async function handleAddProviderKey(req, res, id, cfg, deps) {
6306
6800
  row.apiKeys = [...row.apiKeys ?? [], entry];
6307
6801
  persistProviders(cfg, deps);
6308
6802
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
6309
- return writeJson3(res, 201, { keys: toPoolKeyView(row, cooldown, deps) });
6803
+ return writeJson4(res, 201, { keys: toPoolKeyView(row, cooldown, deps) });
6310
6804
  }
6311
6805
  async function handleUpdateProviderKey(req, res, id, keyId, cfg, deps) {
6312
6806
  if (!id) return writeJsonError(res, 400, "provider id required in path");
@@ -6316,7 +6810,7 @@ async function handleUpdateProviderKey(req, res, id, keyId, cfg, deps) {
6316
6810
  const row = cfg.providers[idx];
6317
6811
  const keyIdx = (row.apiKeys ?? []).findIndex((k) => k.id === keyId);
6318
6812
  if (keyIdx < 0) return writeJsonError(res, 404, `pool key '${keyId}' not found`);
6319
- const body = await readJsonBody3(req);
6813
+ const body = await readJsonBody4(req);
6320
6814
  const existing = row.apiKeys[keyIdx];
6321
6815
  const parsed = parsePoolKeyInput(body, existing);
6322
6816
  const entry = { id: keyId, apiKey: parsed.apiKey || existing.apiKey };
@@ -6326,7 +6820,7 @@ async function handleUpdateProviderKey(req, res, id, keyId, cfg, deps) {
6326
6820
  row.apiKeys[keyIdx] = entry;
6327
6821
  persistProviders(cfg, deps);
6328
6822
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
6329
- return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
6823
+ return writeJson4(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
6330
6824
  }
6331
6825
  async function handleDeleteProviderKey(res, id, keyId, cfg, deps) {
6332
6826
  if (!id) return writeJsonError(res, 400, "provider id required in path");
@@ -6340,7 +6834,7 @@ async function handleDeleteProviderKey(res, id, keyId, cfg, deps) {
6340
6834
  if (row.apiKeys.length === 0) row.apiKeys = void 0;
6341
6835
  persistProviders(cfg, deps);
6342
6836
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
6343
- return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
6837
+ return writeJson4(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
6344
6838
  }
6345
6839
  async function handleToggleProviderKey(req, res, id, keyId, cfg, deps) {
6346
6840
  if (!id) return writeJsonError(res, 400, "provider id required in path");
@@ -6350,11 +6844,11 @@ async function handleToggleProviderKey(req, res, id, keyId, cfg, deps) {
6350
6844
  const row = cfg.providers[idx];
6351
6845
  const keyIdx = (row.apiKeys ?? []).findIndex((k) => k.id === keyId);
6352
6846
  if (keyIdx < 0) return writeJsonError(res, 404, `pool key '${keyId}' not found`);
6353
- const body = await readJsonBody3(req);
6847
+ const body = await readJsonBody4(req);
6354
6848
  row.apiKeys[keyIdx].enabled = Boolean(body["enabled"]);
6355
6849
  persistProviders(cfg, deps);
6356
6850
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
6357
- return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
6851
+ return writeJson4(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
6358
6852
  }
6359
6853
  function parseApiKeysInput(raw, existing) {
6360
6854
  if (!Array.isArray(raw)) return existing;
@@ -6540,13 +7034,13 @@ function handlePresets(res, method) {
6540
7034
  website: p.website,
6541
7035
  modelsEndpoint: p.modelsEndpoint
6542
7036
  }));
6543
- return writeJson3(res, 200, { presets, excluded });
7037
+ return writeJson4(res, 200, { presets, excluded });
6544
7038
  }
6545
7039
  async function handleKeys(req, res, method, rest, deps) {
6546
7040
  if (method === "GET" && rest.length === 0) {
6547
7041
  const rows = await deps.keyDb.outboundApiKeysList();
6548
7042
  const reader = deps.keySpendReader;
6549
- if (!reader) return writeJson3(res, 200, { keys: rows.map(toKeyInfo) });
7043
+ if (!reader) return writeJson4(res, 200, { keys: rows.map(toKeyInfo) });
6550
7044
  const now = Date.now();
6551
7045
  const keys = await Promise.all(
6552
7046
  rows.map(async (row) => {
@@ -6558,13 +7052,13 @@ async function handleKeys(req, res, method, rest, deps) {
6558
7052
  return info;
6559
7053
  })
6560
7054
  );
6561
- return writeJson3(res, 200, { keys });
7055
+ return writeJson4(res, 200, { keys });
6562
7056
  }
6563
7057
  if (method === "POST" && rest.length === 0) {
6564
- const body = await readJsonBody3(req);
7058
+ const body = await readJsonBody4(req);
6565
7059
  const name = typeof body["name"] === "string" && body["name"].trim() ? body["name"].trim() : "key";
6566
7060
  const created = await createNamedKey(deps.keyDb, name);
6567
- return writeJson3(res, 201, {
7061
+ return writeJson4(res, 201, {
6568
7062
  id: created.id,
6569
7063
  name: created.name,
6570
7064
  keyPrefix: created.keyPrefix,
@@ -6575,7 +7069,7 @@ async function handleKeys(req, res, method, rest, deps) {
6575
7069
  }
6576
7070
  if (method === "GET" && rest.length === 2 && rest[1] === "reveal") {
6577
7071
  const revealed = await deps.keyDb.outboundApiKeysReveal(rest[0]);
6578
- if (revealed !== null) return writeJson3(res, 200, { key: revealed });
7072
+ if (revealed !== null) return writeJson4(res, 200, { key: revealed });
6579
7073
  const exists = (await deps.keyDb.outboundApiKeysList()).some((r) => r.id === rest[0]);
6580
7074
  if (!exists) return writeJsonError(res, 404, `key '${rest[0]}' not found`);
6581
7075
  return writeJsonError(
@@ -6590,26 +7084,26 @@ async function handleKeys(req, res, method, rest, deps) {
6590
7084
  const bound = await integrationKeyRequirement(deps, id);
6591
7085
  if (bound) return writeJsonError(res, 409, bound);
6592
7086
  const ok = await deps.keyDb.outboundApiKeysRevoke(id);
6593
- return writeJson3(res, ok ? 200 : 404, { ok });
7087
+ return writeJson4(res, ok ? 200 : 404, { ok });
6594
7088
  }
6595
7089
  if (method === "DELETE" && id && !action) {
6596
7090
  const bound = await integrationKeyRequirement(deps, id);
6597
7091
  if (bound) return writeJsonError(res, 409, bound);
6598
7092
  const ok = await deps.keyDb.outboundApiKeysDelete(id);
6599
- return writeJson3(res, ok ? 200 : 404, { ok });
7093
+ return writeJson4(res, ok ? 200 : 404, { ok });
6600
7094
  }
6601
7095
  if (method === "POST" && id && action === "enabled") {
6602
- const body = await readJsonBody3(req);
7096
+ const body = await readJsonBody4(req);
6603
7097
  const enabled = body["enabled"] === true;
6604
7098
  if (!enabled) {
6605
7099
  const bound = await integrationKeyRequirement(deps, id);
6606
7100
  if (bound) return writeJsonError(res, 409, bound);
6607
7101
  }
6608
7102
  const ok = await deps.keyDb.outboundApiKeysSetEnabled(id, enabled);
6609
- return writeJson3(res, ok ? 200 : 404, { ok, enabled });
7103
+ return writeJson4(res, ok ? 200 : 404, { ok, enabled });
6610
7104
  }
6611
7105
  if (method === "POST" && id && action === "permissions") {
6612
- const body = await readJsonBody3(req);
7106
+ const body = await readJsonBody4(req);
6613
7107
  if (Object.keys(body).length !== 1 || !Object.prototype.hasOwnProperty.call(body, "permissions")) {
6614
7108
  return writeJsonError(res, 400, "body must contain only permissions");
6615
7109
  }
@@ -6624,19 +7118,19 @@ async function handleKeys(req, res, method, rest, deps) {
6624
7118
  );
6625
7119
  }
6626
7120
  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 });
7121
+ if (!before) return writeJson4(res, 404, { ok: false });
7122
+ if (before.revokedAt !== null) return writeJson4(res, 409, { ok: false });
6629
7123
  const required = await integrationKeyRequirement(deps, id, permissions);
6630
7124
  if (required) return writeJsonError(res, 409, required);
6631
7125
  const ok = await deps.keyDb.outboundApiKeysSetPermissions(id, permissions);
6632
7126
  if (!ok) {
6633
7127
  const current = (await deps.keyDb.outboundApiKeysList()).find((row) => row.id === id);
6634
- return writeJson3(res, current?.revokedAt !== null ? 409 : 404, { ok: false });
7128
+ return writeJson4(res, current?.revokedAt !== null ? 409 : 404, { ok: false });
6635
7129
  }
6636
- return writeJson3(res, 200, { ok: true, allowedEndpoints: permissions });
7130
+ return writeJson4(res, 200, { ok: true, allowedEndpoints: permissions });
6637
7131
  }
6638
7132
  if (method === "POST" && id && action === "max-concurrency") {
6639
- const body = await readJsonBody3(req);
7133
+ const body = await readJsonBody4(req);
6640
7134
  const raw = body["maxConcurrency"];
6641
7135
  let value;
6642
7136
  if (raw === null) {
@@ -6651,14 +7145,14 @@ async function handleKeys(req, res, method, rest, deps) {
6651
7145
  );
6652
7146
  }
6653
7147
  const ok = await deps.keyDb.outboundApiKeysSetMaxConcurrency(id, value);
6654
- return writeJson3(res, ok ? 200 : 404, { ok, maxConcurrency: value });
7148
+ return writeJson4(res, ok ? 200 : 404, { ok, maxConcurrency: value });
6655
7149
  }
6656
7150
  if (method === "POST" && id && action === "policy") {
6657
- const body = await readJsonBody3(req);
7151
+ const body = await readJsonBody4(req);
6658
7152
  const parsed = parseKeyPolicyBody(body);
6659
7153
  if (!parsed.ok) return writeJsonError(res, 400, parsed.message);
6660
7154
  const ok = await deps.keyDb.outboundApiKeysSetPolicy(id, parsed.policy);
6661
- return writeJson3(res, ok ? 200 : 404, { ok });
7155
+ return writeJson4(res, ok ? 200 : 404, { ok });
6662
7156
  }
6663
7157
  return writeJsonError(res, 405, `method ${method} not allowed on keys`);
6664
7158
  }
@@ -6749,7 +7243,8 @@ function outboundServerConfigInput(config) {
6749
7243
  userMessageQueue: config.userMessageQueue,
6750
7244
  concurrencyQueue: config.concurrencyQueue,
6751
7245
  voucher: config.voucher,
6752
- anthropic: config.anthropic
7246
+ anthropic: config.anthropic,
7247
+ search: config.search
6753
7248
  };
6754
7249
  }
6755
7250
  function projectImagesConfigForAdmin(config) {
@@ -6812,15 +7307,21 @@ function currentImageGenerationId(deps) {
6812
7307
  }
6813
7308
  async function handleServer(req, res, method, deps) {
6814
7309
  if (method === "GET") {
6815
- const config = await loadServerConfig2(deps.settingsStore);
7310
+ const config = await loadServerConfig3(deps.settingsStore);
6816
7311
  let server = config;
6817
7312
  if (config.proxy) server = { ...server, proxy: redactOutboundProxy(config.proxy) };
6818
7313
  if (config.webhook) server = { ...server, webhook: redactWebhookConfig(config.webhook) };
6819
7314
  if (config.billing) server = { ...server, billing: redactBillingConfig(config.billing) };
6820
- return writeJson3(res, 200, { server: projectImagesConfigForAdmin(server) });
7315
+ if (config.search) {
7316
+ server = {
7317
+ ...server,
7318
+ search: redactSearchServerConfig(config.search)
7319
+ };
7320
+ }
7321
+ return writeJson4(res, 200, { server: projectImagesConfigForAdmin(server) });
6821
7322
  }
6822
7323
  if (method === "PUT") {
6823
- const patch = await readJsonBody3(req);
7324
+ const patch = await readJsonBody4(req);
6824
7325
  const queueErrors = validateQueueSegments(patch);
6825
7326
  if (queueErrors.length > 0) {
6826
7327
  return writeJsonError(res, 400, `invalid queue config: ${queueErrors.join("; ")}`);
@@ -6849,7 +7350,7 @@ async function handleServer(req, res, method, deps) {
6849
7350
  if (billingErrors.length > 0) {
6850
7351
  return writeJsonError(res, 400, `invalid billing config: ${billingErrors.join("; ")}`);
6851
7352
  }
6852
- const current = await loadServerConfig2(deps.settingsStore);
7353
+ const current = await loadServerConfig3(deps.settingsStore);
6853
7354
  let effectivePatch = patch;
6854
7355
  if (patch.proxy) {
6855
7356
  effectivePatch = { ...effectivePatch, proxy: preserveOutboundProxySecrets(patch.proxy, current.proxy) };
@@ -6870,6 +7371,20 @@ async function handleServer(req, res, method, deps) {
6870
7371
  }
6871
7372
  effectivePatch = { ...effectivePatch, images };
6872
7373
  }
7374
+ if (patch.search !== void 0) {
7375
+ const searchPatch = preserveSearchSecrets(
7376
+ patch.search,
7377
+ current.search ?? DEFAULT_SEARCH_SERVER_CONFIG2
7378
+ );
7379
+ const searchErrors = validateSearchServerConfig(searchPatch);
7380
+ if (searchErrors.length > 0) {
7381
+ return writeJsonError(res, 400, `invalid search config: ${searchErrors.join("; ")}`);
7382
+ }
7383
+ effectivePatch = {
7384
+ ...effectivePatch,
7385
+ search: searchPatch
7386
+ };
7387
+ }
6873
7388
  const merged = mergeServerConfig(current, effectivePatch);
6874
7389
  const priorImageGenerationId = currentImageGenerationId(deps);
6875
7390
  try {
@@ -6922,7 +7437,11 @@ async function handleServer(req, res, method, deps) {
6922
7437
  } catch {
6923
7438
  }
6924
7439
  }
6925
- return writeJson3(res, 200, { server: projectImagesConfigForAdmin(merged) });
7440
+ const mergedForAdmin = merged.search ? {
7441
+ ...merged,
7442
+ search: redactSearchServerConfig(merged.search)
7443
+ } : merged;
7444
+ return writeJson4(res, 200, { server: projectImagesConfigForAdmin(mergedForAdmin) });
6926
7445
  }
6927
7446
  return writeJsonError(res, 405, `method ${method} not allowed on server`);
6928
7447
  }
@@ -6939,7 +7458,7 @@ async function handleAccounts(req, res, method, rest, deps) {
6939
7458
  sessionKey: query2.get("sessionKey") ?? void 0,
6940
7459
  limit: Number.isFinite(parsedLimit) ? parsedLimit : 100
6941
7460
  });
6942
- return writeJson3(res, 200, {
7461
+ return writeJson4(res, 200, {
6943
7462
  available: true,
6944
7463
  records,
6945
7464
  capacity: ACCOUNT_ROUTE_ACTIVITY_LIMIT,
@@ -6955,7 +7474,7 @@ async function handleAccounts(req, res, method, rest, deps) {
6955
7474
  providerId: query2.get("providerId") ?? void 0,
6956
7475
  accountId: query2.get("accountId") ?? void 0
6957
7476
  });
6958
- return writeJson3(res, 200, {
7477
+ return writeJson4(res, 200, {
6959
7478
  available: true,
6960
7479
  entries,
6961
7480
  collectedAt: Date.now()
@@ -6974,10 +7493,10 @@ async function handleAccounts(req, res, method, rest, deps) {
6974
7493
  const accounts = await deps.subscriptionAccounts.listAll();
6975
7494
  const providerAccounts = await deps.subscriptionTokenWriter.listSanitizedAccounts();
6976
7495
  const externalCli = await deps.subscriptionTokenWriter.listExternalCliAvailability();
6977
- return writeJson3(res, 200, { accounts, providerAccounts, externalCli });
7496
+ return writeJson4(res, 200, { accounts, providerAccounts, externalCli });
6978
7497
  }
6979
7498
  if (method === "POST" && rest[0] === "batch" && rest.length === 1) {
6980
- const body = await readJsonBody3(req);
7499
+ const body = await readJsonBody4(req);
6981
7500
  const parsed = validateAccountBatchBody(body);
6982
7501
  if (!parsed) return writeJsonError(res, 400, "invalid account batch request");
6983
7502
  const result = await deps.subscriptionTokenWriter.batchManageAccounts(parsed.refs, parsed.mutation);
@@ -6993,15 +7512,15 @@ async function handleAccounts(req, res, method, rest, deps) {
6993
7512
  deps.accountAllowanceService?.removeAccountSnapshot?.(ref.providerId, ref.accountId);
6994
7513
  }
6995
7514
  }
6996
- return writeJson3(res, 200, { ok: true, affected: result.affected });
7515
+ return writeJson4(res, 200, { ok: true, affected: result.affected });
6997
7516
  }
6998
7517
  if (method === "GET" && rest[0] === "codex" && rest[1] === "oauth" && rest[3] === "status") {
6999
7518
  const result = handleCodexOAuthStatus(rest[2], deps);
7000
- return writeJson3(res, result.status, result.body);
7519
+ return writeJson4(res, result.status, result.body);
7001
7520
  }
7002
7521
  if (method === "DELETE" && rest[0] === "codex" && rest[1] === "oauth" && rest[2]) {
7003
7522
  const result = handleCodexOAuthCancel(rest[2], deps);
7004
- return writeJson3(res, result.status, result.body);
7523
+ return writeJson4(res, result.status, result.body);
7005
7524
  }
7006
7525
  if (method === "GET" && rest.length === 3 && rest[2] === "diagnostics") {
7007
7526
  const providerId = asSubscriptionProviderId(rest[0]);
@@ -7023,7 +7542,7 @@ async function handleAccounts(req, res, method, rest, deps) {
7023
7542
  resumeAt: entry.resumeAt
7024
7543
  })) ?? [];
7025
7544
  const diagnostics = [...health2, ...allowance].sort((left, right) => right.at - left.at).slice(0, 200);
7026
- return writeJson3(res, 200, { diagnostics });
7545
+ return writeJson4(res, 200, { diagnostics });
7027
7546
  }
7028
7547
  if (method === "GET" && rest.length === 3 && rest[2] === "events") {
7029
7548
  const providerId = asSubscriptionProviderId(rest[0]);
@@ -7035,17 +7554,17 @@ async function handleAccounts(req, res, method, rest, deps) {
7035
7554
  }
7036
7555
  const snapshot = deps.accountProbeService?.getAllHistory().find((entry) => entry.providerId === providerId && entry.accountId === accountId);
7037
7556
  const diagnostics = getSharedAccountHealth().getDiagnostics({ providerId, accountId });
7038
- return writeJson3(res, 200, { events: snapshot?.records ?? [], diagnostics });
7557
+ return writeJson4(res, 200, { events: snapshot?.records ?? [], diagnostics });
7039
7558
  }
7040
7559
  if (method === "PATCH" && rest.length === 2) {
7041
7560
  const providerId = asSubscriptionProviderId(rest[0]);
7042
7561
  if (!providerId) return writeJsonError(res, 400, `unknown subscription provider '${rest[0] ?? ""}'`);
7043
- const body = await readJsonBody3(req);
7562
+ const body = await readJsonBody4(req);
7044
7563
  const patch = validateAccountMetadataPatch(body);
7045
7564
  if (!patch) return writeJsonError(res, 400, "invalid account metadata patch");
7046
7565
  const result = await deps.subscriptionTokenWriter.patchAccountMetadata(providerId, rest[1], patch);
7047
7566
  if (!result.ok) return writeJsonError(res, 404, `account '${rest[1]}' not found`);
7048
- return writeJson3(res, 200, { ok: true });
7567
+ return writeJson4(res, 200, { ok: true });
7049
7568
  }
7050
7569
  if (method === "PUT" || method === "POST" || method === "DELETE") {
7051
7570
  const providerId = asSubscriptionProviderId(rest[0]);
@@ -7054,15 +7573,15 @@ async function handleAccounts(req, res, method, rest, deps) {
7054
7573
  }
7055
7574
  if (method === "POST" && rest[1] === "oauth" && rest[2] === "start") {
7056
7575
  const result = providerId === "codex" ? handleCodexOAuthStart(deps) : handleOAuthStart(providerId, deps);
7057
- return writeJson3(res, result.status, result.body);
7576
+ return writeJson4(res, result.status, result.body);
7058
7577
  }
7059
7578
  if (method === "POST" && rest[1] === "oauth" && rest[2] === "complete") {
7060
- const body2 = await readJsonBody3(req);
7579
+ const body2 = await readJsonBody4(req);
7061
7580
  const result = await handleOAuthComplete(providerId, body2, deps);
7062
- return writeJson3(res, result.status, result.body);
7581
+ return writeJson4(res, result.status, result.body);
7063
7582
  }
7064
7583
  if (method === "POST" && rest[1] === "accounts") {
7065
- const body2 = await readJsonBody3(req);
7584
+ const body2 = await readJsonBody4(req);
7066
7585
  const block = validateTokenBody(providerId, body2);
7067
7586
  if (!block) {
7068
7587
  return writeJsonError(res, 400, `malformed token body for provider '${providerId}'`);
@@ -7070,20 +7589,20 @@ async function handleAccounts(req, res, method, rest, deps) {
7070
7589
  const label = typeof body2["label"] === "string" && body2["label"].trim() ? body2["label"].trim() : void 0;
7071
7590
  await deps.subscriptionAccountAppender.appendProviderAccount(providerId, block, label);
7072
7591
  const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
7073
- return writeJson3(res, 200, status2 ? { account: status2 } : { ok: true });
7592
+ return writeJson4(res, 200, status2 ? { account: status2 } : { ok: true });
7074
7593
  }
7075
7594
  if (method === "POST" && rest[1] === "import-external") {
7076
7595
  if (providerId !== "claude" && providerId !== "codex") {
7077
7596
  return writeJsonError(res, 400, `provider '${providerId}' has no external CLI store`);
7078
7597
  }
7079
- const body2 = await readJsonBody3(req);
7598
+ const body2 = await readJsonBody4(req);
7080
7599
  const label = typeof body2["label"] === "string" && body2["label"].trim() ? body2["label"].trim() : void 0;
7081
7600
  const result = await deps.subscriptionTokenWriter.importExternalCliAccount(providerId, label);
7082
7601
  if (!result.ok) {
7083
7602
  return writeJsonError(res, 409, `no usable external ${providerId} CLI credential found`);
7084
7603
  }
7085
7604
  const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
7086
- return writeJson3(res, 200, {
7605
+ return writeJson4(res, 200, {
7087
7606
  ok: true,
7088
7607
  account: status2 ?? void 0,
7089
7608
  nativeCredentialMode: result.nativeCredentialMode,
@@ -7098,7 +7617,7 @@ async function handleAccounts(req, res, method, rest, deps) {
7098
7617
  const writer2 = deps.subscriptionTokenWriter;
7099
7618
  const ok = providerId === "claude" ? await writer2.refreshClaudeToken() : providerId === "codex" ? await writer2.refreshCodexToken() : await writer2.refreshGeminiToken();
7100
7619
  const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
7101
- return writeJson3(res, 200, { ok, account: status2 ?? void 0 });
7620
+ return writeJson4(res, 200, { ok, account: status2 ?? void 0 });
7102
7621
  }
7103
7622
  if (method === "POST" && rest.length === 3 && rest[2] === "test") {
7104
7623
  const accountId = rest[1];
@@ -7108,7 +7627,7 @@ async function handleAccounts(req, res, method, rest, deps) {
7108
7627
  return writeJsonError(res, 404, `account '${accountId}' not found`);
7109
7628
  }
7110
7629
  const result = await deps.accountProbeService.testAccountConnection(providerId, accountId);
7111
- return writeJson3(res, 200, {
7630
+ return writeJson4(res, 200, {
7112
7631
  ok: result.ok,
7113
7632
  marked: result.marked,
7114
7633
  tier: result.tier,
@@ -7117,15 +7636,15 @@ async function handleAccounts(req, res, method, rest, deps) {
7117
7636
  }
7118
7637
  if (method === "POST" && rest[2] === "label") {
7119
7638
  const accountId = rest[1];
7120
- const body2 = await readJsonBody3(req);
7639
+ const body2 = await readJsonBody4(req);
7121
7640
  const label = typeof body2["label"] === "string" ? body2["label"].trim() : "";
7122
7641
  const result = await deps.subscriptionTokenWriter.renameAccount(providerId, accountId, label);
7123
7642
  if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
7124
- return writeJson3(res, 200, { ok: true });
7643
+ return writeJson4(res, 200, { ok: true });
7125
7644
  }
7126
7645
  if (method === "POST" && rest[2] === "priority") {
7127
7646
  const accountId = rest[1];
7128
- const body2 = await readJsonBody3(req);
7647
+ const body2 = await readJsonBody4(req);
7129
7648
  const raw = body2["priority"];
7130
7649
  const priority = typeof raw === "number" ? raw : Number(raw);
7131
7650
  if (!Number.isFinite(priority)) {
@@ -7133,11 +7652,11 @@ async function handleAccounts(req, res, method, rest, deps) {
7133
7652
  }
7134
7653
  const result = await deps.subscriptionTokenWriter.setAccountPriority(providerId, accountId, priority);
7135
7654
  if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
7136
- return writeJson3(res, 200, { ok: true });
7655
+ return writeJson4(res, 200, { ok: true });
7137
7656
  }
7138
7657
  if (method === "POST" && rest[2] === "proxy") {
7139
7658
  const accountId = rest[1];
7140
- const body2 = await readJsonBody3(req);
7659
+ const body2 = await readJsonBody4(req);
7141
7660
  const rawProxy = body2["proxy"];
7142
7661
  let proxy;
7143
7662
  if (rawProxy !== null && rawProxy !== void 0) {
@@ -7146,63 +7665,63 @@ async function handleAccounts(req, res, method, rest, deps) {
7146
7665
  }
7147
7666
  const result = await deps.subscriptionTokenWriter.setAccountProxy(providerId, accountId, proxy);
7148
7667
  if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
7149
- return writeJson3(res, 200, { ok: true });
7668
+ return writeJson4(res, 200, { ok: true });
7150
7669
  }
7151
7670
  if (method === "POST" && rest[2] === "supported-models") {
7152
7671
  const accountId = rest[1];
7153
- const body2 = await readJsonBody3(req);
7672
+ const body2 = await readJsonBody4(req);
7154
7673
  const parsed = validateSupportedModelsBody(body2["supportedModels"]);
7155
7674
  if (!parsed.ok) return writeJsonError(res, 400, "invalid supportedModels");
7156
7675
  const result = await deps.subscriptionTokenWriter.setAccountSupportedModels(providerId, accountId, parsed.value);
7157
7676
  if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
7158
- return writeJson3(res, 200, { ok: true });
7677
+ return writeJson4(res, 200, { ok: true });
7159
7678
  }
7160
7679
  if (method === "PUT" && rest[1] === "active") {
7161
- const body2 = await readJsonBody3(req);
7680
+ const body2 = await readJsonBody4(req);
7162
7681
  const id = typeof body2["id"] === "string" ? body2["id"] : "";
7163
7682
  if (!id) return writeJsonError(res, 400, "active switch requires { id }");
7164
7683
  const result = await deps.subscriptionTokenWriter.setActiveAccount(providerId, id);
7165
7684
  if (!result.ok) return writeJsonError(res, 404, `account '${id}' not found`);
7166
- return writeJson3(res, 200, { ok: true });
7685
+ return writeJson4(res, 200, { ok: true });
7167
7686
  }
7168
7687
  if (method === "DELETE" && rest.length === 2) {
7169
7688
  const accountId = rest[1];
7170
7689
  const result = await deps.subscriptionTokenWriter.removeAccount(providerId, accountId);
7171
7690
  if (!result.removed) return writeJsonError(res, 404, `account '${accountId}' not found`);
7172
7691
  deps.accountAllowanceService?.removeAccountSnapshot?.(providerId, accountId);
7173
- return writeJson3(res, 200, { ok: true });
7692
+ return writeJson4(res, 200, { ok: true });
7174
7693
  }
7175
7694
  if (method === "DELETE" && rest.length === 1) {
7176
7695
  await deps.subscriptionTokenWriter.clearProvider(providerId);
7177
7696
  deps.accountAllowanceService?.removeProviderSnapshots?.(providerId);
7178
- return writeJson3(res, 200, { ok: true });
7697
+ return writeJson4(res, 200, { ok: true });
7179
7698
  }
7180
7699
  if (method === "DELETE") {
7181
7700
  return writeJsonError(res, 405, "method DELETE not allowed on this accounts path");
7182
7701
  }
7183
- const body = await readJsonBody3(req);
7702
+ const body = await readJsonBody4(req);
7184
7703
  const config = validateTokenBody(providerId, body);
7185
7704
  if (!config) {
7186
7705
  return writeJsonError(res, 400, `malformed token body for provider '${providerId}'`);
7187
7706
  }
7188
7707
  await deps.subscriptionTokenWriter.writeProviderTokens(providerId, config);
7189
7708
  const status = await statusEntryFor(deps.subscriptionAccounts, providerId);
7190
- return writeJson3(res, 200, status ? { account: status } : { ok: true });
7709
+ return writeJson4(res, 200, status ? { account: status } : { ok: true });
7191
7710
  }
7192
7711
  return writeJsonError(res, 405, `method ${method} not allowed on accounts`);
7193
7712
  }
7194
7713
  async function handleCli(req, res, method, rest, deps) {
7195
7714
  if (method === "GET" && rest.length === 0) {
7196
7715
  const result = handleCliList(process.platform, deps.cliPathProbe);
7197
- return writeJson3(res, result.status, result.body);
7716
+ return writeJson4(res, result.status, result.body);
7198
7717
  }
7199
7718
  if (method === "GET" && rest[0] === "sessions") {
7200
7719
  const result = handleCliSessions();
7201
- return writeJson3(res, result.status, result.body);
7720
+ return writeJson4(res, result.status, result.body);
7202
7721
  }
7203
7722
  if (method === "DELETE" && rest[0] === "sessions" && rest[1]) {
7204
7723
  const result = handleCliStop(rest[1]);
7205
- return writeJson3(res, result.status, result.body);
7724
+ return writeJson4(res, result.status, result.body);
7206
7725
  }
7207
7726
  if (method === "POST" && rest[1] === "install") {
7208
7727
  const cli = rest[0];
@@ -7210,14 +7729,14 @@ async function handleCli(req, res, method, rest, deps) {
7210
7729
  return writeJsonError(res, 400, `unknown cli '${cli ?? ""}'`);
7211
7730
  }
7212
7731
  const result = await handleCliInstall(cli, deps.cliCommandRunner);
7213
- return writeJson3(res, result.status, result.body);
7732
+ return writeJson4(res, result.status, result.body);
7214
7733
  }
7215
7734
  if (method === "POST" && rest[1] === "launch") {
7216
7735
  const cli = rest[0];
7217
7736
  if (!isLaunchCliId(cli)) {
7218
7737
  return writeJsonError(res, 400, `unknown cli '${cli ?? ""}'`);
7219
7738
  }
7220
- const body = await readJsonBody3(req);
7739
+ const body = await readJsonBody4(req);
7221
7740
  const providers = loadConfig(deps.configPath).providers ?? [];
7222
7741
  const result = await handleCliLaunch(cli, body, {
7223
7742
  llmConfig: deps.llmConfig,
@@ -7226,7 +7745,7 @@ async function handleCli(req, res, method, rest, deps) {
7226
7745
  opener: deps.cliTerminalOpener,
7227
7746
  probe: deps.cliPathProbe
7228
7747
  });
7229
- return writeJson3(res, result.status, result.body);
7748
+ return writeJson4(res, result.status, result.body);
7230
7749
  }
7231
7750
  return writeJsonError(res, 405, `method ${method} not allowed on cli`);
7232
7751
  }
@@ -7236,52 +7755,52 @@ async function handleIntegrations(req, res, method, rest, deps) {
7236
7755
  const manager = factory();
7237
7756
  try {
7238
7757
  if (method === "GET" && rest.length === 0) {
7239
- return writeJson3(res, 200, {
7758
+ return writeJson4(res, 200, {
7240
7759
  integrations: await manager.listStatus(),
7241
7760
  gateway: deps.outboundApiServer.getStatus()
7242
7761
  });
7243
7762
  }
7244
7763
  if (method === "POST" && rest.length === 1 && rest[0] === "rotate") {
7245
7764
  await manager.rotateGatewayKey();
7246
- return writeJson3(res, 200, { ok: true, integrations: await manager.listStatus() });
7765
+ return writeJson4(res, 200, { ok: true, integrations: await manager.listStatus() });
7247
7766
  }
7248
7767
  const client = rest[0];
7249
7768
  if (!isIntegrationClient(client)) {
7250
7769
  return writeJsonError(res, 400, `unknown integration client '${client ?? ""}'`);
7251
7770
  }
7252
7771
  if (method === "POST" && rest[1] === "key") {
7253
- const body = await readJsonBody3(req);
7772
+ const body = await readJsonBody4(req);
7254
7773
  if (Object.keys(body).length !== 1 || typeof body.keyId !== "string" || !body.keyId.trim()) {
7255
7774
  return writeJsonError(res, 400, "body must contain a non-empty keyId string");
7256
7775
  }
7257
7776
  const status = await manager.bindIntegrationKey(client, body.keyId.trim());
7258
- return writeJson3(res, 200, { integration: status });
7777
+ return writeJson4(res, 200, { integration: status });
7259
7778
  }
7260
7779
  if (method === "POST" && rest[1] === "plan") {
7261
- const body = await readJsonBody3(req);
7780
+ const body = await readJsonBody4(req);
7262
7781
  const configPath = body.configPath;
7263
7782
  if (configPath !== void 0 && typeof configPath !== "string") {
7264
7783
  return writeJsonError(res, 400, "configPath must be a string");
7265
7784
  }
7266
7785
  const plan = await manager.plan(client, configPath);
7267
- return writeJson3(res, 200, { plan });
7786
+ return writeJson4(res, 200, { plan });
7268
7787
  }
7269
7788
  if (method === "POST" && (rest[1] === "install" || rest[1] === "apply")) {
7270
- const body = await readJsonBody3(req);
7789
+ const body = await readJsonBody4(req);
7271
7790
  const configPath = body.configPath;
7272
7791
  if (configPath !== void 0 && typeof configPath !== "string") {
7273
7792
  return writeJsonError(res, 400, "configPath must be a string");
7274
7793
  }
7275
7794
  const status = await manager.install(client, configPath);
7276
- return writeJson3(res, 200, { integration: status });
7795
+ return writeJson4(res, 200, { integration: status });
7277
7796
  }
7278
7797
  if (method === "POST" && rest[1] === "repair") {
7279
7798
  const status = await manager.repair(client);
7280
- return writeJson3(res, 200, { integration: status });
7799
+ return writeJson4(res, 200, { integration: status });
7281
7800
  }
7282
7801
  if (method === "DELETE" && rest.length === 1 || method === "POST" && rest[1] === "remove") {
7283
7802
  const status = await manager.remove(client);
7284
- return writeJson3(res, 200, { integration: status });
7803
+ return writeJson4(res, 200, { integration: status });
7285
7804
  }
7286
7805
  return writeJsonError(res, 405, `method ${method} not allowed on integrations`);
7287
7806
  } catch (error) {
@@ -7424,7 +7943,7 @@ async function handleImages(res, method, rest, deps) {
7424
7943
  }
7425
7944
  const reader = deps.imageRuntimeStatus;
7426
7945
  if (!reader) return writeJsonError(res, 501, "Images runtime status is not available");
7427
- const serverConfig = await loadServerConfig2(deps.settingsStore);
7946
+ const serverConfig = await loadServerConfig3(deps.settingsStore);
7428
7947
  const images = serverConfig.images ?? DEFAULT_IMAGES_SERVER_CONFIG;
7429
7948
  const lifecycle = reader.status();
7430
7949
  const capability = await reader.inspectCapability(IMAGE_ADMIN_STATUS_TENANT);
@@ -7444,7 +7963,7 @@ async function handleImages(res, method, rest, deps) {
7444
7963
  httpLeases: safeStatusCount(generation.httpLeases),
7445
7964
  hostedLeases: safeStatusCount(generation.hostedLeases)
7446
7965
  }));
7447
- return writeJson3(res, 200, {
7966
+ return writeJson4(res, 200, {
7448
7967
  configured: {
7449
7968
  enabled: images.enabled,
7450
7969
  provider: images.provider,
@@ -7472,7 +7991,7 @@ async function handleImages(res, method, rest, deps) {
7472
7991
  async function handleStatus(res, method, deps) {
7473
7992
  if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on status`);
7474
7993
  const status = deps.outboundApiServer.getStatus();
7475
- const serverConfig = await loadServerConfig2(deps.settingsStore);
7994
+ const serverConfig = await loadServerConfig3(deps.settingsStore);
7476
7995
  const endpoints = ["chat", "responses", "messages", "gemini"].map((endpoint) => {
7477
7996
  const routes = (serverConfig.bindings ?? []).filter((binding) => binding.enabled && binding.endpoint === endpoint).map((binding) => gatewayBindingToEndpointConfig(binding));
7478
7997
  const useSubscription = routes.some((route) => route.useSubscription);
@@ -7510,14 +8029,14 @@ async function handleStatus(res, method, deps) {
7510
8029
  })() : void 0;
7511
8030
  if (status.running) {
7512
8031
  const queueStatus = deps.outboundApiServer.getQueueStatus();
7513
- return writeJson3(res, 200, {
8032
+ return writeJson4(res, 200, {
7514
8033
  ...status,
7515
8034
  endpoints,
7516
8035
  queueStatus,
7517
8036
  ...imageRuntime ? { imageRuntime } : {}
7518
8037
  });
7519
8038
  }
7520
- return writeJson3(res, 200, {
8039
+ return writeJson4(res, 200, {
7521
8040
  ...status,
7522
8041
  endpoints,
7523
8042
  ...imageRuntime ? { imageRuntime } : {}
@@ -7541,18 +8060,18 @@ function resolvePlaygroundPath(endpoint, body) {
7541
8060
  }
7542
8061
  async function handlePlayground(req, res, method, deps) {
7543
8062
  if (method !== "POST") return writeJsonError(res, 405, `method ${method} not allowed on playground`);
7544
- const body = await readJsonBody3(req);
8063
+ const body = await readJsonBody4(req);
7545
8064
  const endpoint = typeof body["endpoint"] === "string" ? body["endpoint"] : "";
7546
8065
  const key = typeof body["key"] === "string" ? body["key"] : "";
7547
8066
  const payload = body["body"];
7548
8067
  const status = deps.outboundApiServer.getStatus();
7549
8068
  if (!status.running || !status.port) return writeJsonError(res, 503, "outbound server not running");
7550
- const path2 = resolvePlaygroundPath(endpoint, isRecord2(payload) ? payload : {});
8069
+ const path2 = resolvePlaygroundPath(endpoint, isRecord3(payload) ? payload : {});
7551
8070
  if (!path2) return writeJsonError(res, 400, `unknown endpoint '${endpoint}'`);
7552
8071
  const upstreamBody = typeof payload === "string" ? payload : JSON.stringify(payload ?? {});
7553
8072
  await proxyToOutbound(res, status.port, path2, key, upstreamBody);
7554
8073
  }
7555
- function isRecord2(v) {
8074
+ function isRecord3(v) {
7556
8075
  return !!v && typeof v === "object" && !Array.isArray(v);
7557
8076
  }
7558
8077
  function proxyToOutbound(res, outboundPort, path2, key, body) {
@@ -7687,7 +8206,7 @@ async function handleUiStatic(req, res, urlPath, uiDist) {
7687
8206
  }
7688
8207
 
7689
8208
  // src/admin/version.ts
7690
- var DAEMON_VERSION = true ? "0.2.0" : "0.0.0-dev";
8209
+ var DAEMON_VERSION = true ? "0.2.1" : "0.0.0-dev";
7691
8210
 
7692
8211
  // src/admin/AdminServer.ts
7693
8212
  var LOOPBACK_ADDR = "127.0.0.1";
@@ -17582,6 +18101,14 @@ function buildDaemon(config, paths) {
17582
18101
  onEvent: (row, at) => usageThroughput.record(row, at)
17583
18102
  });
17584
18103
  const initialImagesConfig = normalizeServerConfig2(decryptedConfig.server).images;
18104
+ const initialSearchConfig = normalizeServerConfig2(decryptedConfig.server).search;
18105
+ for (const issue of validateSearchServerConfig2(
18106
+ decryptedConfig.server?.search
18107
+ )) {
18108
+ logger.warn("[search] ignoring invalid config: " + issue);
18109
+ }
18110
+ const searchRuntime = buildSearchRuntime(initialSearchConfig, { logger });
18111
+ const searchFrontendModes = initialSearchConfig.modes;
17585
18112
  const imageObservability = new ImageObservability();
17586
18113
  const imageRuntimeObservability = Object.freeze({
17587
18114
  telemetrySink: imageObservability.telemetrySink,
@@ -17711,7 +18238,9 @@ function buildDaemon(config, paths) {
17711
18238
  apiKeyPool,
17712
18239
  usageRecorder,
17713
18240
  openAIOperationRegistry,
17714
- responsesHostedImageIngress
18241
+ responsesHostedImageIngress,
18242
+ searchRuntime,
18243
+ searchFrontendModes
17715
18244
  });
17716
18245
  if (providerProxy.getDeps().openAIOperationRegistry !== openAIOperationRegistry) {
17717
18246
  throw new Error(
@@ -17776,7 +18305,9 @@ function buildDaemon(config, paths) {
17776
18305
  keySpendTracker,
17777
18306
  // configurable-logging: route the server's OWN lifecycle + relay dispatch-error
17778
18307
  // lines through the injected logger (honors level/format/file sink).
17779
- logger
18308
+ logger,
18309
+ // plan 阶段5: the same instance the managed frontends hold.
18310
+ searchRuntime
17780
18311
  });
17781
18312
  const auditDir = defaultAuditDir(paths.configPath);
17782
18313
  const billingDir = defaultBillingDir(paths.configPath);
@@ -17798,6 +18329,10 @@ function buildDaemon(config, paths) {
17798
18329
  }),
17799
18330
  routeLeaseManager,
17800
18331
  subscriptionAccounts,
18332
+ // search-settings-ui D3: the daemon's ONE search runtime + its
18333
+ // bootstrap-captured modes, for `GET /admin/api/search/diagnostics` and
18334
+ // `POST /admin/api/search/test` (501 when a light embedder omits it).
18335
+ searchStatus: { runtime: searchRuntime, modes: searchFrontendModes },
17801
18336
  accountAllowanceService,
17802
18337
  allowanceRefreshScheduler: claudeAllowanceRefreshScheduler,
17803
18338
  accountProbeService: accountHealthProbeScheduler,
@@ -17934,6 +18469,8 @@ function buildDaemon(config, paths) {
17934
18469
  keyDb,
17935
18470
  settingsStore,
17936
18471
  openAIOperationRegistry,
18472
+ searchRuntime,
18473
+ searchFrontendModes,
17937
18474
  imageRuntimeManager,
17938
18475
  imageObservability,
17939
18476
  imageCleanupService,
@@ -18080,6 +18617,116 @@ async function runImagesLiveDoctor(config, doctor, signal = new AbortController(
18080
18617
  );
18081
18618
  return true;
18082
18619
  }
18620
+ function readSearchApiConfigFromEnv(env = process.env) {
18621
+ const configs = {};
18622
+ const read = (name) => {
18623
+ const value = env[`OMNICROSS_SEARCH_${name}`]?.trim();
18624
+ return value ? value : void 0;
18625
+ };
18626
+ const tavilyKey = read("TAVILY_API_KEY");
18627
+ if (tavilyKey) {
18628
+ configs.tavily = { apiKey: tavilyKey, ...optionalHost(read("TAVILY_API_HOST")) };
18629
+ }
18630
+ const jinaKey = read("JINA_API_KEY");
18631
+ const jinaHost = read("JINA_API_HOST");
18632
+ if (jinaKey || jinaHost) {
18633
+ configs.jina = { ...jinaKey ? { apiKey: jinaKey } : {}, ...optionalHost(jinaHost) };
18634
+ }
18635
+ const searxngHost = read("SEARXNG_API_HOST");
18636
+ if (searxngHost) {
18637
+ const username = read("SEARXNG_BASIC_AUTH_USERNAME");
18638
+ const password = read("SEARXNG_BASIC_AUTH_PASSWORD");
18639
+ configs.searxng = {
18640
+ apiHost: searxngHost,
18641
+ ...username ? { basicAuthUsername: username } : {},
18642
+ ...password ? { basicAuthPassword: password } : {}
18643
+ };
18644
+ }
18645
+ const zhipuKey = read("ZHIPU_API_KEY");
18646
+ if (zhipuKey) {
18647
+ configs.zhipu = { apiKey: zhipuKey, ...optionalHost(read("ZHIPU_API_HOST")) };
18648
+ }
18649
+ const zaiKey = read("Z_AI_API_KEY");
18650
+ if (zaiKey) {
18651
+ configs["z.ai"] = { apiKey: zaiKey, ...optionalHost(read("Z_AI_API_HOST")) };
18652
+ }
18653
+ return configs;
18654
+ }
18655
+ function optionalHost(apiHost) {
18656
+ return apiHost ? { apiHost } : {};
18657
+ }
18658
+ function resolveSearchApiConfigs(configured, env = process.env) {
18659
+ const fromEnv = readSearchApiConfigFromEnv(env);
18660
+ const resolved = {};
18661
+ const tavily = configured?.tavily ?? fromEnv.tavily;
18662
+ if (tavily) resolved.tavily = tavily;
18663
+ const jina = configured?.jina ?? fromEnv.jina;
18664
+ if (jina) resolved.jina = jina;
18665
+ const searxng = configured?.searxng ?? fromEnv.searxng;
18666
+ if (searxng) resolved.searxng = searxng;
18667
+ const zhipu = configured?.zhipu ?? fromEnv.zhipu;
18668
+ if (zhipu) resolved.zhipu = zhipu;
18669
+ const zai = configured?.["z.ai"] ?? fromEnv["z.ai"];
18670
+ if (zai) resolved["z.ai"] = zai;
18671
+ return resolved;
18672
+ }
18673
+ function formatCapabilities(capabilities) {
18674
+ return [
18675
+ `apiKey=${capabilities.requiresApiKey}`,
18676
+ `cancellation=${capabilities.supportsCancellation}`,
18677
+ `urlRead=${capabilities.supportsUrlRead}`,
18678
+ `region=${capabilities.supportsRegion}`,
18679
+ `language=${capabilities.supportsLanguage}`,
18680
+ `timeRange=${capabilities.supportsTimeRange}`,
18681
+ `maxResults=${capabilities.maxResults ?? "unbounded"}`
18682
+ ].join(", ");
18683
+ }
18684
+ function formatDiagnostic(diagnostic) {
18685
+ const parts = [diagnostic.status];
18686
+ if (diagnostic.reason) parts.push(diagnostic.reason);
18687
+ if (diagnostic.error) {
18688
+ const { code, details } = diagnostic.error;
18689
+ parts.push(
18690
+ `code=${code}, transport=${details?.transport ?? "unknown"}, stage=${details?.stage ?? "unknown"}`
18691
+ );
18692
+ }
18693
+ return parts.join(" \u2014 ");
18694
+ }
18695
+ async function runSearchDoctor(live, env = process.env, source = {}) {
18696
+ const apiConfigs = resolveSearchApiConfigs(source.config?.providers, env);
18697
+ const egressPolicy = source.config && source.config.egress.allowedPrivateHosts.length > 0 ? { allowedPrivateHosts: [...source.config.egress.allowedPrivateHosts] } : void 0;
18698
+ const contributions = [
18699
+ ...builtinHttpSearchContributions4(),
18700
+ ...apiSearchContributions3(apiConfigs, { ...egressPolicy ? { egressPolicy } : {} })
18701
+ ];
18702
+ const declarations = source.runtime?.listProviders() ?? contributions;
18703
+ console.info("omnicross doctor search \u2014 builtin search contributions (offline, no network)");
18704
+ const modes = source.config?.modes ?? DEFAULT_SEARCH_FRONTEND_MODES;
18705
+ console.info(
18706
+ ` [i] frontend modes: ${SEARCH_FRONTEND_NAMES.map((name) => `${name}=${modes[name]}`).join(", ")}`
18707
+ );
18708
+ console.info(
18709
+ " [i] codex mode applies immediately; responses/anthropic modes, provider config, egress allowlist and policy apply on daemon restart"
18710
+ );
18711
+ for (const row of buildSearchDoctorSnapshot(declarations, apiConfigs)) {
18712
+ const mark = row.status === "unconfigured" ? "\u2013" : "\u2713";
18713
+ const suffix = row.status ? ` \u2014 ${row.status}: ${row.reason ?? ""}` : "";
18714
+ console.info(
18715
+ ` [${mark}] ${row.providerId}: source=${row.source}, kind=${row.kind}, ${formatCapabilities(row.capabilities)}${suffix}`
18716
+ );
18717
+ }
18718
+ if (!live) return 0;
18719
+ console.info(
18720
+ ` [\u26A0] live search sends ONE fixed public query per provider ("${SEARCH_DOCTOR_QUERY}") to the engine itself`
18721
+ );
18722
+ let hardFailure = false;
18723
+ for (const diagnostic of await runSearchLiveChecks(contributions)) {
18724
+ const mark = diagnostic.status === "healthy" ? "\u2713" : diagnostic.status === "failed" ? "\u2717" : "\u26A0";
18725
+ if (diagnostic.status === "failed") hardFailure = true;
18726
+ console.info(` [${mark}] ${diagnostic.providerId}: ${formatDiagnostic(diagnostic)}`);
18727
+ }
18728
+ return hardFailure ? 1 : 0;
18729
+ }
18083
18730
  async function runLiveProbe(url, key, fetchImpl = fetch) {
18084
18731
  try {
18085
18732
  const res = await fetchImpl(`${url.replace(/\/+$/, "")}/v1/messages/count_tokens`, {
@@ -18116,11 +18763,12 @@ async function runDoctor(argv, fetchImpl = fetch) {
18116
18763
  allowPositionals: true
18117
18764
  });
18118
18765
  const subject = positionals[0] ?? "claude";
18119
- if (subject !== "claude" && subject !== "images") {
18120
- throw new Error(`doctor: unknown subject '${subject}' (supported: 'claude', 'images')`);
18766
+ if (subject !== "claude" && subject !== "images" && subject !== "search") {
18767
+ throw new Error(`doctor: unknown subject '${subject}' (supported: 'claude', 'images', 'search')`);
18121
18768
  }
18122
18769
  const configPath = values.config;
18123
18770
  if (!configPath) {
18771
+ if (subject === "search") return runSearchDoctor(values.live === true);
18124
18772
  throw new Error("doctor: --config <path> is required (the same config `omnicross start` uses)");
18125
18773
  }
18126
18774
  const config = loadConfig(configPath);
@@ -18132,7 +18780,13 @@ async function runDoctor(argv, fetchImpl = fetch) {
18132
18780
  };
18133
18781
  const daemon = await buildDaemon(config, paths);
18134
18782
  try {
18135
- const serverConfig = await loadServerConfig3(daemon.settingsStore);
18783
+ const serverConfig = await loadServerConfig4(daemon.settingsStore);
18784
+ if (subject === "search") {
18785
+ return await runSearchDoctor(values.live === true, process.env, {
18786
+ ...serverConfig.search ? { config: serverConfig.search } : {},
18787
+ runtime: daemon.searchRuntime
18788
+ });
18789
+ }
18136
18790
  const checks = subject === "images" ? buildImagesDoctorChecks(await daemon.imageDoctor.inspectLocal(
18137
18791
  serverConfig.images ?? DEFAULT_IMAGES_SERVER_CONFIG2
18138
18792
  )) : buildClaudeDoctorChecks(serverConfig);
@@ -19209,7 +19863,7 @@ function tokensSuffix(configPath) {
19209
19863
 
19210
19864
  // src/commands/start.ts
19211
19865
  import { parseArgs as parseArgs10 } from "util";
19212
- import { loadServerConfig as loadServerConfig4 } from "@omnicross/core/outbound-api";
19866
+ import { loadServerConfig as loadServerConfig5 } from "@omnicross/core/outbound-api";
19213
19867
  import { getSharedAccountHealth as getSharedAccountHealth5 } from "@omnicross/core/pipeline/SubscriptionAccountHealth";
19214
19868
  import { getSharedAccountAllowanceScheduling as getSharedAccountAllowanceScheduling6 } from "@omnicross/core/pipeline/AccountAllowanceScheduling";
19215
19869
 
@@ -19271,7 +19925,7 @@ async function runStart(argv) {
19271
19925
  await daemon.llmConfig.ready();
19272
19926
  await daemon.migrateUsageStore();
19273
19927
  await daemon.providerProxy.start();
19274
- const serverConfig = await loadServerConfig4(daemon.settingsStore);
19928
+ const serverConfig = await loadServerConfig5(daemon.settingsStore);
19275
19929
  await daemon.imageCleanupService.runOnce();
19276
19930
  daemon.imageCleanupService.start();
19277
19931
  getSharedAccountHealth5().configure({
@@ -19294,7 +19948,9 @@ async function runStart(argv) {
19294
19948
  voucher: serverConfig.voucher,
19295
19949
  // claude-api-protocol-fidelity (§10): count_tokens strategy/budget,
19296
19950
  // /v1/models shape, synthetic-ping heartbeat (hot-applied inside applyConfig).
19297
- anthropic: serverConfig.anthropic
19951
+ anthropic: serverConfig.anthropic,
19952
+ // plan 阶段5: the Codex frontend mode is read live per request from here.
19953
+ search: serverConfig.search
19298
19954
  });
19299
19955
  let dashboardUrl = null;
19300
19956
  if (!values["no-dashboard"]) {
@@ -19409,6 +20065,16 @@ Usage:
19409
20065
  Check local Images config, roots, stores, permissions,
19410
20066
  account and cached evidence. --live warns, then consumes
19411
20067
  at most one minimal subscription image request.
20068
+ omnicross doctor search [--live] List the builtin search providers and their declared
20069
+ capabilities (offline, no config needed). --live sends ONE
20070
+ fixed public query per provider and reports healthy /
20071
+ degraded / blocked / failed.
20072
+ Keyed API providers show as unconfigured unless enabled
20073
+ through the DIAGNOSTIC-ONLY environment variables
20074
+ OMNICROSS_SEARCH_{TAVILY,JINA,SEARXNG,ZHIPU,Z_AI}_API_KEY
20075
+ / _API_HOST (plus SEARXNG_BASIC_AUTH_USERNAME/_PASSWORD).
20076
+ These are read only by this command and are not the
20077
+ configuration system.
19412
20078
  `;
19413
20079
  async function main() {
19414
20080
  const [, , subcommand, ...rest] = process.argv;