@bitkyc08/opencodex 2.49.0 → 2.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/AGENTS_INSTALL.md +9 -1
  2. package/README.md +3 -0
  3. package/gui/dist/assets/index-C39tnjXO.js +115 -0
  4. package/gui/dist/index.html +1 -1
  5. package/package.json +1 -1
  6. package/src/claude/inbound.ts +17 -5
  7. package/src/cli/account-api.ts +18 -3
  8. package/src/cli/account-auth.ts +8 -1
  9. package/src/cli/account-extended.ts +2 -1
  10. package/src/cli/account.ts +1 -0
  11. package/src/cli/capabilities.ts +15 -1
  12. package/src/cli/index.ts +5 -1
  13. package/src/cli/models-runtime.ts +8 -3
  14. package/src/cli/observe.ts +13 -3
  15. package/src/clients/config-export/zcode.ts +24 -0
  16. package/src/codex/account-runtime-state.ts +6 -1
  17. package/src/codex/account-store.ts +72 -9
  18. package/src/codex/account-usability.ts +3 -2
  19. package/src/codex/auth-api.ts +107 -23
  20. package/src/codex/auth-context.ts +21 -0
  21. package/src/codex/catalog/parsing.ts +23 -0
  22. package/src/codex/catalog/provider-fetch.ts +71 -2
  23. package/src/codex/catalog/sync.ts +14 -0
  24. package/src/codex/inject.ts +3 -2
  25. package/src/codex/quota-auto-refresh.ts +6 -1
  26. package/src/codex/quota.ts +54 -8
  27. package/src/combos/index.ts +2 -0
  28. package/src/combos/resolve.ts +52 -0
  29. package/src/config.ts +58 -0
  30. package/src/generated/compatibility-version.json +72 -60
  31. package/src/lib/errors.ts +8 -0
  32. package/src/lib/privacy.ts +25 -0
  33. package/src/oauth/health.ts +47 -12
  34. package/src/oauth/index.ts +46 -8
  35. package/src/oauth/token-guardian.ts +32 -6
  36. package/src/providers/google-ai-studio-model-discovery.ts +74 -0
  37. package/src/providers/opencode-zen-rate-limit.ts +75 -0
  38. package/src/providers/quota.ts +15 -0
  39. package/src/providers/registry.ts +1 -1
  40. package/src/server/auth-cors.ts +6 -0
  41. package/src/server/chat-completions.ts +4 -4
  42. package/src/server/chat-native.ts +10 -1
  43. package/src/server/claude-messages.ts +5 -5
  44. package/src/server/images.ts +2 -2
  45. package/src/server/index.ts +25 -2
  46. package/src/server/management/logs-usage-routes.ts +4 -1
  47. package/src/server/management/model-rows.ts +16 -1
  48. package/src/server/management/oauth-account-routes.ts +6 -2
  49. package/src/server/management/provider-routes.ts +9 -2
  50. package/src/server/management/request-history-routes.ts +4 -2
  51. package/src/server/management/route-registry.ts +5 -4
  52. package/src/server/management/shared.ts +66 -3
  53. package/src/server/management-api.ts +1 -1
  54. package/src/server/request-decompress.ts +91 -3
  55. package/src/server/request-log.ts +10 -0
  56. package/src/server/responses/codex-ws-wire.ts +1 -1
  57. package/src/server/responses/compact.ts +8 -2
  58. package/src/server/responses/context-overflow.ts +11 -0
  59. package/src/server/responses/core.ts +144 -38
  60. package/src/server/responses/policy-fallback.ts +6 -2
  61. package/src/server/search.ts +2 -2
  62. package/src/service.ts +92 -7
  63. package/src/types/accounts.ts +18 -0
  64. package/src/types/config.ts +36 -0
  65. package/src/types/provider.ts +56 -0
  66. package/src/types.ts +4 -0
  67. package/src/web-search/ollama-executor.ts +127 -0
  68. package/src/web-search/passthrough-bridge.ts +761 -0
  69. package/gui/dist/assets/index-BtyONQrZ.js +0 -115
