@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.
- package/README.md +0 -244
- package/dist/src/auth/types.d.ts +2 -2
- package/dist/src/config/config.d.ts +25 -8
- package/dist/src/config/config.js +27 -14
- package/dist/src/config/config.js.map +1 -1
- package/dist/src/config/constants.d.ts +11 -0
- package/dist/src/config/constants.js +16 -0
- package/dist/src/config/constants.js.map +1 -0
- package/dist/src/core/baseLlmClient.d.ts +77 -0
- package/dist/src/core/baseLlmClient.js +175 -0
- package/dist/src/core/baseLlmClient.js.map +1 -0
- package/dist/src/core/client.d.ts +10 -0
- package/dist/src/core/client.js +70 -109
- package/dist/src/core/client.js.map +1 -1
- package/dist/src/core/coreToolScheduler.d.ts +2 -0
- package/dist/src/core/coreToolScheduler.js +24 -4
- package/dist/src/core/coreToolScheduler.js.map +1 -1
- package/dist/src/core/geminiChat.js +21 -14
- package/dist/src/core/geminiChat.js.map +1 -1
- package/dist/src/core/turn.d.ts +1 -4
- package/dist/src/core/turn.js +2 -12
- package/dist/src/core/turn.js.map +1 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcp/oauth-provider.js +2 -0
- package/dist/src/mcp/oauth-provider.js.map +1 -1
- package/dist/src/mcp/sa-impersonation-provider.d.ts +33 -0
- package/dist/src/mcp/sa-impersonation-provider.js +130 -0
- package/dist/src/mcp/sa-impersonation-provider.js.map +1 -0
- package/dist/src/providers/anthropic/AnthropicProvider.js +2 -2
- package/dist/src/providers/anthropic/AnthropicProvider.js.map +1 -1
- package/dist/src/providers/openai/OpenAIProvider.js +4 -4
- package/dist/src/providers/openai/OpenAIProvider.js.map +1 -1
- package/dist/src/services/fileSystemService.d.ts +9 -0
- package/dist/src/services/fileSystemService.js +12 -1
- package/dist/src/services/fileSystemService.js.map +1 -1
- package/dist/src/telemetry/types.d.ts +1 -1
- package/dist/src/telemetry/types.js.map +1 -1
- package/dist/src/tools/glob.d.ts +3 -2
- package/dist/src/tools/glob.js +1 -1
- package/dist/src/tools/glob.js.map +1 -1
- package/dist/src/tools/ls.d.ts +1 -1
- package/dist/src/tools/ls.js +1 -1
- package/dist/src/tools/ls.js.map +1 -1
- package/dist/src/tools/mcp-client.d.ts +6 -16
- package/dist/src/tools/mcp-client.js +22 -67
- package/dist/src/tools/mcp-client.js.map +1 -1
- package/dist/src/tools/memoryTool.d.ts +1 -0
- package/dist/src/tools/memoryTool.js +2 -0
- package/dist/src/tools/memoryTool.js.map +1 -1
- package/dist/src/tools/modifiable-tool.d.ts +1 -1
- package/dist/src/tools/modifiable-tool.js +9 -1
- package/dist/src/tools/modifiable-tool.js.map +1 -1
- package/dist/src/tools/shell.js +59 -3
- package/dist/src/tools/shell.js.map +1 -1
- package/dist/src/tools/smart-edit.d.ts +19 -0
- package/dist/src/tools/smart-edit.js +105 -3
- package/dist/src/tools/smart-edit.js.map +1 -1
- package/dist/src/tools/tool-error.d.ts +1 -0
- package/dist/src/tools/tool-error.js +1 -0
- package/dist/src/tools/tool-error.js.map +1 -1
- package/dist/src/utils/bfsFileSearch.d.ts +2 -2
- package/dist/src/utils/editor.js +5 -3
- package/dist/src/utils/editor.js.map +1 -1
- package/dist/src/utils/getFolderStructure.d.ts +2 -2
- package/dist/src/utils/getFolderStructure.js +1 -1
- package/dist/src/utils/getFolderStructure.js.map +1 -1
- package/dist/src/utils/llm-edit-fixer.js +10 -1
- package/dist/src/utils/llm-edit-fixer.js.map +1 -1
- package/dist/src/utils/memoryDiscovery.d.ts +1 -1
- package/dist/src/utils/memoryDiscovery.js +1 -1
- package/dist/src/utils/memoryDiscovery.js.map +1 -1
- package/dist/src/utils/memoryImportProcessor.js +13 -20
- package/dist/src/utils/memoryImportProcessor.js.map +1 -1
- package/dist/src/utils/retry.d.ts +5 -1
- package/dist/src/utils/retry.js +20 -5
- package/dist/src/utils/retry.js.map +1 -1
- package/dist/src/utils/schemaValidator.js +11 -1
- package/dist/src/utils/schemaValidator.js.map +1 -1
- package/dist/src/utils/shell-utils.d.ts +1 -0
- package/dist/src/utils/shell-utils.js +6 -2
- package/dist/src/utils/shell-utils.js.map +1 -1
- package/dist/src/utils/thoughtUtils.d.ts +21 -0
- package/dist/src/utils/thoughtUtils.js +39 -0
- package/dist/src/utils/thoughtUtils.js.map +1 -0
- package/dist/src/utils/tool-utils.js +2 -2
- package/dist/src/utils/tool-utils.js.map +1 -1
- 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;
|
package/dist/src/core/client.js
CHANGED
|
@@ -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
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
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
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|