@koreanhole/opencodex 3.1.0 → 3.2.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.
@@ -1,26 +1,11 @@
1
- import {
2
- AcpClient,
3
- AcpError,
4
- type AcpDeps,
5
- } from "../acp-client";
6
1
  import { geminiBinaryFingerprintSync, resolveGeminiBinary } from "./binary";
7
- import {
8
- buildGeminiAcpEnv,
9
- geminiAcpArgs,
10
- geminiAcpEnabled,
11
- geminiModelIds,
12
- probeGeminiAcpSupport,
13
- stageGeminiAcpWorkspace,
14
- } from "./acp";
2
+ import { geminiHeadlessEnabled } from "./headless";
15
3
  import { discoverGeminiModelMetadata, mergeGeminiModelDescriptors } from "./model-metadata";
16
- import {
17
- getGeminiRuntime,
18
- recordGeminiCompressionThreshold,
19
- recordGeminiInitialize,
20
- } from "./runtime";
4
+ import { getGeminiRuntime, recordGeminiCompressionThreshold } from "./runtime";
5
+ import type { CliAgentTurnDeps } from "../types";
21
6
  import type { OcxProviderModel } from "../../../types";
22
7
 
23
- export type GeminiLiveModelsFailure = "not_found" | "unsupported" | "auth_required" | "backoff" | "transport";
8
+ export type GeminiLiveModelsFailure = "not_found" | "unsupported" | "backoff" | "transport";
24
9
 
25
10
  export type GeminiLiveModelsResult =
26
11
  | {
@@ -42,7 +27,7 @@ export type GeminiLiveModelsResult =
42
27
  export interface GeminiLiveModelsOptions {
43
28
  binary?: string;
44
29
  env?: NodeJS.ProcessEnv;
45
- deps?: AcpDeps;
30
+ deps?: CliAgentTurnDeps;
46
31
  packageRoot?: string;
47
32
  ttlMs?: number;
48
33
  forceRefresh?: boolean;
@@ -82,14 +67,14 @@ function staleResult(fingerprint: string, entry: CacheEntry | undefined): Gemini
82
67
  };
83
68
  }
84
69
 