@@ -86,6 +86,57 @@ export interface RateLimitRetryPolicy {
86
86
  respectRetryAfter?: boolean;
87
87
  }
88
88
 
89
+ /**
90
+ * Backend ids admitted by `providers.<name>.webSearchBridge.backend`. Only `"ollama"` has a
91
+ * shipped executor; every other id is explicit-only and inert, the same contract the top-level
92
+ * `webSearchSidecar` uses for backends whose executor has not landed. Naming one of them keeps
93
+ * the bridge disarmed rather than silently falling back to a different search provider — in
94
+ * particular it never auto-selects a paid Luna or Exa search.
95
+ */
96
+ export const PROVIDER_WEB_SEARCH_BRIDGE_BACKENDS = [
97
+ "ollama",
98
+ "openai",
99
+ "anthropic",
100
+ "xai",
101
+ "gemini",
102
+ "exa",
103
+ ] as const;
104
+
105
+ export type ProviderWebSearchBridgeBackend = typeof PROVIDER_WEB_SEARCH_BRIDGE_BACKENDS[number];
106
+
107
+ /**
108
+ * Opt-in hosted-web-search bridge for a KEY-auth Responses passthrough provider
109
+ * (`providers.<name>.webSearchBridge`), default OFF (#3761).
110
+ *
111
+ * Codex always declares the hosted `{type:"web_search"}` tool. On the passthrough the proxy
112
+ * treats that as "the destination runs search itself" and relays it unchanged, which is true for
113
+ * the ChatGPT backend and for xAI but false for an OpenAI-shaped key gateway such as Ollama
114
+ * Cloud: the model answers with a `function_call` named `web_search` that nothing executes,
115
+ * and the undeclared-tool guard ends the turn. With this block enabled the proxy intercepts that
116
+ * call, runs the configured search backend itself, feeds the result back upstream, and shows
117
+ * Codex a hosted `web_search_call` cell.
118
+ *
119
+ * Never armed for `authMode: "forward"` (ChatGPT) or for a provider that executes hosted search
120
+ * upstream; see `planPassthroughWebSearchBridge` in `src/web-search/passthrough-bridge.ts`.
121
+ */
122
+ export interface ProviderWebSearchBridgeConfig {
123
+ /** Master switch. Absent or false keeps today's relay-and-fail behavior exactly. */
124
+ enabled?: boolean;
125
+ /** Which executor runs the search. Absent disarms the bridge; there is no implicit default. */
126
+ backend?: ProviderWebSearchBridgeBackend;
127
+ /** Searches executed per turn before the bridge refuses further ones (1..10, default 3). */
128
+ maxSearches?: number;
129
+ /** Per-search deadline in milliseconds (1000..600000, default 60000). */
130
+ timeoutMs?: number;
131
+ /**
132
+ * Absolute search-API URL. Required to use the `ollama` backend against anything other than
133
+ * the canonical `https://ollama.com` origin, which is the only origin derived automatically.
134
+ * The bridge sends the PROVIDER's own API key to this URL, so an operator setting it is
135
+ * authorizing that key for this destination.
136
+ */
137
+ endpoint?: string;
138
+ }
139
+
89
140
  /**
90
141
  * User-configured display price for one model (USD per 1M tokens).
91
142
  * Mirrors the `Cost4` shape used by the usage cost estimator; structurally
@@ -550,6 +601,11 @@ export interface OcxProviderConfig {
550
601
  * SSE/JSON; raw inspection state remains authoritative.
551
602
  */
552
603
  responsesSnapshotRepair?: boolean;
