jeo-code 0.8.30 → 0.8.31
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/CHANGELOG.md +13 -0
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/package.json +1 -1
- package/src/agent/web-search.ts +223 -3
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
6
6
|
|
|
7
7
|
The README mirrors the latest 5 entries — regenerate with `bun run changelog:sync`.
|
|
8
8
|
|
|
9
|
+
## [0.8.31] - 2026-07-20
|
|
10
|
+
_`web_search` no longer silently degrades every non-Anthropic model to keyless DuckDuckGo scraping — OpenAI and Gemini sessions now get their own native, hosted search tool (matching the same active-model-gated, credential-required design Anthropic already had), with DuckDuckGo remaining the always-on terminal fallback for everyone else._
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Native OpenAI web search** (`src/agent/web-search.ts`) — routes an active `openai`-provider session through the Responses API's hosted `web_search` tool (`api.openai.com/v1/responses`, per platform.openai.com/docs/guides/tools-web-search), parsing `web_search_call` output items for the executed queries and `message` items' `annotations[].url_citation` for inline-cited sources. Gated on a REAL API key only — the ChatGPT/Codex OAuth backend (`chatgpt.com/backend-api/codex/responses`) is a separate, undocumented surface never verified to support hosted Responses tools, so it deliberately never qualifies (falls through to DuckDuckGo instead of guessing). New `JEO_SEARCH_API_KEY_OPENAI` / `JEO_SEARCH_MODEL_OPENAI` overrides (default model: `gpt-4o-mini`).
|
|
14
|
+
- **Native Gemini web search** (`src/agent/web-search.ts`) — routes an active `gemini` (and `antigravity`, when a plain `GEMINI_API_KEY` also happens to be configured) session through Grounding with Google Search (`generateContent` with `tools: [{googleSearch: {}}]`, per ai.google.dev/gemini-api/docs/google-search), parsing `groundingMetadata.groundingChunks`/`groundingSupports`/`webSearchQueries` (verified against the current `google-genai` SDK's `GroundingMetadata`/`GroundingChunk`/`GroundingSupport` schema, whose Python snake_case fields map to the documented classic-endpoint camelCase wire shape) into sources + inline citations. Gated on a REAL API key only — Gemini/Google OAuth is scoped to the separate Cloud Code Assist/antigravity backend and never serves this endpoint (matching `model-manager.ts`'s existing `oauthServesModel` contract). New `JEO_SEARCH_API_KEY_GEMINI` / `JEO_SEARCH_MODEL_GEMINI` overrides (default model: `gemini-2.5-flash`).
|
|
15
|
+
|
|
16
|
+
### Verified
|
|
17
|
+
- `test/web-search.test.ts` — 23 pass (was 14): +9 new tests covering response parsing against real documented fixture shapes, live-mocked-fetch request-shape assertions (exact URL/headers/body for both new providers), provider-chain routing (openai/gemini/antigravity → their own native provider when THEIR OWN credential is set, never borrowing another provider's key), and the no-API-key → DuckDuckGo fallback path for both.
|
|
18
|
+
- Real (unmocked) end-to-end run of the full chain in this environment (`bun -e`, no OpenAI/Gemini API keys configured here) — genuinely hit DuckDuckGo over the network and returned real, current search results, confirming the provider-chain plumbing works live, not just under test mocks.
|
|
19
|
+
- Full `bun test` — 3090 pass / 0 fail across 304 files.
|
|
20
|
+
- `bun run typecheck` — no errors.
|
|
21
|
+
|
|
9
22
|
## [0.8.30] - 2026-07-20
|
|
10
23
|
_`bun run build` (the host-only dev binary build) has been silently broken for every user who ran it — a drift between the plain `package.json` `build` script and the actual working release-binary build command (`scripts/ci-release-build-binaries.ts`), which had already documented and fixed the exact same crash._
|
|
11
24
|
|
package/README.ja.md
CHANGED
|
@@ -283,11 +283,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
|
|
|
283
283
|
## 変更履歴 (Changelog)
|
|
284
284
|
|
|
285
285
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
286
|
+
- **[0.8.31]** (2026-07-20) — `web_search` no longer silently degrades every non-Anthropic model to keyless DuckDuckGo scraping — OpenAI and Gemini sessions now get their own native, hosted search tool (matching the same active-model-gated, credential-required design Anthropic already had), with DuckDuckGo remaining the always-on terminal fallback for everyone else.
|
|
286
287
|
- **[0.8.30]** (2026-07-20) — `bun run build` (the host-only dev binary build) has been silently broken for every user who ran it — a drift between the plain `package.json` `build` script and the actual working release-binary build command (`scripts/ci-release-build-binaries.ts`), which had already documented and fixed the exact same crash.
|
|
287
288
|
- **[0.8.29]** (2026-07-20) — gajae-code (gjc) v0.10.2→v0.11.4 gap analysis: most of that range is gjc-specific architecture jeo intentionally does not replicate (SDK broker/session-index recovery, Gajae Pet, coordinator-mcp, Telegram/Discord rich-delivery internals, browser tab workers, worktree-subcommand removal, RPC durable model selection) — one genuinely applicable TUI bug was found and closed; the CLI empty-non-TTY-stdin fix (gjc #2586) was investigated and found already-correct by construction (jeo's one-shot gate forces true whenever stdin isn't a TTY, regardless of args, so the hang gjc fixed cannot occur here).
|
|
288
289
|
- **[0.8.28]** (2026-07-16) — Improved the welcome UI's right-side table to display dynamic sections (What's New, Flow keys, Project pulse, and Session trail) matching gajae-code (gjc) >= 0.8.0 features.
|
|
289
290
|
- **[0.8.27]** (2026-07-16) — Repeat-read recovery now survives both context-overflow and refusal-result elision without reopening mutating-call loops.
|
|
290
|
-
- **[0.8.26]** (2026-07-14) — Model catalog cleanup (drop sub-4.6 Anthropic `claude-haiku-4-5` from the exposed catalog/aliases per user direction) plus a focused gap analysis against `gajae-code`'s (gjc) 0.8.0→0.10.1 release history (~200 PRs) — most of gjc's surface (RPC/ACP/coordinator-mcp/IRC-sidebar/psmux/Windows-team) doesn't exist in jeo's smaller architecture and was ruled out with direct evidence; a few genuinely applicable gaps were found and closed.
|
|
291
291
|
|
|
292
292
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
293
293
|
<!-- CHANGELOG:END -->
|
package/README.ko.md
CHANGED
|
@@ -284,11 +284,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
|
|
|
284
284
|
## 변경 이력 (Changelog)
|
|
285
285
|
|
|
286
286
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
287
|
+
- **[0.8.31]** (2026-07-20) — `web_search` no longer silently degrades every non-Anthropic model to keyless DuckDuckGo scraping — OpenAI and Gemini sessions now get their own native, hosted search tool (matching the same active-model-gated, credential-required design Anthropic already had), with DuckDuckGo remaining the always-on terminal fallback for everyone else.
|
|
287
288
|
- **[0.8.30]** (2026-07-20) — `bun run build` (the host-only dev binary build) has been silently broken for every user who ran it — a drift between the plain `package.json` `build` script and the actual working release-binary build command (`scripts/ci-release-build-binaries.ts`), which had already documented and fixed the exact same crash.
|
|
288
289
|
- **[0.8.29]** (2026-07-20) — gajae-code (gjc) v0.10.2→v0.11.4 gap analysis: most of that range is gjc-specific architecture jeo intentionally does not replicate (SDK broker/session-index recovery, Gajae Pet, coordinator-mcp, Telegram/Discord rich-delivery internals, browser tab workers, worktree-subcommand removal, RPC durable model selection) — one genuinely applicable TUI bug was found and closed; the CLI empty-non-TTY-stdin fix (gjc #2586) was investigated and found already-correct by construction (jeo's one-shot gate forces true whenever stdin isn't a TTY, regardless of args, so the hang gjc fixed cannot occur here).
|
|
289
290
|
- **[0.8.28]** (2026-07-16) — Improved the welcome UI's right-side table to display dynamic sections (What's New, Flow keys, Project pulse, and Session trail) matching gajae-code (gjc) >= 0.8.0 features.
|
|
290
291
|
- **[0.8.27]** (2026-07-16) — Repeat-read recovery now survives both context-overflow and refusal-result elision without reopening mutating-call loops.
|
|
291
|
-
- **[0.8.26]** (2026-07-14) — Model catalog cleanup (drop sub-4.6 Anthropic `claude-haiku-4-5` from the exposed catalog/aliases per user direction) plus a focused gap analysis against `gajae-code`'s (gjc) 0.8.0→0.10.1 release history (~200 PRs) — most of gjc's surface (RPC/ACP/coordinator-mcp/IRC-sidebar/psmux/Windows-team) doesn't exist in jeo's smaller architecture and was ruled out with direct evidence; a few genuinely applicable gaps were found and closed.
|
|
292
292
|
|
|
293
293
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
294
294
|
<!-- CHANGELOG:END -->
|
package/README.md
CHANGED
|
@@ -291,11 +291,11 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
|
|
|
291
291
|
## Changelog
|
|
292
292
|
|
|
293
293
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
294
|
+
- **[0.8.31]** (2026-07-20) — `web_search` no longer silently degrades every non-Anthropic model to keyless DuckDuckGo scraping — OpenAI and Gemini sessions now get their own native, hosted search tool (matching the same active-model-gated, credential-required design Anthropic already had), with DuckDuckGo remaining the always-on terminal fallback for everyone else.
|
|
294
295
|
- **[0.8.30]** (2026-07-20) — `bun run build` (the host-only dev binary build) has been silently broken for every user who ran it — a drift between the plain `package.json` `build` script and the actual working release-binary build command (`scripts/ci-release-build-binaries.ts`), which had already documented and fixed the exact same crash.
|
|
295
296
|
- **[0.8.29]** (2026-07-20) — gajae-code (gjc) v0.10.2→v0.11.4 gap analysis: most of that range is gjc-specific architecture jeo intentionally does not replicate (SDK broker/session-index recovery, Gajae Pet, coordinator-mcp, Telegram/Discord rich-delivery internals, browser tab workers, worktree-subcommand removal, RPC durable model selection) — one genuinely applicable TUI bug was found and closed; the CLI empty-non-TTY-stdin fix (gjc #2586) was investigated and found already-correct by construction (jeo's one-shot gate forces true whenever stdin isn't a TTY, regardless of args, so the hang gjc fixed cannot occur here).
|
|
296
297
|
- **[0.8.28]** (2026-07-16) — Improved the welcome UI's right-side table to display dynamic sections (What's New, Flow keys, Project pulse, and Session trail) matching gajae-code (gjc) >= 0.8.0 features.
|
|
297
298
|
- **[0.8.27]** (2026-07-16) — Repeat-read recovery now survives both context-overflow and refusal-result elision without reopening mutating-call loops.
|
|
298
|
-
- **[0.8.26]** (2026-07-14) — Model catalog cleanup (drop sub-4.6 Anthropic `claude-haiku-4-5` from the exposed catalog/aliases per user direction) plus a focused gap analysis against `gajae-code`'s (gjc) 0.8.0→0.10.1 release history (~200 PRs) — most of gjc's surface (RPC/ACP/coordinator-mcp/IRC-sidebar/psmux/Windows-team) doesn't exist in jeo's smaller architecture and was ruled out with direct evidence; a few genuinely applicable gaps were found and closed.
|
|
299
299
|
|
|
300
300
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
301
301
|
<!-- CHANGELOG:END -->
|
package/README.zh.md
CHANGED
|
@@ -283,11 +283,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
|
|
|
283
283
|
## 更新日志 (Changelog)
|
|
284
284
|
|
|
285
285
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
286
|
+
- **[0.8.31]** (2026-07-20) — `web_search` no longer silently degrades every non-Anthropic model to keyless DuckDuckGo scraping — OpenAI and Gemini sessions now get their own native, hosted search tool (matching the same active-model-gated, credential-required design Anthropic already had), with DuckDuckGo remaining the always-on terminal fallback for everyone else.
|
|
286
287
|
- **[0.8.30]** (2026-07-20) — `bun run build` (the host-only dev binary build) has been silently broken for every user who ran it — a drift between the plain `package.json` `build` script and the actual working release-binary build command (`scripts/ci-release-build-binaries.ts`), which had already documented and fixed the exact same crash.
|
|
287
288
|
- **[0.8.29]** (2026-07-20) — gajae-code (gjc) v0.10.2→v0.11.4 gap analysis: most of that range is gjc-specific architecture jeo intentionally does not replicate (SDK broker/session-index recovery, Gajae Pet, coordinator-mcp, Telegram/Discord rich-delivery internals, browser tab workers, worktree-subcommand removal, RPC durable model selection) — one genuinely applicable TUI bug was found and closed; the CLI empty-non-TTY-stdin fix (gjc #2586) was investigated and found already-correct by construction (jeo's one-shot gate forces true whenever stdin isn't a TTY, regardless of args, so the hang gjc fixed cannot occur here).
|
|
288
289
|
- **[0.8.28]** (2026-07-16) — Improved the welcome UI's right-side table to display dynamic sections (What's New, Flow keys, Project pulse, and Session trail) matching gajae-code (gjc) >= 0.8.0 features.
|
|
289
290
|
- **[0.8.27]** (2026-07-16) — Repeat-read recovery now survives both context-overflow and refusal-result elision without reopening mutating-call loops.
|
|
290
|
-
- **[0.8.26]** (2026-07-14) — Model catalog cleanup (drop sub-4.6 Anthropic `claude-haiku-4-5` from the exposed catalog/aliases per user direction) plus a focused gap analysis against `gajae-code`'s (gjc) 0.8.0→0.10.1 release history (~200 PRs) — most of gjc's surface (RPC/ACP/coordinator-mcp/IRC-sidebar/psmux/Windows-team) doesn't exist in jeo's smaller architecture and was ruled out with direct evidence; a few genuinely applicable gaps were found and closed.
|
|
291
291
|
|
|
292
292
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
293
293
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
package/src/agent/web-search.ts
CHANGED
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
* available is primary.
|
|
8
8
|
* 2. Otherwise the ACTIVE MODEL's own native search provider is primary,
|
|
9
9
|
* but only when that provider's credentials are present (active-model
|
|
10
|
-
* gating — never credential scanning).
|
|
11
|
-
*
|
|
10
|
+
* gating — never credential scanning). Native providers today: Anthropic's
|
|
11
|
+
* server-side `web_search_20250305` tool, OpenAI's Responses API `web_search`
|
|
12
|
+
* hosted tool, and Gemini's `googleSearch` grounding tool (also used for
|
|
13
|
+
* `antigravity` sessions when a plain `GEMINI_API_KEY` happens to be set —
|
|
14
|
+
* Google/Gemini OAuth does not serve this endpoint, so it never qualifies).
|
|
12
15
|
* 3. DuckDuckGo (keyless HTML scraping, no API key, no OAuth) is ALWAYS
|
|
13
16
|
* appended as the terminal fallback, so a missing/failed primary still
|
|
14
17
|
* returns real results with zero configuration — results are always
|
|
@@ -53,7 +56,7 @@ export interface SearchRequest {
|
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
export interface SearchProviderDef {
|
|
56
|
-
id: "anthropic" | "duckduckgo";
|
|
59
|
+
id: "anthropic" | "openai" | "gemini" | "duckduckgo";
|
|
57
60
|
label: string;
|
|
58
61
|
/** Credential gate — keyless providers return true unconditionally. */
|
|
59
62
|
isAvailable(): Promise<boolean> | boolean;
|
|
@@ -204,6 +207,196 @@ async function searchAnthropic(req: SearchRequest): Promise<WebSearchResponse> {
|
|
|
204
207
|
return parsed;
|
|
205
208
|
}
|
|
206
209
|
|
|
210
|
+
// ── OpenAI provider (Responses API `web_search` hosted tool) ─────────────────
|
|
211
|
+
|
|
212
|
+
const OPENAI_RESPONSES_URL = "https://api.openai.com/v1/responses";
|
|
213
|
+
// Cheapest current tool-calling-capable model — the hosted `web_search` tool works
|
|
214
|
+
// across the whole Responses API model line (per platform.openai.com/docs/guides/
|
|
215
|
+
// tools-web-search), not just the now-deprecated `*-search-preview` variants.
|
|
216
|
+
const DEFAULT_SEARCH_MODEL_OPENAI = "gpt-4o-mini";
|
|
217
|
+
const OPENAI_HARD_TIMEOUT_MS = 90_000;
|
|
218
|
+
|
|
219
|
+
/** Only a real API key is trusted for the hosted `web_search` tool. The ChatGPT/Codex
|
|
220
|
+
* OAuth backend (`chatgpt.com/backend-api/codex/responses`, see openai-responses.ts) is
|
|
221
|
+
* a SEPARATE, undocumented surface never verified to support hosted Responses tools —
|
|
222
|
+
* `api.openai.com/v1/responses` with a plain Bearer API key is the documented, stable
|
|
223
|
+
* path this provider uses exclusively. */
|
|
224
|
+
async function resolveOpenAISearchCredential(): Promise<Credential> {
|
|
225
|
+
const dedicated = jeoEnv("SEARCH_API_KEY_OPENAI");
|
|
226
|
+
if (dedicated) return { kind: "api_key", provider: "openai", token: dedicated };
|
|
227
|
+
const cred = await resolveCredential("openai");
|
|
228
|
+
return cred.kind === "api_key" ? cred : { kind: "none", provider: "openai" };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Parse a non-streaming Responses API `web_search` result into the unified shape.
|
|
232
|
+
* Per platform.openai.com/docs/guides/tools-web-search: `output` items of type
|
|
233
|
+
* `web_search_call` carry the executed queries (`action.query`), `message` items
|
|
234
|
+
* carry `content[].text` + `annotations[]` of type `url_citation` (url/title/
|
|
235
|
+
* start_index/end_index) for inline-cited sources. */
|
|
236
|
+
export function parseOpenAISearchResponse(response: any): WebSearchResponse {
|
|
237
|
+
const answerParts: string[] = [];
|
|
238
|
+
const searchQueries: string[] = [];
|
|
239
|
+
const citations: WebSearchResponse["citations"] = [];
|
|
240
|
+
const sourcesByUrl = new Map<string, WebSearchSource>();
|
|
241
|
+
|
|
242
|
+
for (const item of response?.output ?? []) {
|
|
243
|
+
if (item?.type === "web_search_call" && item.action?.type === "search" && typeof item.action.query === "string") {
|
|
244
|
+
searchQueries.push(item.action.query);
|
|
245
|
+
} else if (item?.type === "message" && Array.isArray(item.content)) {
|
|
246
|
+
for (const block of item.content) {
|
|
247
|
+
if ((block?.type === "output_text" || block?.type === "text") && typeof block.text === "string") {
|
|
248
|
+
const text = block.text as string;
|
|
249
|
+
answerParts.push(text);
|
|
250
|
+
for (const a of block.annotations ?? []) {
|
|
251
|
+
if (a?.type === "url_citation" && typeof a.url === "string") {
|
|
252
|
+
const citedText = typeof a.start_index === "number" && typeof a.end_index === "number"
|
|
253
|
+
? text.slice(a.start_index, a.end_index)
|
|
254
|
+
: undefined;
|
|
255
|
+
citations.push({ url: a.url, title: typeof a.title === "string" ? a.title : undefined, citedText });
|
|
256
|
+
if (!sourcesByUrl.has(a.url)) sourcesByUrl.set(a.url, { title: (typeof a.title === "string" && a.title) || a.url, url: a.url });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
provider: "OpenAI",
|
|
266
|
+
answer: answerParts.join("\n\n") || undefined,
|
|
267
|
+
sources: [...sourcesByUrl.values()],
|
|
268
|
+
citations,
|
|
269
|
+
searchQueries,
|
|
270
|
+
usage: response?.usage
|
|
271
|
+
? {
|
|
272
|
+
inputTokens: response.usage.input_tokens,
|
|
273
|
+
outputTokens: response.usage.output_tokens,
|
|
274
|
+
searchRequests: searchQueries.length || undefined,
|
|
275
|
+
}
|
|
276
|
+
: undefined,
|
|
277
|
+
model: typeof response?.model === "string" ? response.model : undefined,
|
|
278
|
+
requestId: typeof response?.id === "string" ? response.id : undefined,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function searchOpenAI(req: SearchRequest): Promise<WebSearchResponse> {
|
|
283
|
+
const credential = await resolveOpenAISearchCredential();
|
|
284
|
+
if (credential.kind !== "api_key") {
|
|
285
|
+
throw new Error("OpenAI: no API key credentials (set OPENAI_API_KEY or JEO_SEARCH_API_KEY_OPENAI — the ChatGPT/Codex subscription login does not serve the hosted web_search tool)");
|
|
286
|
+
}
|
|
287
|
+
const effectiveQuery = req.recency ? `${req.query} (results from the last ${req.recency})` : req.query;
|
|
288
|
+
const body = {
|
|
289
|
+
model: jeoEnv("SEARCH_MODEL_OPENAI") || DEFAULT_SEARCH_MODEL_OPENAI,
|
|
290
|
+
tools: [{ type: "web_search" }],
|
|
291
|
+
input: effectiveQuery,
|
|
292
|
+
};
|
|
293
|
+
const response = await fetch(OPENAI_RESPONSES_URL, {
|
|
294
|
+
method: "POST",
|
|
295
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${credential.token}` },
|
|
296
|
+
body: JSON.stringify(body),
|
|
297
|
+
signal: AbortSignal.timeout(OPENAI_HARD_TIMEOUT_MS),
|
|
298
|
+
});
|
|
299
|
+
if (!response.ok) {
|
|
300
|
+
const detail = (await response.text().catch(() => "")).slice(0, 300);
|
|
301
|
+
throw new Error(`OpenAI API error (${response.status}): ${detail}`);
|
|
302
|
+
}
|
|
303
|
+
const parsed = parseOpenAISearchResponse(await response.json().catch(() => null));
|
|
304
|
+
if (req.limit && req.limit > 0 && parsed.sources.length > req.limit) {
|
|
305
|
+
parsed.sources = parsed.sources.slice(0, req.limit);
|
|
306
|
+
}
|
|
307
|
+
return parsed;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ── Gemini provider (Grounding with Google Search, generateContent tool) ─────
|
|
311
|
+
|
|
312
|
+
const GEMINI_GENERATE_URL_BASE = "https://generativelanguage.googleapis.com/v1beta/models";
|
|
313
|
+
const DEFAULT_SEARCH_MODEL_GEMINI = "gemini-2.5-flash";
|
|
314
|
+
const GEMINI_HARD_TIMEOUT_MS = 90_000;
|
|
315
|
+
|
|
316
|
+
/** Only a real API key is trusted. The plain `generativelanguage.googleapis.com`
|
|
317
|
+
* endpoint this hits does NOT serve Gemini/Google OAuth credentials — those are
|
|
318
|
+
* scoped to the separate Cloud Code Assist / antigravity backend (see
|
|
319
|
+
* model-manager.ts's `oauthServesModel`: a Gemini OAuth token never serves
|
|
320
|
+
* `google/gemini-*` models on its own). */
|
|
321
|
+
async function resolveGeminiSearchCredential(): Promise<Credential> {
|
|
322
|
+
const dedicated = jeoEnv("SEARCH_API_KEY_GEMINI");
|
|
323
|
+
if (dedicated) return { kind: "api_key", provider: "gemini", token: dedicated };
|
|
324
|
+
const cred = await resolveCredential("gemini");
|
|
325
|
+
return cred.kind === "api_key" ? cred : { kind: "none", provider: "gemini" };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
interface GeminiGroundingChunk { web?: { uri?: string; title?: string } }
|
|
329
|
+
interface GeminiGroundingSupport { segment?: { startIndex?: number; endIndex?: number; text?: string }; groundingChunkIndices?: number[] }
|
|
330
|
+
|
|
331
|
+
/** Parse a `generateContent` response carrying `groundingMetadata` (Grounding with
|
|
332
|
+
* Google Search) into the unified shape. Per ai.google.dev/gemini-api/docs/google-search
|
|
333
|
+
* and the current google-genai SDK's GroundingMetadata/GroundingChunk/GroundingSupport
|
|
334
|
+
* schema (Python SDK's snake_case maps to camelCase on the classic REST wire:
|
|
335
|
+
* groundingChunks, groundingSupports, webSearchQueries, groundingChunkIndices). */
|
|
336
|
+
export function parseGeminiSearchResponse(response: any): WebSearchResponse {
|
|
337
|
+
const candidate = response?.candidates?.[0];
|
|
338
|
+
const parts: any[] = candidate?.content?.parts ?? [];
|
|
339
|
+
const answer = parts.map((p: any) => (typeof p?.text === "string" ? p.text : "")).join("");
|
|
340
|
+
const grounding = candidate?.groundingMetadata;
|
|
341
|
+
const chunks: GeminiGroundingChunk[] = grounding?.groundingChunks ?? [];
|
|
342
|
+
const sources: WebSearchSource[] = chunks
|
|
343
|
+
.filter(c => !!c.web?.uri)
|
|
344
|
+
.map(c => ({ title: c.web!.title || c.web!.uri!, url: c.web!.uri! }));
|
|
345
|
+
const citations: WebSearchResponse["citations"] = (grounding?.groundingSupports as GeminiGroundingSupport[] | undefined ?? []).flatMap(s => {
|
|
346
|
+
const text = s.segment?.text
|
|
347
|
+
?? (typeof s.segment?.startIndex === "number" && typeof s.segment?.endIndex === "number" ? answer.slice(s.segment.startIndex, s.segment.endIndex) : undefined);
|
|
348
|
+
return (s.groundingChunkIndices ?? [])
|
|
349
|
+
.map(i => chunks[i])
|
|
350
|
+
.filter((chunk): chunk is GeminiGroundingChunk & { web: { uri: string } } => !!chunk?.web?.uri)
|
|
351
|
+
.map(chunk => ({ url: chunk.web.uri, title: chunk.web.title, citedText: text }));
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
provider: "Gemini",
|
|
356
|
+
answer: answer.trim() || undefined,
|
|
357
|
+
sources,
|
|
358
|
+
citations,
|
|
359
|
+
searchQueries: grounding?.webSearchQueries ?? [],
|
|
360
|
+
usage: response?.usageMetadata
|
|
361
|
+
? {
|
|
362
|
+
inputTokens: response.usageMetadata.promptTokenCount,
|
|
363
|
+
outputTokens: response.usageMetadata.candidatesTokenCount,
|
|
364
|
+
searchRequests: (grounding?.webSearchQueries ?? []).length || undefined,
|
|
365
|
+
}
|
|
366
|
+
: undefined,
|
|
367
|
+
model: typeof response?.modelVersion === "string" ? response.modelVersion : undefined,
|
|
368
|
+
requestId: typeof response?.responseId === "string" ? response.responseId : undefined,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function searchGemini(req: SearchRequest): Promise<WebSearchResponse> {
|
|
373
|
+
const credential = await resolveGeminiSearchCredential();
|
|
374
|
+
if (credential.kind !== "api_key") {
|
|
375
|
+
throw new Error("Gemini: no API key credentials (set GEMINI_API_KEY or JEO_SEARCH_API_KEY_GEMINI — Google/Gemini OAuth does not serve this endpoint)");
|
|
376
|
+
}
|
|
377
|
+
const effectiveQuery = req.recency ? `${req.query} (results from the last ${req.recency})` : req.query;
|
|
378
|
+
const model = jeoEnv("SEARCH_MODEL_GEMINI") || DEFAULT_SEARCH_MODEL_GEMINI;
|
|
379
|
+
const body = {
|
|
380
|
+
contents: [{ role: "user", parts: [{ text: effectiveQuery }] }],
|
|
381
|
+
tools: [{ googleSearch: {} }],
|
|
382
|
+
};
|
|
383
|
+
const response = await fetch(`${GEMINI_GENERATE_URL_BASE}/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(credential.token)}`, {
|
|
384
|
+
method: "POST",
|
|
385
|
+
headers: { "content-type": "application/json" },
|
|
386
|
+
body: JSON.stringify(body),
|
|
387
|
+
signal: AbortSignal.timeout(GEMINI_HARD_TIMEOUT_MS),
|
|
388
|
+
});
|
|
389
|
+
if (!response.ok) {
|
|
390
|
+
const detail = (await response.text().catch(() => "")).slice(0, 300);
|
|
391
|
+
throw new Error(`Gemini API error (${response.status}): ${detail}`);
|
|
392
|
+
}
|
|
393
|
+
const parsed = parseGeminiSearchResponse(await response.json().catch(() => null));
|
|
394
|
+
if (req.limit && req.limit > 0 && parsed.sources.length > req.limit) {
|
|
395
|
+
parsed.sources = parsed.sources.slice(0, req.limit);
|
|
396
|
+
}
|
|
397
|
+
return parsed;
|
|
398
|
+
}
|
|
399
|
+
|
|
207
400
|
// ── DuckDuckGo provider (keyless terminal fallback; gjc port) ────────────────
|
|
208
401
|
|
|
209
402
|
const DDG_HTML_ENDPOINT = "https://html.duckduckgo.com/html/";
|
|
@@ -381,6 +574,26 @@ const PROVIDERS: Record<SearchProviderDef["id"], SearchProviderDef> = {
|
|
|
381
574
|
},
|
|
382
575
|
search: searchAnthropic,
|
|
383
576
|
},
|
|
577
|
+
openai: {
|
|
578
|
+
id: "openai",
|
|
579
|
+
label: "OpenAI",
|
|
580
|
+
isAvailable: async () => {
|
|
581
|
+
if (jeoEnv("SEARCH_API_KEY_OPENAI")) return true;
|
|
582
|
+
const cred = await resolveCredential("openai").catch(() => null);
|
|
583
|
+
return cred?.kind === "api_key";
|
|
584
|
+
},
|
|
585
|
+
search: searchOpenAI,
|
|
586
|
+
},
|
|
587
|
+
gemini: {
|
|
588
|
+
id: "gemini",
|
|
589
|
+
label: "Gemini",
|
|
590
|
+
isAvailable: async () => {
|
|
591
|
+
if (jeoEnv("SEARCH_API_KEY_GEMINI")) return true;
|
|
592
|
+
const cred = await resolveCredential("gemini").catch(() => null);
|
|
593
|
+
return cred?.kind === "api_key";
|
|
594
|
+
},
|
|
595
|
+
search: searchGemini,
|
|
596
|
+
},
|
|
384
597
|
duckduckgo: {
|
|
385
598
|
id: "duckduckgo",
|
|
386
599
|
label: "DuckDuckGo",
|
|
@@ -393,6 +606,13 @@ const PROVIDERS: Record<SearchProviderDef["id"], SearchProviderDef> = {
|
|
|
393
606
|
* Providers without a jeo-native search implementation fall through to DuckDuckGo. */
|
|
394
607
|
const MODEL_PROVIDER_TO_SEARCH: Record<string, SearchProviderDef["id"]> = {
|
|
395
608
|
anthropic: "anthropic",
|
|
609
|
+
openai: "openai",
|
|
610
|
+
gemini: "gemini",
|
|
611
|
+
// antigravity sessions route to Google's OWN native search when a plain
|
|
612
|
+
// GEMINI_API_KEY also happens to be configured — the antigravity OAuth
|
|
613
|
+
// credential itself never qualifies (see resolveGeminiSearchCredential),
|
|
614
|
+
// so this never silently scans an unrelated provider's credentials.
|
|
615
|
+
antigravity: "gemini",
|
|
396
616
|
};
|
|
397
617
|
|
|
398
618
|
let activeModelHint: string | undefined;
|