85
- /** Discover Gemini's advertised model roster without a paid completion. */
70
+ /** Discover the installed Gemini CLI roster from local package metadata. */
86
71
  export async function discoverGeminiLiveModels(options: GeminiLiveModelsOptions = {}): Promise<GeminiLiveModelsResult> {
87
72
  const sourceEnv = options.env ?? process.env;
88
73
  const now = options.now ?? Date.now;
89
74
  const ttlMs = options.ttlMs ?? 5 * 60_000;
90
75
  const failureBackoffMs = options.failureBackoffMs ?? 60_000;
91
- if (!geminiAcpEnabled(sourceEnv)) {
92
- return { ok: false, reason: "unsupported", detail: "ACP is disabled by OPENCODEX_GEMINI_ACP=0." };
76
+ if (!geminiHeadlessEnabled(sourceEnv)) {
77
+ return { ok: false, reason: "unsupported", detail: "Gemini provider is disabled by OPENCODEX_GEMINI_ACP=0." };
93
78
  }
94
79
  const binary = options.binary ?? resolveGeminiBinary(undefined, sourceEnv);
95
80
  if (!binary) return { ok: false, reason: "not_found", detail: "Gemini CLI not found on PATH." };
@@ -105,33 +90,7 @@ export async function discoverGeminiLiveModels(options: GeminiLiveModelsOptions
105
90
  ?? { ok: false, reason: "backoff", detail: "Gemini model discovery is cooling down after a failure.", fingerprint };
106
91
  }
107
92
 
108
- const env = buildGeminiAcpEnv(sourceEnv, options.deps?.platform ?? process.platform);
109
- const mode = await probeGeminiAcpSupport(binary, env, { ...options.deps, env: sourceEnv });
110
- if (!mode.acp) {
111
- failureCache.set(fingerprint, timestamp);
112
- if (current) current.lastFailureAt = timestamp;
113
- return staleResult(fingerprint, current)
114
- ?? { ok: false, reason: "unsupported", detail: "Gemini --help does not advertise --acp.", fingerprint };
115
- }
116
- const workspace = await stageGeminiAcpWorkspace([], undefined, { sourceEnv });
117
- const sessionEnv = {
118
- ...env,
119
- GEMINI_CLI_HOME: workspace.geminiHome,
120
- GEMINI_CLI_SYSTEM_SETTINGS_PATH: workspace.settingsPath,
121
- };
122
- let client: AcpClient | undefined;
123
93
  try {
124
- client = await AcpClient.connect({
125
- binary,
126
- env: sessionEnv,
127
- cwd: workspace.cwd,
128
- args: geminiAcpArgs(workspace.policyPath),
129
- label: "Gemini ACP",
130
- fallbackAllowed: false,
131
- }, options.deps);
132
- if (client.initializeResult) await recordGeminiInitialize(fingerprint, client.initializeResult, sourceEnv);
133
- const session = await client.newSession({ cwd: workspace.cwd, mcpServers: [] });
134
- const models = geminiModelIds(session);
135
94
  const metadata = await discoverGeminiModelMetadata({
136
95
  binary,
137
96
  env: sourceEnv,
@@ -139,9 +98,16 @@ export async function discoverGeminiLiveModels(options: GeminiLiveModelsOptions
139
98
  ...(options.now ? { now: options.now } : {}),
140
99
  ...(options.forceRefresh ? { ttlMs: 0 } : {}),
141
100
  });
101
+ const ids = Object.keys(metadata.capabilities);
102
+ if (ids.length === 0) {
103
+ failureCache.set(fingerprint, timestamp);
104
+ if (current) current.lastFailureAt = timestamp;
105
+ return staleResult(fingerprint, current)
106
+ ?? { ok: false, reason: "transport", detail: metadata.warning ?? "Gemini model metadata was not found.", fingerprint };
107
+ }
142
108
  await recordGeminiCompressionThreshold(fingerprint, metadata.compressionThreshold, sourceEnv);
143
109
  const runtime = await getGeminiRuntime(fingerprint, sourceEnv);
144
- const modelInfo = mergeGeminiModelDescriptors(session.availableModels, metadata).map(model => {
110
+ const modelInfo = mergeGeminiModelDescriptors(ids.map(modelId => ({ modelId })), metadata).map(model => {
145
111
  const observed = runtime.models[model.id];
146
112
  const contextWindow = observed?.contextSize ?? model.contextWindowTokens;
147
113
  return {
@@ -152,32 +118,21 @@ export async function discoverGeminiLiveModels(options: GeminiLiveModelsOptions
152
118
  ...(runtime.compressionThreshold !== undefined
153
119
  ? { compressionThreshold: runtime.compressionThreshold }
154
120
  : {}),
155
- inputModalities: [
156
- "text" as const,
157
- ...(runtime.prompt.image ? ["image" as const] : []),
158
- ],
159
- loadSession: runtime.loadSession,
121
+ inputModalities: ["text" as const],
122
+ loadSession: false,
160
123
  };
161
124
  });
162
- const source = session.availableModels.length > 0 ? "models" : "default";
163
- cache.set(fingerprint, { models, modelInfo, source, fetchedAt: timestamp, ...(metadata.warning ? { metadataWarning: metadata.warning } : {}) });
125
+ cache.set(fingerprint, { models: ids, modelInfo, source: "models", fetchedAt: timestamp, ...(metadata.warning ? { metadataWarning: metadata.warning } : {}) });
164
126
  failureCache.delete(fingerprint);
165
- return { ok: true, models, modelInfo, source, fingerprint, ...(metadata.warning ? { metadataWarning: metadata.warning } : {}) };
127
+ return { ok: true, models: ids, modelInfo, source: "models", fingerprint, ...(metadata.warning ? { metadataWarning: metadata.warning } : {}) };
166
128
  } catch (error) {
167
129
  failureCache.set(fingerprint, timestamp);
168
130
  if (current) current.lastFailureAt = timestamp;
169
- const stale = staleResult(fingerprint, current);
170
- if (error instanceof AcpError && error.authRequired) {
171
- return stale ?? { ok: false, reason: "auth_required", detail: error.message, fingerprint };
172
- }
173
- return stale ?? {
131
+ return staleResult(fingerprint, current) ?? {
174
132
  ok: false,
175
133
  reason: "transport",
176
134
  detail: error instanceof Error ? error.message : String(error),
177
135
  fingerprint,
178
136
  };
179
- } finally {
180
- await client?.terminate();
181
- await workspace.cleanup();
182
137
  }
183
138
  }
@@ -1,7 +1,6 @@
1
1
  import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { getConfigDir } from "../../../config";
4
- import type { AcpInitialize } from "../acp-client";
5
4
 
6
5
  export interface GeminiModelRuntime {
7
6
  contextSize?: number;
@@ -12,12 +11,6 @@ export interface GeminiModelRuntime {
12
11
  }
13
12
 
14
13
  export interface GeminiRuntime {
15
- loadSession: boolean;
16
- prompt: {
17
- image: boolean;
18
- audio: boolean;
19
- embeddedContext: boolean;
20
- };
21
14
  compressionThreshold?: number;
22
15
  models: Record<string, GeminiModelRuntime>;
23
16
  }
@@ -38,19 +31,12 @@ function runtimePath(env: NodeJS.ProcessEnv): string {
38
31
  }
39
32
 
40
33
  function emptyRuntime(): GeminiRuntime {
41
- return {
42
- loadSession: false,
43
- prompt: { image: false, audio: false, embeddedContext: false },
44
- models: {},
45
- };
34
+ return { models: {} };
46
35
  }
47
36
 
48
37
  function parseRuntime(value: unknown): GeminiRuntime | undefined {
49
38
  if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
50
39
  const row = value as Record<string, unknown>;
51
- const prompt = row.prompt && typeof row.prompt === "object" && !Array.isArray(row.prompt)
52
- ? row.prompt as Record<string, unknown>
53
- : {};
54
40
  const models: Record<string, GeminiModelRuntime> = {};
55
41
  if (row.models && typeof row.models === "object" && !Array.isArray(row.models)) {
56
42
  for (const [modelId, raw] of Object.entries(row.models as Record<string, unknown>)) {
@@ -67,12 +53,6 @@ function parseRuntime(value: unknown): GeminiRuntime | undefined {
67
53
  }
68
54
  const threshold = positive(row.compressionThreshold);
69
55
  return {
70
- loadSession: row.loadSession === true,
71
- prompt: {
72
- image: prompt.image === true,
73
- audio: prompt.audio === true,
74
- embeddedContext: prompt.embeddedContext === true,
75
- },
76
56
  ...(threshold !== undefined && threshold <= 1 ? { compressionThreshold: threshold } : {}),
77
57
  models,
78
58
  };
@@ -123,47 +103,6 @@ export async function getGeminiRuntime(
123
103
  return runtimeCache.get(fingerprint) ?? emptyRuntime();
124
104
  }
125
105
 
126
- export async function recordGeminiInitialize(
127
- fingerprint: string,
128
- initialize: AcpInitialize,
129
- env: NodeJS.ProcessEnv = process.env,
130
- ): Promise<void> {
131
- await readDisk(env);
132
- const current = runtimeCache.get(fingerprint) ?? emptyRuntime();
133
- runtimeCache.set(fingerprint, {
134
- ...current,
135
- loadSession: initialize.capabilities.loadSession,
136
- prompt: { ...initialize.capabilities.prompt },
137
- });
138
- await persist(env).catch(() => undefined);
139
- }
140
-
141
- export async function recordGeminiUsage(
142
- fingerprint: string,
143
- modelId: string,
144
- usedTokens: number,
145
- contextSize: number,
146
- promptChars: number,
147
- env: NodeJS.ProcessEnv = process.env,
148
- ): Promise<void> {
149
- if (!positive(contextSize) || !nonNegative(usedTokens) || !nonNegative(promptChars)) return;
150
- await readDisk(env);
151
- const current = runtimeCache.get(fingerprint) ?? emptyRuntime();
152
- const previous = current.models[modelId];
153
- const charsPerToken = usedTokens > 0 && promptChars > 0
154
- ? promptChars / usedTokens
155
- : previous?.charsPerToken;
156
- current.models[modelId] = {
157
- contextSize,
158
- usedTokens,
159
- promptChars,
160
- ...(charsPerToken !== undefined ? { charsPerToken } : {}),
161
- observedAt: Date.now(),
162
- };
163
- runtimeCache.set(fingerprint, current);
164
- await persist(env).catch(() => undefined);
165
- }
166
-
167
106
  export async function recordGeminiCompressionThreshold(
168
107
  fingerprint: string,
169
108
  threshold: number | undefined,
@@ -19,6 +19,7 @@ export type StreamMessage = Record<string, unknown>;
19
19
  export async function* readJsonLines(
20
20
  chunks: AsyncIterable<Uint8Array>,
21
21
  limits: { maxLineBytes?: number; maxTotalBytes?: number } = {},
22
+ label = "Cline",
22
23
  ): AsyncGenerator<StreamMessage> {
23
24
  const maxLineBytes = limits.maxLineBytes ?? MAX_STREAM_LINE_BYTES;
24
25
  const maxTotalBytes = limits.maxTotalBytes ?? MAX_STREAM_TOTAL_BYTES;
@@ -34,10 +35,10 @@ export async function* readJsonLines(
34
35
  try {
35
36
  parsed = JSON.parse(trimmed);
36
37
  } catch {
37
- throw new CodingAgentProtocolError(`Malformed Cline JSON frame: ${trimmed.slice(0, 64)}`);
38
+ throw new CodingAgentProtocolError(`Malformed ${label} JSON frame: ${trimmed.slice(0, 64)}`);
38
39
  }
39
40
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
40
- throw new CodingAgentProtocolError(`Non-object Cline JSON frame: ${trimmed.slice(0, 64)}`);
41
+ throw new CodingAgentProtocolError(`Non-object ${label} JSON frame: ${trimmed.slice(0, 64)}`);
41
42
  }
42
43
  yield parsed as StreamMessage;
43
44
  };
@@ -49,7 +50,7 @@ export async function* readJsonLines(
49
50
  const end = newline < 0 ? text.length : newline;
50
51
  const part = text.slice(start, end);
51
52
  lineBytes += Buffer.byteLength(part);
52
- if (lineBytes > maxLineBytes) throw new CodingAgentProtocolError("Cline stream line exceeded the byte limit");
53
+ if (lineBytes > maxLineBytes) throw new CodingAgentProtocolError(`${label} stream line exceeded the byte limit`);
53
54
  if (part) parts.push(part);
54
55
  if (newline < 0) break;
55
56
  const line = parts.join("");
@@ -62,7 +63,7 @@ export async function* readJsonLines(
62
63
 
63
64
  for await (const chunk of chunks) {
64
65
  totalBytes += chunk.byteLength;
65
- if (totalBytes > maxTotalBytes) throw new CodingAgentProtocolError("Cline stream exceeded the total byte limit");
66
+ if (totalBytes > maxTotalBytes) throw new CodingAgentProtocolError(`${label} stream exceeded the total byte limit`);
66
67
  yield* consume(decoder.decode(chunk, { stream: true }));
67
68
  }
68
69
  yield* consume(decoder.decode());
@@ -146,6 +146,10 @@ async function forwardStream(
146
146
  const reader = stream.getReader();
147
147
  const decoder = new TextDecoder();
148
148
  let buffer = "";
149
+ const ping = setInterval(() => {
150
+ if (ws.data.closed) return;
151
+ try { ws.ping(); } catch { /* client gone */ }
152
+ }, 5_000);
149
153
  try {
150
154
  while (!ws.data.closed && !signal.aborted) {
151
155
  const { done, value } = await reader.read();
@@ -165,6 +169,7 @@ async function forwardStream(
165
169
  }
166
170
  }
167
171
  } finally {
172
+ clearInterval(ping);
168
173
  try { await reader.cancel(); } catch { /* client gone */ }
169
174
  }
170
175
  }