@vybestack/llxprt-code-core 0.5.0-nightly.251123.79a9619f1 → 0.5.0-nightly.251124.0158ea13c

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 (89) hide show
  1. package/README.md +0 -244
  2. package/dist/src/auth/types.d.ts +2 -2
  3. package/dist/src/config/config.d.ts +25 -8
  4. package/dist/src/config/config.js +27 -14
  5. package/dist/src/config/config.js.map +1 -1
  6. package/dist/src/config/constants.d.ts +11 -0
  7. package/dist/src/config/constants.js +16 -0
  8. package/dist/src/config/constants.js.map +1 -0
  9. package/dist/src/core/baseLlmClient.d.ts +77 -0
  10. package/dist/src/core/baseLlmClient.js +175 -0
  11. package/dist/src/core/baseLlmClient.js.map +1 -0
  12. package/dist/src/core/client.d.ts +10 -0
  13. package/dist/src/core/client.js +70 -109
  14. package/dist/src/core/client.js.map +1 -1
  15. package/dist/src/core/coreToolScheduler.d.ts +2 -0
  16. package/dist/src/core/coreToolScheduler.js +24 -4
  17. package/dist/src/core/coreToolScheduler.js.map +1 -1
  18. package/dist/src/core/geminiChat.js +21 -14
  19. package/dist/src/core/geminiChat.js.map +1 -1
  20. package/dist/src/core/turn.d.ts +1 -4
  21. package/dist/src/core/turn.js +2 -12
  22. package/dist/src/core/turn.js.map +1 -1
  23. package/dist/src/index.d.ts +2 -0
  24. package/dist/src/index.js +2 -0
  25. package/dist/src/index.js.map +1 -1
  26. package/dist/src/mcp/oauth-provider.js +2 -0
  27. package/dist/src/mcp/oauth-provider.js.map +1 -1
  28. package/dist/src/mcp/sa-impersonation-provider.d.ts +33 -0
  29. package/dist/src/mcp/sa-impersonation-provider.js +130 -0
  30. package/dist/src/mcp/sa-impersonation-provider.js.map +1 -0
  31. package/dist/src/providers/anthropic/AnthropicProvider.js +2 -2
  32. package/dist/src/providers/anthropic/AnthropicProvider.js.map +1 -1
  33. package/dist/src/providers/openai/OpenAIProvider.js +4 -4
  34. package/dist/src/providers/openai/OpenAIProvider.js.map +1 -1
  35. package/dist/src/services/fileSystemService.d.ts +9 -0
  36. package/dist/src/services/fileSystemService.js +12 -1
  37. package/dist/src/services/fileSystemService.js.map +1 -1
  38. package/dist/src/telemetry/types.d.ts +1 -1
  39. package/dist/src/telemetry/types.js.map +1 -1
  40. package/dist/src/tools/glob.d.ts +3 -2
  41. package/dist/src/tools/glob.js +1 -1
  42. package/dist/src/tools/glob.js.map +1 -1
  43. package/dist/src/tools/ls.d.ts +1 -1
  44. package/dist/src/tools/ls.js +1 -1
  45. package/dist/src/tools/ls.js.map +1 -1
  46. package/dist/src/tools/mcp-client.d.ts +6 -16
  47. package/dist/src/tools/mcp-client.js +22 -67
  48. package/dist/src/tools/mcp-client.js.map +1 -1
  49. package/dist/src/tools/memoryTool.d.ts +1 -0
  50. package/dist/src/tools/memoryTool.js +2 -0
  51. package/dist/src/tools/memoryTool.js.map +1 -1
  52. package/dist/src/tools/modifiable-tool.d.ts +1 -1
  53. package/dist/src/tools/modifiable-tool.js +9 -1
  54. package/dist/src/tools/modifiable-tool.js.map +1 -1
  55. package/dist/src/tools/shell.js +59 -3
  56. package/dist/src/tools/shell.js.map +1 -1
  57. package/dist/src/tools/smart-edit.d.ts +19 -0
  58. package/dist/src/tools/smart-edit.js +105 -3
  59. package/dist/src/tools/smart-edit.js.map +1 -1
  60. package/dist/src/tools/tool-error.d.ts +1 -0
  61. package/dist/src/tools/tool-error.js +1 -0
  62. package/dist/src/tools/tool-error.js.map +1 -1
  63. package/dist/src/utils/bfsFileSearch.d.ts +2 -2
  64. package/dist/src/utils/editor.js +5 -3
  65. package/dist/src/utils/editor.js.map +1 -1
  66. package/dist/src/utils/getFolderStructure.d.ts +2 -2
  67. package/dist/src/utils/getFolderStructure.js +1 -1
  68. package/dist/src/utils/getFolderStructure.js.map +1 -1
  69. package/dist/src/utils/llm-edit-fixer.js +10 -1
  70. package/dist/src/utils/llm-edit-fixer.js.map +1 -1
  71. package/dist/src/utils/memoryDiscovery.d.ts +1 -1
  72. package/dist/src/utils/memoryDiscovery.js +1 -1
  73. package/dist/src/utils/memoryDiscovery.js.map +1 -1
  74. package/dist/src/utils/memoryImportProcessor.js +13 -20
  75. package/dist/src/utils/memoryImportProcessor.js.map +1 -1
  76. package/dist/src/utils/retry.d.ts +5 -1
  77. package/dist/src/utils/retry.js +20 -5
  78. package/dist/src/utils/retry.js.map +1 -1
  79. package/dist/src/utils/schemaValidator.js +11 -1
  80. package/dist/src/utils/schemaValidator.js.map +1 -1
  81. package/dist/src/utils/shell-utils.d.ts +1 -0
  82. package/dist/src/utils/shell-utils.js +6 -2
  83. package/dist/src/utils/shell-utils.js.map +1 -1
  84. package/dist/src/utils/thoughtUtils.d.ts +21 -0
  85. package/dist/src/utils/thoughtUtils.js +39 -0
  86. package/dist/src/utils/thoughtUtils.js.map +1 -0
  87. package/dist/src/utils/tool-utils.js +2 -2
  88. package/dist/src/utils/tool-utils.js.map +1 -1
  89. package/package.json +2 -2
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import type { Content } from '@google/genai';
7
+ import type { ContentGenerator } from './contentGenerator.js';
8
+ /**
9
+ * Options for generateJson method
10
+ */
11
+ export interface GenerateJsonOptions {
12
+ prompt: string;
13
+ schema?: Record<string, unknown>;
14
+ model: string;
15
+ temperature?: number;
16
+ systemInstruction?: string;
17
+ promptId?: string;
18
+ }
19
+ /**
20
+ * Options for generateEmbedding method
21
+ */
22
+ export interface GenerateEmbeddingOptions {
23
+ text: string | string[];
24
+ model: string;
25
+ }
26
+ /**
27
+ * Options for countTokens method
28
+ */
29
+ export interface CountTokensOptions {
30
+ text?: string;
31
+ contents?: Content[];
32
+ model: string;
33
+ }
34
+ /**
35
+ * BaseLLMClient extracts stateless utility methods for LLM operations.
36
+ * Unlike the main Client class, this handles utility calls without conversation state.
37
+ *
38
+ * This implements the baseLlmClient pattern from upstream gemini-cli but adapted
39
+ * for llxprt's multi-provider architecture.
40
+ *
41
+ * Key features:
42
+ * - Multi-provider support (Anthropic, OpenAI, Gemini, Vertex AI)
43
+ * - Stateless operations (no conversation history)
44
+ * - Clean separation from GeminiClient
45
+ * - Dependency injection for testing
46
+ */
47
+ export declare class BaseLLMClient {
48
+ private readonly contentGenerator;
49
+ constructor(contentGenerator: ContentGenerator | null);
50
+ /**
51
+ * Generate structured JSON from a prompt with optional schema validation.
52
+ * Supports all providers through the ContentGenerator abstraction.
53
+ *
54
+ * @param options - Generation options including prompt, schema, model, etc.
55
+ * @returns Parsed JSON object
56
+ * @throws Error if generation fails or response cannot be parsed
57
+ */
58
+ generateJson<T = unknown>(options: GenerateJsonOptions): Promise<T>;
59
+ /**
60
+ * Generate embeddings for text input.
61
+ * Supports single text string or array of strings.
62
+ *
63
+ * @param options - Embedding options including text and model
64
+ * @returns Embedding vector(s) as number array(s)
65
+ * @throws Error if generation fails or response is invalid
66
+ */
67
+ generateEmbedding(options: GenerateEmbeddingOptions): Promise<number[] | number[][]>;
68
+ /**
69
+ * Count tokens in text or contents without making an API call to generate.
70
+ * Useful for checking context limits before generation.
71
+ *
72
+ * @param options - Options including text/contents and model
73
+ * @returns Token count
74
+ * @throws Error if counting fails
75
+ */
76
+ countTokens(options: CountTokensOptions): Promise<number>;
77
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { getResponseText } from '../utils/generateContentResponseUtilities.js';
7
+ import { getErrorMessage } from '../utils/errors.js';
8
+ /**
9
+ * Extracts JSON from a string that might be wrapped in markdown code blocks
10
+ * @param text - The raw text that might contain markdown-wrapped JSON
11
+ * @returns The extracted JSON string or the original text if no markdown found
12
+ */
13
+ function extractJsonFromMarkdown(text) {
14
+ // Try to match ```json ... ``` or ``` ... ```
15
+ const markdownMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
16
+ if (markdownMatch && markdownMatch[1]) {
17
+ return markdownMatch[1].trim();
18
+ }
19
+ // If no markdown found, return trimmed original text
20
+ return text.trim();
21
+ }
22
+ /**
23
+ * BaseLLMClient extracts stateless utility methods for LLM operations.
24
+ * Unlike the main Client class, this handles utility calls without conversation state.
25
+ *
26
+ * This implements the baseLlmClient pattern from upstream gemini-cli but adapted
27
+ * for llxprt's multi-provider architecture.
28
+ *
29
+ * Key features:
30
+ * - Multi-provider support (Anthropic, OpenAI, Gemini, Vertex AI)
31
+ * - Stateless operations (no conversation history)
32
+ * - Clean separation from GeminiClient
33
+ * - Dependency injection for testing
34
+ */
35
+ export class BaseLLMClient {
36
+ contentGenerator;
37
+ constructor(contentGenerator) {
38
+ this.contentGenerator = contentGenerator;
39
+ if (!contentGenerator) {
40
+ throw new Error('ContentGenerator is required');
41
+ }
42
+ }
43
+ /**
44
+ * Generate structured JSON from a prompt with optional schema validation.
45
+ * Supports all providers through the ContentGenerator abstraction.
46
+ *
47
+ * @param options - Generation options including prompt, schema, model, etc.
48
+ * @returns Parsed JSON object
49
+ * @throws Error if generation fails or response cannot be parsed
50
+ */
51
+ async generateJson(options) {
52
+ const { prompt, schema, model, temperature = 0, systemInstruction, promptId = 'baseLlmClient-generateJson', } = options;
53
+ try {
54
+ const contents = [
55
+ {
56
+ role: 'user',
57
+ parts: [{ text: prompt }],
58
+ },
59
+ ];
60
+ const config = {
61
+ temperature,
62
+ topP: 1,
63
+ };
64
+ if (systemInstruction) {
65
+ config.systemInstruction = { text: systemInstruction };
66
+ }
67
+ if (schema) {
68
+ config.responseJsonSchema = schema;
69
+ config.responseMimeType = 'application/json';
70
+ }
71
+ const result = await this.contentGenerator.generateContent({
72
+ model,
73
+ config,
74
+ contents,
75
+ }, promptId);
76
+ let text = getResponseText(result);
77
+ if (!text) {
78
+ throw new Error('API returned an empty response for generateJson.');
79
+ }
80
+ // Handle markdown wrapping
81
+ const prefix = '```json';
82
+ const suffix = '```';
83
+ if (text.startsWith(prefix) && text.endsWith(suffix)) {
84
+ text = text
85
+ .substring(prefix.length, text.length - suffix.length)
86
+ .trim();
87
+ }
88
+ try {
89
+ // Extract JSON from potential markdown wrapper
90
+ const cleanedText = extractJsonFromMarkdown(text);
91
+ return JSON.parse(cleanedText);
92
+ }
93
+ catch (parseError) {
94
+ throw new Error(`Failed to parse API response as JSON: ${getErrorMessage(parseError)}`);
95
+ }
96
+ }
97
+ catch (error) {
98
+ throw new Error(`Failed to generate JSON content: ${getErrorMessage(error)}`);
99
+ }
100
+ }
101
+ /**
102
+ * Generate embeddings for text input.
103
+ * Supports single text string or array of strings.
104
+ *
105
+ * @param options - Embedding options including text and model
106
+ * @returns Embedding vector(s) as number array(s)
107
+ * @throws Error if generation fails or response is invalid
108
+ */
109
+ async generateEmbedding(options) {
110
+ const { text, model } = options;
111
+ try {
112
+ const texts = Array.isArray(text) ? text : [text];
113
+ const embedContentResponse = await this.contentGenerator.embedContent({
114
+ model,
115
+ contents: texts,
116
+ });
117
+ if (!embedContentResponse.embeddings ||
118
+ embedContentResponse.embeddings.length === 0) {
119
+ throw new Error('No embeddings found in API response.');
120
+ }
121
+ if (embedContentResponse.embeddings.length !== texts.length) {
122
+ throw new Error(`API returned a mismatched number of embeddings. Expected ${texts.length}, got ${embedContentResponse.embeddings.length}.`);
123
+ }
124
+ const embeddings = embedContentResponse.embeddings.map((embedding, index) => {
125
+ const values = embedding.values;
126
+ if (!values || values.length === 0) {
127
+ throw new Error(`API returned an empty embedding for input text at index ${index}: "${texts[index]}"`);
128
+ }
129
+ return values;
130
+ });
131
+ // Return single array if input was a single string
132
+ return Array.isArray(text) ? embeddings : embeddings[0];
133
+ }
134
+ catch (error) {
135
+ throw new Error(`Failed to generate embedding: ${getErrorMessage(error)}`);
136
+ }
137
+ }
138
+ /**
139
+ * Count tokens in text or contents without making an API call to generate.
140
+ * Useful for checking context limits before generation.
141
+ *
142
+ * @param options - Options including text/contents and model
143
+ * @returns Token count
144
+ * @throws Error if counting fails
145
+ */
146
+ async countTokens(options) {
147
+ const { text, contents, model } = options;
148
+ try {
149
+ let requestContents;
150
+ if (contents) {
151
+ requestContents = contents;
152
+ }
153
+ else if (text) {
154
+ requestContents = [
155
+ {
156
+ role: 'user',
157
+ parts: [{ text }],
158
+ },
159
+ ];
160
+ }
161
+ else {
162
+ throw new Error('Either text or contents must be provided');
163
+ }
164
+ const response = await this.contentGenerator.countTokens({
165
+ model,
166
+ contents: requestContents,
167
+ });
168
+ return response.totalTokens ?? 0;
169
+ }
170
+ catch (error) {
171
+ throw new Error(`Failed to count tokens: ${getErrorMessage(error)}`);
172
+ }
173
+ }
174
+ }
175
+ //# sourceMappingURL=baseLlmClient.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseLlmClient.js","sourceRoot":"","sources":["../../../src/core/baseLlmClient.ts"],"names":[],"mappings":"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"}
@@ -59,6 +59,11 @@ export declare class GeminiClient {
59
59
  private readonly runtimeState;
60
60
  private _historyService?;
61
61
  private _unsubscribe?;
62
+ /**
63
+ * BaseLLMClient for stateless utility operations (generateJson, embeddings, etc.)
64
+ * Lazily initialized when needed
65
+ */
66
+ private _baseLlmClient?;
62
67
  /**
63
68
  * @plan PLAN-20251027-STATELESS5.P10
64
69
  * @requirement REQ-STAT5-003.1
@@ -74,6 +79,11 @@ export declare class GeminiClient {
74
79
  private lazyInitialize;
75
80
  getContentGenerator(): ContentGenerator;
76
81
  getUserTier(): UserTierId | undefined;
82
+ /**
83
+ * Get or create the BaseLLMClient for stateless utility operations.
84
+ * This is lazily initialized to avoid creating it when not needed.
85
+ */
86
+ private getBaseLlmClient;
77
87
  private processComplexityAnalysis;
78
88
  private shouldEscalateReminder;
79
89
  private isTodoToolCall;
@@ -7,7 +7,6 @@ import { getDirectoryContextString, getEnvironmentContext, } from '../utils/envi
7
7
  import { Turn, GeminiEventType, DEFAULT_AGENT_ID, } from './turn.js';
8
8
  import { CompressionStatus } from './turn.js';
9
9
  import { getCoreSystemPromptAsync, getCompressionPrompt } from './prompts.js';
10
- import { getResponseText } from '../utils/generateContentResponseUtilities.js';
11
10
  import { reportError } from '../utils/errorReporting.js';
12
11
  import { GeminiChat } from './geminiChat.js';
13
12
  import { DebugLogger } from '../debug/index.js';
@@ -30,6 +29,7 @@ import { TodoStore } from '../tools/todo-store.js';
30
29
  import { isFunctionResponse } from '../utils/messageInspectors.js';
31
30
  import { estimateTokens as estimateTextTokens } from '../utils/toolOutputLimiter.js';
32
31
  import { subscribeToAgentRuntimeState } from '../runtime/AgentRuntimeState.js';
32
+ import { BaseLLMClient } from './baseLlmClient.js';
33
33
  const COMPLEXITY_ESCALATION_TURN_THRESHOLD = 3;
34
34
  const TODO_PROMPT_SUFFIX = 'Use TODO List to organize this effort.';
35
35
  function isThinkingSupported(model) {
@@ -37,20 +37,6 @@ function isThinkingSupported(model) {
37
37
  return true;
38
38
  return false;
39
39
  }
40
- /**
41
- * Extracts JSON from a string that might be wrapped in markdown code blocks
42
- * @param text - The raw text that might contain markdown-wrapped JSON
43
- * @returns The extracted JSON string or the original text if no markdown found
44
- */
45
- function extractJsonFromMarkdown(text) {
46
- // Try to match ```json ... ``` or ``` ... ```
47
- const markdownMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
48
- if (markdownMatch && markdownMatch[1]) {
49
- return markdownMatch[1].trim();
50
- }
51
- // If no markdown found, return trimmed original text
52
- return text.trim();
53
- }
54
40
  /**
55
41
  * Returns the index of the content after the fraction of the total characters in the history.
56
42
  *
@@ -66,7 +52,6 @@ export function findCompressSplitPoint(contents, fraction) {
66
52
  let lastSplitPoint = 0;
67
53
  let cumulativeCharCount = 0;
68
54
  for (let i = 0; i < contents.length; i++) {
69
- cumulativeCharCount += charCounts[i];
70
55
  const content = contents[i];
71
56
  const hasFunctionResponse = content.parts?.some((part) => !!part.functionResponse);
72
57
  if (content.role === 'user' && !hasFunctionResponse) {
@@ -75,6 +60,7 @@ export function findCompressSplitPoint(contents, fraction) {
75
60
  }
76
61
  lastSplitPoint = i;
77
62
  }
63
+ cumulativeCharCount += charCounts[i];
78
64
  }
79
65
  const lastContent = contents[contents.length - 1];
80
66
  if (lastContent?.role === 'model' &&
@@ -127,6 +113,11 @@ export class GeminiClient {
127
113
  runtimeState;
128
114
  _historyService;
129
115
  _unsubscribe;
116
+ /**
117
+ * BaseLLMClient for stateless utility operations (generateJson, embeddings, etc.)
118
+ * Lazily initialized when needed
119
+ */
120
+ _baseLlmClient;
130
121
  /**
131
122
  * @plan PLAN-20251027-STATELESS5.P10
132
123
  * @requirement REQ-STAT5-003.1
@@ -222,6 +213,16 @@ export class GeminiClient {
222
213
  getUserTier() {
223
214
  return this.contentGenerator?.userTier;
224
215
  }
216
+ /**
217
+ * Get or create the BaseLLMClient for stateless utility operations.
218
+ * This is lazily initialized to avoid creating it when not needed.
219
+ */
220
+ getBaseLlmClient() {
221
+ if (!this._baseLlmClient) {
222
+ this._baseLlmClient = new BaseLLMClient(this.getContentGenerator());
223
+ }
224
+ return this._baseLlmClient;
225
+ }
225
226
  processComplexityAnalysis(analysis) {
226
227
  if (!this.todoToolsAvailable) {
227
228
  this.consecutiveComplexTurns = 0;
@@ -1082,78 +1083,61 @@ export class GeminiClient {
1082
1083
  }
1083
1084
  async generateJson(contents, schema, abortSignal, model, config = {}) {
1084
1085
  await this.lazyInitialize();
1085
- // Use the provided model parameter directly
1086
1086
  const modelToUse = model;
1087
1087
  try {
1088
1088
  const userMemory = this.config.getUserMemory();
1089
- // Provider name removed from prompt call signature
1090
1089
  const systemInstruction = await getCoreSystemPromptAsync(userMemory, modelToUse, this.getEnabledToolNamesForPrompt());
1091
- const requestConfig = {
1092
- abortSignal,
1093
- ...this.generateContentConfig,
1094
- ...config,
1090
+ // Convert Content[] to a single prompt for BaseLLMClient
1091
+ // This preserves the conversation context in the prompt
1092
+ const prompt = contents
1093
+ .map((c) => c.parts
1094
+ ?.map((p) => ('text' in p ? p.text : ''))
1095
+ .filter(Boolean)
1096
+ .join('\n'))
1097
+ .filter(Boolean)
1098
+ .join('\n\n');
1099
+ // Use BaseLLMClient for the core JSON generation
1100
+ // This delegates to the stateless utility layer
1101
+ const baseLlmClient = this.getBaseLlmClient();
1102
+ const apiCall = async () => {
1103
+ try {
1104
+ return await baseLlmClient.generateJson({
1105
+ prompt,
1106
+ schema,
1107
+ model: modelToUse,
1108
+ temperature: config.temperature ?? this.generateContentConfig.temperature,
1109
+ systemInstruction,
1110
+ promptId: this.lastPromptId || this.config.getSessionId(),
1111
+ });
1112
+ }
1113
+ catch (error) {
1114
+ // Preserve abort signal behavior
1115
+ if (abortSignal.aborted) {
1116
+ throw error;
1117
+ }
1118
+ throw error;
1119
+ }
1095
1120
  };
1096
- const apiCall = () => this.getContentGenerator().generateContent({
1097
- model: modelToUse,
1098
- config: {
1099
- ...requestConfig,
1100
- systemInstruction,
1101
- responseJsonSchema: schema,
1102
- responseMimeType: 'application/json',
1103
- },
1104
- contents,
1105
- }, this.lastPromptId || this.config.getSessionId());
1106
1121
  const result = await retryWithBackoff(apiCall);
1107
- let text = getResponseText(result);
1108
- if (!text) {
1109
- const error = new Error('API returned an empty response for generateJson.');
1110
- await reportError(error, 'Error in generateJson: API returned an empty response.', contents, 'generateJson-empty-response');
1111
- throw error;
1112
- }
1113
- const prefix = '```json';
1114
- const suffix = '```';
1115
- if (text.startsWith(prefix) && text.endsWith(suffix)) {
1116
- // Note: upstream added logMalformedJsonResponse here but our telemetry doesn't have it
1117
- text = text
1118
- .substring(prefix.length, text.length - suffix.length)
1119
- .trim();
1120
- }
1121
- try {
1122
- // Extract JSON from potential markdown wrapper
1123
- const cleanedText = extractJsonFromMarkdown(text);
1124
- // Special case: Gemini sometimes returns just "user" or "model" for next speaker checks
1125
- // This happens particularly with non-ASCII content in the conversation
1126
- if ((cleanedText === 'user' || cleanedText === 'model') &&
1127
- contents.some((c) => c.parts?.some((p) => 'text' in p && p.text?.includes('next_speaker')))) {
1128
- this.logger.warn(() => `[generateJson] Gemini returned plain text "${cleanedText}" instead of JSON for next speaker check. Converting to valid response.`);
1129
- return {
1130
- reasoning: 'Gemini returned plain text response',
1131
- next_speaker: cleanedText,
1132
- };
1133
- }
1134
- return JSON.parse(cleanedText);
1135
- }
1136
- catch (parseError) {
1137
- // Log both the original and cleaned text for debugging
1138
- await reportError(parseError, 'Failed to parse JSON response from generateJson.', {
1139
- responseTextFailedToParse: text,
1140
- cleanedTextFailedToParse: extractJsonFromMarkdown(text),
1141
- originalRequestContents: contents,
1142
- }, 'generateJson-parse');
1143
- throw new Error(`Failed to parse API response as JSON: ${getErrorMessage(parseError)}`);
1122
+ // Special case: Gemini sometimes returns just "user" or "model" for next speaker checks
1123
+ // This happens particularly with non-ASCII content in the conversation
1124
+ if (typeof result === 'string' &&
1125
+ (result === 'user' || result === 'model') &&
1126
+ contents.some((c) => c.parts?.some((p) => 'text' in p && p.text?.includes('next_speaker')))) {
1127
+ this.logger.warn(() => `[generateJson] Gemini returned plain text "${result}" instead of JSON for next speaker check. Converting to valid response.`);
1128
+ return {
1129
+ reasoning: 'Gemini returned plain text response',
1130
+ next_speaker: result,
1131
+ };
1144
1132
  }
1133
+ return result;
1145
1134
  }
1146
1135
  catch (error) {
1147
1136
  if (abortSignal.aborted) {
1148
1137
  throw error;
1149
1138
  }
1150
- // Avoid double reporting for the empty response case handled above
1151
- if (error instanceof Error &&
1152
- error.message === 'API returned an empty response for generateJson.') {
1153
- throw error;
1154
- }
1155
1139
  await reportError(error, 'Error generating JSON content via API.', contents, 'generateJson-api');
1156
- throw new Error(`Failed to generate JSON content: ${getErrorMessage(error)}`);
1140
+ throw error;
1157
1141
  }
1158
1142
  }
1159
1143
  async generateContent(contents, generationConfig, abortSignal, model) {
@@ -1196,25 +1180,14 @@ export class GeminiClient {
1196
1180
  if (!texts || texts.length === 0) {
1197
1181
  return [];
1198
1182
  }
1199
- const embedModelParams = {
1183
+ // Delegate to BaseLLMClient for stateless embedding generation
1184
+ const baseLlmClient = this.getBaseLlmClient();
1185
+ const result = await baseLlmClient.generateEmbedding({
1186
+ text: texts,
1200
1187
  model: this.embeddingModel,
1201
- contents: texts,
1202
- };
1203
- const embedContentResponse = await this.getContentGenerator().embedContent(embedModelParams);
1204
- if (!embedContentResponse.embeddings ||
1205
- embedContentResponse.embeddings.length === 0) {
1206
- throw new Error('No embeddings found in API response.');
1207
- }
1208
- if (embedContentResponse.embeddings.length !== texts.length) {
1209
- throw new Error(`API returned a mismatched number of embeddings. Expected ${texts.length}, got ${embedContentResponse.embeddings.length}.`);
1210
- }
1211
- return embedContentResponse.embeddings.map((embedding, index) => {
1212
- const values = embedding.values;
1213
- if (!values || values.length === 0) {
1214
- throw new Error(`API returned an empty embedding for input text at index ${index}: "${texts[index]}"`);
1215
- }
1216
- return values;
1217
1188
  });
1189
+ // Result is already validated by BaseLLMClient
1190
+ return result;
1218
1191
  }
1219
1192
  /**
1220
1193
  * Manually trigger chat compression
@@ -1239,24 +1212,12 @@ export class GeminiClient {
1239
1212
  compressionStatus: CompressionStatus.NOOP,
1240
1213
  };
1241
1214
  }
1242
- // Note: chat variable used later in method
1215
+ // Use lastPromptTokenCount from telemetry service as the source of truth
1216
+ // This is more accurate than estimating from history
1217
+ const originalTokenCount = uiTelemetryService.getLastPromptTokenCount();
1243
1218
  // @plan PLAN-20251027-STATELESS5.P10
1244
1219
  // @requirement REQ-STAT5-003.1
1245
1220
  const model = this.runtimeState.model;
1246
- // Get the ACTUAL token count from the history service, not the curated subset
1247
- const historyService = this.getChat().getHistoryService();
1248
- const originalTokenCount = historyService
1249
- ? historyService.getTotalTokens()
1250
- : 0;
1251
- if (originalTokenCount === undefined) {
1252
- console.warn(`Could not determine token count for model ${model}.`);
1253
- this.hasFailedCompressionAttempt = !force && true;
1254
- return {
1255
- originalTokenCount: 0,
1256
- newTokenCount: 0,
1257
- compressionStatus: CompressionStatus.COMPRESSION_FAILED_TOKEN_COUNT_ERROR,
1258
- };
1259
- }
1260
1221
  const contextPercentageThreshold = this.config.getChatCompression()?.contextPercentageThreshold;
1261
1222
  // Don't compress if not forced and we are under the limit.
1262
1223
  if (!force) {
@@ -1319,11 +1280,9 @@ export class GeminiClient {
1319
1280
  compressionStatus: CompressionStatus.COMPRESSION_FAILED_TOKEN_COUNT_ERROR,
1320
1281
  };
1321
1282
  }
1322
- uiTelemetryService.setLastPromptTokenCount(newTokenCount);
1323
1283
  // TODO: Add proper telemetry logging once available
1324
1284
  console.debug(`Chat compression: ${originalTokenCount} -> ${newTokenCount} tokens`);
1325
1285
  if (newTokenCount > originalTokenCount) {
1326
- this.getChat().setHistory(curatedHistory);
1327
1286
  this.hasFailedCompressionAttempt = !force && true;
1328
1287
  return {
1329
1288
  originalTokenCount,
@@ -1333,6 +1292,8 @@ export class GeminiClient {
1333
1292
  }
1334
1293
  else {
1335
1294
  this.chat = compressedChat; // Chat compression successful, set new state.
1295
+ // Update telemetry service with new token count
1296
+ uiTelemetryService.setLastPromptTokenCount(newTokenCount);
1336
1297
  // Emit token update event for the new compressed chat
1337
1298
  // This ensures the UI updates with the new token count
1338
1299
  // Only emit if compression was successful