604
+ /**
605
+ * Opt-in hosted-web-search bridge for this KEY-auth Responses passthrough provider (#3761).
606
+ * Absent or disabled leaves the passthrough byte-identical to today.
607
+ */
608
+ webSearchBridge?: ProviderWebSearchBridgeConfig;
553
609
  /**
554
610
  * Provider-wide mapping from Codex effort labels to upstream `reasoning_effort` values.
555
611
  * Map a label to the reserved value `"__omit__"` to send no reasoning field at all for that
package/src/types.ts CHANGED
@@ -101,6 +101,8 @@ export type {
101
101
  ResponsesItemIdRepairConfig,
102
102
  RateLimitRetryPolicy,
103
103
  TransientRetryPolicy,
104
+ ProviderWebSearchBridgeBackend,
105
+ ProviderWebSearchBridgeConfig,
104
106
  ProviderCostOverlay,
105
107
  RequestPacingRule,
106
108
  ProviderRequestPacingConfig,
@@ -111,6 +113,8 @@ export type {
111
113
  OcxProviderConfig,
112
114
  } from "./types/provider";
113
115
 
116
+ export { PROVIDER_WEB_SEARCH_BRIDGE_BACKENDS } from "./types/provider";
117
+
114
118
  export type {
115
119
  CodexAccount,
116
120
  CodexAccountCredentials,
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Execute ONE web search via the Ollama web-search API — the executor behind
3
+ * `providers.<name>.webSearchBridge.backend: "ollama"` (#3761).
4
+ *
5
+ * Documented contract: POST <origin>/api/web_search with a bearer key returns
6
+ * {results: [{title, url, content}]}; `max_results` defaults to 5 and caps at 10
7
+ * (https://docs.ollama.com/web-search). Like Exa, this lane returns ranked results
8
+ * rather than a prose answer, so the outcome text is a digest the routed model
9
+ * synthesizes from.
10
+ *
11
+ * The key is the PROVIDER's own API key: an operator who enables the bridge is reusing
12
+ * their Ollama Cloud route key on a second Ollama endpoint. That is why the planner
13
+ * refuses to derive a non-canonical origin on its own.
14
+ *
15
+ * Never throws; every error string passes redactSecretString and scrubs the literal key.
16
+ */
17
+ import { applyUpstreamRecoveryInit, fetchWithResetRetry } from "../lib/upstream-retry";
18
+ import { cancelBodyOnAbort, signalWithTimeout } from "../lib/abort";
19
+ import { readBoundedResponseBytes } from "../lib/bounded-body";
20
+ import { sidecarEnter } from "../lib/sidecar-tracker";
21
+ import { redactSecretString } from "../lib/redact";
22
+ import { MAX_SIDECAR_RESPONSE_BYTES, type WebSearchSource } from "./parse";
23
+ import type { SidecarOutcome } from "./executor";
24
+
25
+ /** Documented ceiling for the API's own `max_results`; a larger value is rejected upstream. */
26
+ export const OLLAMA_WEB_SEARCH_MAX_RESULTS = 5;
27
+ const OLLAMA_SNIPPET_CHARS = 1000;
28
+
29
+ function isRec(value: unknown): value is Record<string, unknown> {
30
+ return !!value && typeof value === "object" && !Array.isArray(value);
31
+ }
32
+
33
+ export async function runOllamaWebSearch(
34
+ query: string,
35
+ apiKey: string,
36
+ endpoint: string,
37
+ timeoutMs: number,
38
+ abortSignal?: AbortSignal,
39
+ ): Promise<SidecarOutcome> {
40
+ if (!apiKey) {
41
+ return { text: "", sources: [], error: "ollama web-search backend selected without a provider apiKey" };
42
+ }
43
+ // The executor KNOWS the secret, so pattern-based redaction is not enough: scrub the
44
+ // literal value before anything derived from an upstream body is returned.
45
+ const scrub = (value: string) =>
46
+ redactSecretString(value.split(apiKey).join("[redacted-provider-key]"));
47
+ const linkedSignal = signalWithTimeout(timeoutMs, abortSignal);
48
+ const sidecarExit = sidecarEnter("web-search");
49
+ const startedAt = Date.now();
50
+ try {
51
+ const res = await fetchWithResetRetry(
52
+ recovery => fetch(endpoint, applyUpstreamRecoveryInit({
53
+ method: "POST",
54
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
55
+ body: JSON.stringify({ query, max_results: OLLAMA_WEB_SEARCH_MAX_RESULTS }),
56
+ signal: linkedSignal.signal,
57
+ // Bun forwards custom headers across redirects, so a redirect would leak the key.
58
+ redirect: "manual",
59
+ }, recovery)),
60
+ { abortSignal: linkedSignal.signal, label: "ollama-web-search-bridge" },
61
+ );
62
+ const detachBodyGuard = cancelBodyOnAbort(res.body, linkedSignal.signal);
63
+ try {
64
+ let bounded: Awaited<ReturnType<typeof readBoundedResponseBytes>> | null = null;
65
+ try {
66
+ bounded = await readBoundedResponseBytes(res, {
67
+ maxBytes: MAX_SIDECAR_RESPONSE_BYTES,
68
+ signal: linkedSignal.signal,
69
+ });
70
+ } catch {
71
+ const reason = linkedSignal.signal.reason;
72
+ if (linkedSignal.signal.aborted && reason instanceof Error && reason.name === "TimeoutError") {
73
+ throw reason;
74
+ }
75
+ // A body-read failure degrades to the status-only outcome below.
76
+ }
77
+ if (bounded?.oversized) {
78
+ const prefix = res.ok ? "ollama web-search response" : `ollama web-search HTTP ${res.status} response`;
79
+ return { text: "", sources: [], error: `${prefix} exceeded byte bound` };
80
+ }
81
+ const text = bounded ? new TextDecoder().decode(bounded.bytes) : "";
82
+ if (!res.ok) {
83
+ // Scrub BEFORE truncating: slicing first can cut the literal key at the boundary
84
+ // and leave an unscrubbable prefix in the surviving text.
85
+ return { text: "", sources: [], error: `ollama web-search HTTP ${res.status}: ${scrub(text).slice(0, 200)}` };
86
+ }
87
+ let payload: unknown = null;
88
+ try {
89
+ payload = JSON.parse(text);
90
+ } catch {
91
+ // The mapper owns the stable malformed/empty JSON outcome.
92
+ }
93
+ return mapOllamaSearchResponse(payload);
94
+ } finally {
95
+ detachBodyGuard();
96
+ }
97
+ } catch (error) {
98
+ const kind = error instanceof Error && error.name === "TimeoutError" ? "timeout" : "connect_error";
99
+ console.warn(`[web-search] ollama bridge ${kind} (${Date.now() - startedAt}ms)`);
100
+ return { text: "", sources: [], error: scrub(error instanceof Error ? error.message : String(error)) };
101
+ } finally {
102
+ sidecarExit();
103
+ linkedSignal.cleanup();
104
+ }
105
+ }
106
+
107
+ /** Map an Ollama /api/web_search payload to a digest the routed model can synthesize from. */
108
+ export function mapOllamaSearchResponse(payload: unknown): SidecarOutcome {
109
+ if (!isRec(payload) || !Array.isArray(payload.results)) {
110
+ return { text: "", sources: [], error: "ollama web-search returned a non-JSON or shapeless body" };
111
+ }
112
+ const sources: WebSearchSource[] = [];
113
+ const lines: string[] = [];
114
+ const seen = new Set<string>();
115
+ for (const result of payload.results) {
116
+ if (!isRec(result) || typeof result.url !== "string" || result.url.length === 0) continue;
117
+ if (seen.has(result.url)) continue;
118
+ seen.add(result.url);
119
+ const title = typeof result.title === "string" && result.title.length > 0 ? result.title : result.url;
120
+ sources.push({ url: result.url, ...(title !== result.url ? { title } : {}) });
121
+ const snippet = typeof result.content === "string" ? result.content.trim().slice(0, OLLAMA_SNIPPET_CHARS) : "";
122
+ lines.push(`- ${title}: ${snippet || "(no excerpt)"} [${result.url}]`);
123
+ }
124
+ if (lines.length === 0) return { text: "", sources: [], error: "ollama web-search returned no results" };
125
+ return { text: `Search results:\n${lines.join("\n")}`, sources };
126
+ }
127
+