jeopi-cli 16.6.6 → 16.6.9

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.
@@ -1,5 +1,5 @@
1
1
  import type { AgentMessage } from "jeopi-agent-core";
2
- import { getBlobsDir, isEnoent, parseJsonlLenient } from "jeopi-utils";
2
+ import { getBlobsDir, isEnoent, parseJsonlLenient, readLines } from "jeopi-utils";
3
3
  import { BlobStore, isBlobRef, resolveImageData, resolveImageDataUrl } from "./blob-store";
4
4
  import { buildSessionContext } from "./session-context";
5
5
  import {
@@ -109,66 +109,24 @@ export async function loadEntriesFromFileStream(filePath: string): Promise<{
109
109
  const entries: FileEntry[] = [];
110
110
  let titleSlot: SessionTitleUpdate | undefined;
111
111
  let sawFirstLine = false;
112
- // Byte buffer (NOT a decoded string): multibyte UTF-8 sequences that straddle
113
- // a stream-chunk boundary stay intact, and Bun.JSONL.parseChunk accepts typed
114
- // arrays directly. Only the unconsumed remainder is held (≤ one record + a
115
- // chunk), so the ≥8MiB memory guard is preserved (the file is never fully
116
- // loaded into memory).
117
- let buffer: Uint8Array = new Uint8Array();
112
+ // readLines grows its byte buffer geometrically and yields complete records.
113
+ // Parse each record once: retrying an incomplete multi-MB summary for every
114
+ // stream chunk repeatedly copies and parses the same growing payload.
118
115
  const decoder = new TextDecoder();
119
116
 
120
- const drain = () => {
121
- while (buffer.length > 0) {
122
- const { values, error, read, done } = Bun.JSONL.parseChunk(buffer);
123
- if (values.length > 0) {
124
- for (const value of values) entries.push(value as FileEntry);
125
- }
126
- if (error) {
127
- // Malformed record: skip past the next newline and continue.
128
- const nextNewline = buffer.indexOf(0x0a, read);
129
- if (nextNewline === -1) break; // rest of the bad line not yet received
130
- buffer = buffer.subarray(nextNewline + 1);
131
- continue;
132
- }
133
- if (read === 0) break; // incomplete record awaiting more data
134
- buffer = buffer.subarray(read);
135
- if (done) {
136
- buffer = new Uint8Array();
137
- break;
138
- }
139
- }
140
- };
141
-
142
117
  try {
143
- for await (const chunk of Bun.file(filePath).stream()) {
144
- buffer = buffer.length === 0 ? chunk : Buffer.concat([buffer, chunk]);
145
- // The optional fixed-width title slot is a physical first line that is
146
- // NOT JSON; peel it before the parser would (correctly) reject it. The
147
- // first line ends at a '\n' byte, so it is a complete UTF-8 sequence and
148
- // safe to decode. A non-slot first line is a real entry and is left for
149
- // the parser; a blank first line is left for the parser to skip.
118
+ for await (const bytes of readLines(Bun.file(filePath).stream())) {
119
+ const line = decoder.decode(bytes);
150
120
  if (!sawFirstLine) {
151
- const newline = buffer.indexOf(0x0a);
152
- if (newline !== -1) {
153
- sawFirstLine = true;
154
- const firstLine = decoder.decode(buffer.subarray(0, newline)).trim();
155
- if (firstLine) {
156
- const slot = parseTitleSlotLine(firstLine);
157
- if (slot) {
158
- titleSlot = titleUpdateFromSlot(slot);
159
- buffer = buffer.subarray(newline + 1);
160
- }
161
- }
121
+ sawFirstLine = true;
122
+ const slot = parseTitleSlotLine(line.trim());
123
+ if (slot) {
124
+ titleSlot = titleUpdateFromSlot(slot);
125
+ continue;
162
126
  }
163
127
  }
164
- drain();
165
- }
166
- // A trailing record without a final newline: terminate it so the parser
167
- // can complete it (readline yielded it; parseChunk needs the delimiter).
168
- if (buffer.length > 0 && buffer[buffer.length - 1] !== 0x0a) {
169
- buffer = Buffer.concat([buffer, new Uint8Array([0x0a])]);
128
+ for (const entry of parseJsonlLenient<RawFileEntry>(line)) entries.push(entry as FileEntry);
170
129
  }
171
- drain();
172
130
  } catch (err) {
173
131
  if (isEnoent(err)) return { entries: [], titleSlot: undefined };
174
132
  throw err;
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { type } from "arktype";
9
9
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "jeopi-agent-core";
10
- import type { AuthStorage } from "jeopi-ai";
10
+ import type { Api, AuthStorage, Model } from "jeopi-ai";
11
11
  import { prompt } from "jeopi-utils";
12
12
  import { settings } from "../../config/settings";
13
13
  import type { CustomTool, CustomToolContext, RenderResultOptions } from "../../extensibility/custom-tools/types";
@@ -117,6 +117,7 @@ function hasRenderableSearchContent(response: SearchResponse): boolean {
117
117
 
118
118
  interface ExecuteSearchOptions {
119
119
  authStorage: AuthStorage;
120
+ model?: Model<Api>;
120
121
  sessionId?: string;
121
122
  signal?: AbortSignal;
122
123
  }
@@ -127,7 +128,7 @@ async function executeSearch(
127
128
  params: SearchQueryParams,
128
129
  options: ExecuteSearchOptions,
129
130
  ): Promise<{ content: Array<{ type: "text"; text: string }>; details: SearchRenderDetails }> {
130
- const { authStorage, sessionId, signal } = options;
131
+ const { authStorage, model, sessionId, signal } = options;
131
132
  const explicitProvider = params.provider;
132
133
  let providers: SearchProvider[];
133
134
  if (explicitProvider && explicitProvider !== "auto") {
@@ -174,6 +175,7 @@ async function executeSearch(
174
175
  numSearchResults: params.num_search_results,
175
176
  temperature: params.temperature,
176
177
  signal,
178
+ model,
177
179
  authStorage,
178
180
  sessionId,
179
181
  antigravityEndpointMode,
@@ -222,7 +224,7 @@ async function executeSearch(
222
224
  */
223
225
  export async function runSearchQuery(
224
226
  params: SearchQueryParams,
225
- options: { authStorage?: AuthStorage; sessionId?: string; signal?: AbortSignal } = {},
227
+ options: { authStorage?: AuthStorage; model?: Model<Api>; sessionId?: string; signal?: AbortSignal } = {},
226
228
  ): Promise<{ content: Array<{ type: "text"; text: string }>; details: SearchRenderDetails }> {
227
229
  const createdAuthStorage = options.authStorage ? undefined : await discoverAuthStorage();
228
230
  const authStorage = options.authStorage ?? createdAuthStorage;
@@ -232,6 +234,7 @@ export async function runSearchQuery(
232
234
  try {
233
235
  return await executeSearch("cli-web-search", params, {
234
236
  authStorage,
237
+ model: options.model,
235
238
  sessionId: options.sessionId,
236
239
  signal: options.signal,
237
240
  });
@@ -271,7 +274,12 @@ export class WebSearchTool implements AgentTool<typeof webSearchSchema, SearchRe
271
274
  ): Promise<AgentToolResult<SearchRenderDetails>> {
272
275
  const authStorage = this.#session.authStorage ?? (await discoverAuthStorage());
273
276
  const sessionId = this.#session.getSessionId?.() ?? undefined;
274
- return executeSearch(_toolCallId, params, { authStorage, sessionId, signal });
277
+ return executeSearch(_toolCallId, params, {
278
+ authStorage,
279
+ model: this.#session.getActiveModel?.(),
280
+ sessionId,
281
+ signal,
282
+ });
275
283
  }
276
284
  }
277
285
 
@@ -292,7 +300,7 @@ export const webSearchCustomTool: CustomTool<typeof webSearchSchema, SearchRende
292
300
  ) {
293
301
  const authStorage = ctx.modelRegistry?.authStorage ?? (await discoverAuthStorage());
294
302
  const sessionId = ctx.sessionManager.getSessionId();
295
- return executeSearch(toolCallId, params, { authStorage, sessionId, signal });
303
+ return executeSearch(toolCallId, params, { authStorage, model: ctx.model, sessionId, signal });
296
304
  },
297
305
 
298
306
  renderCall(args: SearchToolParams, options: RenderResultOptions, theme: Theme) {
@@ -1,6 +1,17 @@
1
- import type { AuthStorage, FetchImpl } from "jeopi-ai";
1
+ import type { Api, AuthStorage, FetchImpl, Model } from "jeopi-ai";
2
2
  import type { SearchProviderId, SearchResponse } from "../types";
3
3
 
4
+ export type GeminiOAuthProvider = "google-gemini-cli" | "google-antigravity";
5
+
6
+ /** Return the OAuth provider owned by a matching Gemini model, if any. */
7
+ export function resolveGeminiOAuthProvider(model?: Model<Api>): GeminiOAuthProvider | undefined {
8
+ if (!model?.id.startsWith("gemini-")) return undefined;
9
+ if (model.provider === "google-antigravity" || model.provider === "google-gemini-cli") {
10
+ return model.provider;
11
+ }
12
+ return undefined;
13
+ }
14
+
4
15
  /**
5
16
  * Shared web search parameters passed to providers.
6
17
  *
@@ -51,6 +62,8 @@ export interface SearchParams {
51
62
  * caller's agent session when available; otherwise omit.
52
63
  */
53
64
  sessionId?: string;
65
+ /** Active session model, when the caller has one. Providers may use it only when it matches their protocol. */
66
+ model?: Model<Api>;
54
67
  antigravityEndpointMode?: "auto" | "production" | "sandbox";
55
68
  }
56
69
 
@@ -8,7 +8,7 @@
8
8
  * sibling SQLite store and never POSTs the broker sentinel to a Google token
9
9
  * endpoint.
10
10
  */
11
- import { type AuthStorage, type FetchImpl, type OAuthAccess, withOAuthAccess } from "jeopi-ai";
11
+ import { type Api, type AuthStorage, type FetchImpl, type Model, type OAuthAccess, withOAuthAccess } from "jeopi-ai";
12
12
  import {
13
13
  ANTIGRAVITY_SYSTEM_INSTRUCTION,
14
14
  getAntigravityUserAgent,
@@ -18,8 +18,8 @@ import { fetchWithRetry } from "jeopi-utils";
18
18
 
19
19
  import type { SearchCitation, SearchResponse, SearchSource } from "../../../web/search/types";
20
20
  import { SearchProviderError } from "../../../web/search/types";
21
- import type { SearchParams } from "./base";
22
- import { SearchProvider } from "./base";
21
+ import type { GeminiOAuthProvider, SearchParams } from "./base";
22
+ import { resolveGeminiOAuthProvider, SearchProvider } from "./base";
23
23
  import { classifyProviderHttpError, withHardTimeout } from "./utils";
24
24
 
25
25
  const DEFAULT_ENDPOINT = "https://cloudcode-pa.googleapis.com";
@@ -52,6 +52,7 @@ export interface GeminiSearchParams extends GeminiToolParams {
52
52
  temperature?: number;
53
53
  signal?: AbortSignal;
54
54
  authStorage: AuthStorage;
55
+ model?: Model<Api>;
55
56
  sessionId?: string;
56
57
  fetch?: FetchImpl;
57
58
  antigravityEndpointMode?: "auto" | "production" | "sandbox";
@@ -102,8 +103,10 @@ export async function findGeminiAuth(
102
103
  authStorage: AuthStorage,
103
104
  sessionId: string | undefined,
104
105
  signal: AbortSignal | undefined,
106
+ preferredProvider?: GeminiOAuthProvider,
105
107
  ): Promise<GeminiAuthSeed | null> {
106
- for (const provider of GEMINI_PROVIDERS) {
108
+ const providers = preferredProvider ? [preferredProvider] : GEMINI_PROVIDERS;
109
+ for (const provider of providers) {
107
110
  const access = await authStorage.getOAuthAccess(provider, sessionId, { signal });
108
111
  if (!access?.accessToken || !access.projectId) continue;
109
112
  return { provider, access, projectId: access.projectId };
@@ -160,13 +163,16 @@ interface CloudCodeResponseChunk {
160
163
  response?: GeminiModelResponse;
161
164
  }
162
165
 
163
- async function parseGeminiSearchStream(body: ReadableStream<Uint8Array>): Promise<GeminiSearchResult> {
166
+ async function parseGeminiSearchStream(
167
+ body: ReadableStream<Uint8Array>,
168
+ fallbackModel: string = DEFAULT_MODEL,
169
+ ): Promise<GeminiSearchResult> {
164
170
  const answerParts: string[] = [];
165
171
  const sources: SearchSource[] = [];
166
172
  const citations: SearchCitation[] = [];
167
173
  const searchQueries: string[] = [];
168
174
  const seenUrls = new Set<string>();
169
- let model = DEFAULT_MODEL;
175
+ let model = fallbackModel;
170
176
  let usage: { inputTokens: number; outputTokens: number; totalTokens: number } | undefined;
171
177
 
172
178
  const reader = body.getReader();
@@ -287,6 +293,7 @@ async function parseGeminiSearchStream(body: ReadableStream<Uint8Array>): Promis
287
293
  */
288
294
  async function callGeminiSearch(
289
295
  auth: GeminiAuth,
296
+ modelId: string,
290
297
  query: string,
291
298
  systemPrompt: string | undefined,
292
299
  maxOutputTokens: number | undefined,
@@ -330,7 +337,7 @@ async function callGeminiSearch(
330
337
 
331
338
  const requestBody: Record<string, unknown> = {
332
339
  project: auth.projectId,
333
- model: DEFAULT_MODEL,
340
+ model: modelId,
334
341
  request: {
335
342
  contents: [
336
343
  {
@@ -414,7 +421,7 @@ async function callGeminiSearch(
414
421
  throw new SearchProviderError("gemini", "Gemini API returned no response body", 500);
415
422
  }
416
423
 
417
- return parseGeminiSearchStream(response.body);
424
+ return parseGeminiSearchStream(response.body, modelId);
418
425
  }
419
426
 
420
427
  async function callGeminiDeveloperSearch(
@@ -494,7 +501,10 @@ async function callGeminiDeveloperSearch(
494
501
  * Executes a web search using Google Gemini with Google Search grounding.
495
502
  */
496
503
  export async function searchGemini(params: GeminiSearchParams): Promise<SearchResponse> {
497
- const seed = await findGeminiAuth(params.authStorage, params.sessionId, params.signal);
504
+ const selectedProvider = resolveGeminiOAuthProvider(params.model);
505
+ const selectedModelId =
506
+ selectedProvider && params.model ? (params.model.requestModelId ?? params.model.id) : DEFAULT_MODEL;
507
+ const seed = await findGeminiAuth(params.authStorage, params.sessionId, params.signal, selectedProvider);
498
508
  let result: GeminiSearchResult;
499
509
 
500
510
  if (seed) {
@@ -513,6 +523,7 @@ export async function searchGemini(params: GeminiSearchParams): Promise<SearchRe
513
523
  projectId: access.projectId ?? seed.projectId,
514
524
  isAntigravity,
515
525
  },
526
+ selectedModelId,
516
527
  params.query,
517
528
  params.system_prompt,
518
529
  params.max_output_tokens,
@@ -528,6 +539,12 @@ export async function searchGemini(params: GeminiSearchParams): Promise<SearchRe
528
539
  ),
529
540
  { sessionId: params.sessionId, signal: params.signal, seed: seed.access },
530
541
  );
542
+ } else if (selectedProvider) {
543
+ throw new SearchProviderError(
544
+ "gemini",
545
+ `No OAuth credentials found for selected Gemini provider "${selectedProvider}".`,
546
+ 401,
547
+ );
531
548
  } else {
532
549
  const apiKey = await params.authStorage.getApiKey(DEVELOPER_API_PROVIDER, params.sessionId, {
533
550
  signal: params.signal,
@@ -594,8 +611,10 @@ export class GeminiProvider extends SearchProvider {
594
611
  url_context: params.urlContext,
595
612
  signal: params.signal,
596
613
  authStorage: params.authStorage,
614
+ model: params.model,
597
615
  sessionId: params.sessionId,
598
616
  fetch: params.fetch,
617
+ antigravityEndpointMode: params.antigravityEndpointMode,
599
618
  });
600
619
  }
601
620
  }