byterover-cli 1.4.0 → 1.6.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.
- package/README.md +193 -12
- package/dist/core/domain/cipher/process/types.d.ts +1 -1
- package/dist/core/domain/entities/provider-config.d.ts +92 -0
- package/dist/core/domain/entities/provider-config.js +181 -0
- package/dist/core/domain/entities/provider-registry.d.ts +55 -0
- package/dist/core/domain/entities/provider-registry.js +74 -0
- package/dist/core/domain/errors/headless-prompt-error.d.ts +11 -0
- package/dist/core/domain/errors/headless-prompt-error.js +18 -0
- package/dist/core/interfaces/cipher/i-content-generator.d.ts +30 -0
- package/dist/core/interfaces/cipher/i-content-generator.js +12 -1
- package/dist/core/interfaces/cipher/message-factory.d.ts +4 -1
- package/dist/core/interfaces/cipher/message-factory.js +5 -0
- package/dist/core/interfaces/cipher/message-types.d.ts +19 -1
- package/dist/core/interfaces/i-cogit-pull-service.d.ts +0 -1
- package/dist/core/interfaces/i-memory-retrieval-service.d.ts +0 -1
- package/dist/core/interfaces/i-memory-storage-service.d.ts +0 -2
- package/dist/core/interfaces/i-provider-config-store.d.ts +88 -0
- package/dist/core/interfaces/i-provider-config-store.js +1 -0
- package/dist/core/interfaces/i-provider-keychain-store.d.ts +33 -0
- package/dist/core/interfaces/i-provider-keychain-store.js +1 -0
- package/dist/core/interfaces/i-space-service.d.ts +1 -2
- package/dist/core/interfaces/i-team-service.d.ts +1 -2
- package/dist/core/interfaces/i-user-service.d.ts +1 -2
- package/dist/core/interfaces/usecase/i-curate-use-case.d.ts +2 -0
- package/dist/core/interfaces/usecase/i-init-use-case.d.ts +9 -3
- package/dist/core/interfaces/usecase/i-login-use-case.d.ts +4 -1
- package/dist/core/interfaces/usecase/i-pull-use-case.d.ts +5 -3
- package/dist/core/interfaces/usecase/i-push-use-case.d.ts +6 -4
- package/dist/core/interfaces/usecase/i-query-use-case.d.ts +2 -0
- package/dist/core/interfaces/usecase/i-status-use-case.d.ts +1 -0
- package/dist/infra/cipher/agent/service-initializer.d.ts +1 -1
- package/dist/infra/cipher/agent/service-initializer.js +0 -1
- package/dist/infra/cipher/file-system/file-system-service.js +5 -5
- package/dist/infra/cipher/http/internal-llm-http-service.d.ts +40 -1
- package/dist/infra/cipher/http/internal-llm-http-service.js +153 -4
- package/dist/infra/cipher/llm/formatters/gemini-formatter.js +8 -1
- package/dist/infra/cipher/llm/generators/byterover-content-generator.d.ts +2 -3
- package/dist/infra/cipher/llm/generators/byterover-content-generator.js +20 -11
- package/dist/infra/cipher/llm/generators/openrouter-content-generator.d.ts +1 -0
- package/dist/infra/cipher/llm/generators/openrouter-content-generator.js +26 -0
- package/dist/infra/cipher/llm/internal-llm-service.d.ts +13 -0
- package/dist/infra/cipher/llm/internal-llm-service.js +75 -4
- package/dist/infra/cipher/llm/model-capabilities.d.ts +74 -0
- package/dist/infra/cipher/llm/model-capabilities.js +157 -0
- package/dist/infra/cipher/llm/openrouter-llm-service.d.ts +35 -1
- package/dist/infra/cipher/llm/openrouter-llm-service.js +216 -28
- package/dist/infra/cipher/llm/stream-processor.d.ts +22 -2
- package/dist/infra/cipher/llm/stream-processor.js +78 -4
- package/dist/infra/cipher/llm/thought-parser.d.ts +1 -1
- package/dist/infra/cipher/llm/thought-parser.js +5 -5
- package/dist/infra/cipher/llm/transformers/openrouter-stream-transformer.d.ts +49 -0
- package/dist/infra/cipher/llm/transformers/openrouter-stream-transformer.js +272 -0
- package/dist/infra/cipher/llm/transformers/reasoning-extractor.d.ts +71 -0
- package/dist/infra/cipher/llm/transformers/reasoning-extractor.js +253 -0
- package/dist/infra/cipher/process/process-service.js +1 -1
- package/dist/infra/cipher/session/chat-session.d.ts +2 -0
- package/dist/infra/cipher/session/chat-session.js +13 -2
- package/dist/infra/cipher/storage/message-storage-service.js +4 -0
- package/dist/infra/cipher/tools/implementations/bash-exec-tool.js +3 -3
- package/dist/infra/cipher/tools/implementations/task-tool.js +1 -1
- package/dist/infra/cogit/http-cogit-pull-service.js +1 -1
- package/dist/infra/cogit/http-cogit-push-service.js +0 -1
- package/dist/infra/http/authenticated-http-client.d.ts +1 -3
- package/dist/infra/http/authenticated-http-client.js +1 -5
- package/dist/infra/http/openrouter-api-client.d.ts +148 -0
- package/dist/infra/http/openrouter-api-client.js +161 -0
- package/dist/infra/mcp/tools/task-result-waiter.js +9 -1
- package/dist/infra/memory/http-memory-retrieval-service.js +1 -1
- package/dist/infra/memory/http-memory-storage-service.js +2 -2
- package/dist/infra/process/agent-worker.js +178 -70
- package/dist/infra/process/inline-agent-executor.d.ts +32 -0
- package/dist/infra/process/inline-agent-executor.js +259 -0
- package/dist/infra/process/transport-handlers.d.ts +25 -4
- package/dist/infra/process/transport-handlers.js +57 -10
- package/dist/infra/repl/commands/connectors-command.js +2 -2
- package/dist/infra/repl/commands/index.js +5 -0
- package/dist/infra/repl/commands/model-command.d.ts +13 -0
- package/dist/infra/repl/commands/model-command.js +212 -0
- package/dist/infra/repl/commands/provider-command.d.ts +13 -0
- package/dist/infra/repl/commands/provider-command.js +181 -0
- package/dist/infra/repl/transport-client-helper.js +6 -2
- package/dist/infra/space/http-space-service.d.ts +1 -1
- package/dist/infra/space/http-space-service.js +2 -2
- package/dist/infra/storage/file-provider-config-store.d.ts +83 -0
- package/dist/infra/storage/file-provider-config-store.js +157 -0
- package/dist/infra/storage/provider-keychain-store.d.ts +37 -0
- package/dist/infra/storage/provider-keychain-store.js +75 -0
- package/dist/infra/storage/token-store.d.ts +4 -3
- package/dist/infra/storage/token-store.js +6 -5
- package/dist/infra/team/http-team-service.d.ts +1 -1
- package/dist/infra/team/http-team-service.js +2 -2
- package/dist/infra/terminal/headless-terminal.d.ts +91 -0
- package/dist/infra/terminal/headless-terminal.js +211 -0
- package/dist/infra/transport/socket-io-transport-client.d.ts +20 -0
- package/dist/infra/transport/socket-io-transport-client.js +88 -1
- package/dist/infra/usecase/curate-use-case.d.ts +40 -1
- package/dist/infra/usecase/curate-use-case.js +176 -15
- package/dist/infra/usecase/init-use-case.d.ts +27 -5
- package/dist/infra/usecase/init-use-case.js +200 -34
- package/dist/infra/usecase/login-use-case.d.ts +10 -8
- package/dist/infra/usecase/login-use-case.js +35 -2
- package/dist/infra/usecase/pull-use-case.d.ts +19 -5
- package/dist/infra/usecase/pull-use-case.js +71 -13
- package/dist/infra/usecase/push-use-case.d.ts +18 -5
- package/dist/infra/usecase/push-use-case.js +81 -14
- package/dist/infra/usecase/query-use-case.d.ts +21 -0
- package/dist/infra/usecase/query-use-case.js +114 -29
- package/dist/infra/usecase/space-list-use-case.js +1 -1
- package/dist/infra/usecase/space-switch-use-case.js +2 -2
- package/dist/infra/usecase/status-use-case.d.ts +36 -0
- package/dist/infra/usecase/status-use-case.js +185 -48
- package/dist/infra/user/http-user-service.d.ts +1 -1
- package/dist/infra/user/http-user-service.js +2 -2
- package/dist/oclif/commands/curate.d.ts +6 -1
- package/dist/oclif/commands/curate.js +24 -3
- package/dist/oclif/commands/init.d.ts +18 -0
- package/dist/oclif/commands/init.js +129 -0
- package/dist/oclif/commands/login.d.ts +9 -0
- package/dist/oclif/commands/login.js +45 -0
- package/dist/oclif/commands/pull.d.ts +16 -0
- package/dist/oclif/commands/pull.js +78 -0
- package/dist/oclif/commands/push.d.ts +17 -0
- package/dist/oclif/commands/push.js +87 -0
- package/dist/oclif/commands/query.d.ts +6 -1
- package/dist/oclif/commands/query.js +29 -4
- package/dist/oclif/commands/status.d.ts +5 -1
- package/dist/oclif/commands/status.js +17 -5
- package/dist/resources/tools/bash_exec.txt +1 -1
- package/dist/tui/components/api-key-dialog.d.ts +39 -0
- package/dist/tui/components/api-key-dialog.js +94 -0
- package/dist/tui/components/execution/execution-changes.d.ts +3 -1
- package/dist/tui/components/execution/execution-changes.js +4 -4
- package/dist/tui/components/execution/execution-content.d.ts +1 -1
- package/dist/tui/components/execution/execution-content.js +4 -12
- package/dist/tui/components/execution/execution-input.js +1 -1
- package/dist/tui/components/execution/execution-progress.d.ts +10 -13
- package/dist/tui/components/execution/execution-progress.js +70 -17
- package/dist/tui/components/execution/execution-reasoning.d.ts +16 -0
- package/dist/tui/components/execution/execution-reasoning.js +34 -0
- package/dist/tui/components/execution/execution-tool.d.ts +23 -0
- package/dist/tui/components/execution/execution-tool.js +125 -0
- package/dist/tui/components/execution/expanded-log-view.js +3 -3
- package/dist/tui/components/execution/log-item.d.ts +2 -0
- package/dist/tui/components/execution/log-item.js +6 -4
- package/dist/tui/components/index.d.ts +2 -0
- package/dist/tui/components/index.js +2 -0
- package/dist/tui/components/inline-prompts/inline-select.js +3 -2
- package/dist/tui/components/model-dialog.d.ts +63 -0
- package/dist/tui/components/model-dialog.js +89 -0
- package/dist/tui/components/onboarding/onboarding-flow.js +8 -2
- package/dist/tui/components/provider-dialog.d.ts +27 -0
- package/dist/tui/components/provider-dialog.js +31 -0
- package/dist/tui/components/reasoning-text.d.ts +26 -0
- package/dist/tui/components/reasoning-text.js +49 -0
- package/dist/tui/components/selectable-list.d.ts +54 -0
- package/dist/tui/components/selectable-list.js +180 -0
- package/dist/tui/components/streaming-text.d.ts +30 -0
- package/dist/tui/components/streaming-text.js +52 -0
- package/dist/tui/contexts/tasks-context.d.ts +15 -0
- package/dist/tui/contexts/tasks-context.js +224 -40
- package/dist/tui/contexts/theme-context.d.ts +1 -0
- package/dist/tui/contexts/theme-context.js +3 -2
- package/dist/tui/hooks/use-activity-logs.js +7 -1
- package/dist/tui/hooks/use-auth-polling.js +1 -1
- package/dist/tui/types/messages.d.ts +32 -5
- package/dist/tui/utils/index.d.ts +1 -1
- package/dist/tui/utils/index.js +1 -1
- package/dist/tui/utils/log.d.ts +0 -9
- package/dist/tui/utils/log.js +2 -53
- package/dist/tui/views/command-view.js +4 -1
- package/dist/utils/environment-detector.d.ts +15 -0
- package/dist/utils/environment-detector.js +62 -1
- package/oclif.manifest.json +287 -5
- package/package.json +1 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { StreamChunkType } from '../../../core/interfaces/cipher/i-content-generator.js';
|
|
1
2
|
import { AuthenticatedHttpClient } from '../../http/authenticated-http-client.js';
|
|
3
|
+
import { ThoughtParser } from '../llm/thought-parser.js';
|
|
2
4
|
/**
|
|
3
5
|
* ByteRover HTTP LLM API client.
|
|
4
6
|
*
|
|
@@ -29,7 +31,6 @@ export class ByteRoverLlmHttpService {
|
|
|
29
31
|
*/
|
|
30
32
|
constructor(config) {
|
|
31
33
|
this.config = {
|
|
32
|
-
accessToken: config.accessToken,
|
|
33
34
|
apiBaseUrl: config.apiBaseUrl,
|
|
34
35
|
projectId: config.projectId ?? 'byterover',
|
|
35
36
|
region: config.region ?? 'us-east1',
|
|
@@ -72,6 +73,30 @@ export class ByteRoverLlmHttpService {
|
|
|
72
73
|
};
|
|
73
74
|
return this.callHttpGenerate(request);
|
|
74
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Call ByteRover REST LLM service to generate content with streaming.
|
|
78
|
+
*
|
|
79
|
+
* Currently falls back to non-streaming endpoint since /api/llm/generate/stream
|
|
80
|
+
* doesn't exist on the backend yet. Extracts thinking/reasoning from the complete
|
|
81
|
+
* response and yields them as separate chunks.
|
|
82
|
+
*
|
|
83
|
+
* When backend streaming is available, this will use SSE for true streaming.
|
|
84
|
+
*
|
|
85
|
+
* @param contents - For Gemini: Content[]. For Claude: MessageCreateParamsNonStreaming (complete body)
|
|
86
|
+
* @param config - For Gemini: GenerateContentConfig. For Claude: RequestOptions (optional HTTP options)
|
|
87
|
+
* @param model - Model to use (detects provider from model name)
|
|
88
|
+
* @param executionMetadata - Optional execution metadata (mode, executionContext)
|
|
89
|
+
* @yields GenerateContentChunk objects as they are generated
|
|
90
|
+
*/
|
|
91
|
+
async *generateContentStream(contents, config, model, executionMetadata) {
|
|
92
|
+
// Fall back to non-streaming endpoint and simulate streaming
|
|
93
|
+
// by extracting thinking from the complete response
|
|
94
|
+
const response = await this.generateContent(contents, config, model, executionMetadata);
|
|
95
|
+
// Extract and yield thinking/reasoning chunks first
|
|
96
|
+
yield* this.extractThinkingFromResponse(response);
|
|
97
|
+
// Then yield the final content
|
|
98
|
+
yield* this.extractContentFromResponse(response);
|
|
99
|
+
}
|
|
75
100
|
/**
|
|
76
101
|
* Call the ByteRover REST Generate endpoint.
|
|
77
102
|
*
|
|
@@ -83,11 +108,11 @@ export class ByteRoverLlmHttpService {
|
|
|
83
108
|
*/
|
|
84
109
|
async callHttpGenerate(request) {
|
|
85
110
|
const url = `${this.config.apiBaseUrl}/api/llm/generate`;
|
|
86
|
-
const httpClient = new AuthenticatedHttpClient(this.config.
|
|
87
|
-
const
|
|
111
|
+
const httpClient = new AuthenticatedHttpClient(this.config.sessionKey);
|
|
112
|
+
const httpResponse = await httpClient.post(url, request, {
|
|
88
113
|
timeout: this.config.timeout,
|
|
89
114
|
});
|
|
90
|
-
return
|
|
115
|
+
return httpResponse.data;
|
|
91
116
|
}
|
|
92
117
|
/**
|
|
93
118
|
* Detect LLM provider from model identifier.
|
|
@@ -113,4 +138,128 @@ export class ByteRoverLlmHttpService {
|
|
|
113
138
|
detectRegionFromModel(model) {
|
|
114
139
|
return model.toLowerCase().startsWith('claude') ? 'us-east5' : 'global';
|
|
115
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Extract content chunks from a complete response.
|
|
143
|
+
*
|
|
144
|
+
* Looks for text parts (excluding thinking) and function calls,
|
|
145
|
+
* yields them as final chunks.
|
|
146
|
+
*
|
|
147
|
+
* @param response - Complete GenerateContentResponse
|
|
148
|
+
* @yields GenerateContentChunk for content and tool calls
|
|
149
|
+
*/
|
|
150
|
+
*extractContentFromResponse(response) {
|
|
151
|
+
const { candidates } = response;
|
|
152
|
+
if (!candidates || candidates.length === 0) {
|
|
153
|
+
yield {
|
|
154
|
+
content: '',
|
|
155
|
+
finishReason: 'stop',
|
|
156
|
+
isComplete: true,
|
|
157
|
+
};
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const candidate = candidates[0];
|
|
161
|
+
const parts = candidate?.content?.parts;
|
|
162
|
+
const finishReason = this.mapFinishReason(candidate?.finishReason ?? 'STOP');
|
|
163
|
+
if (!parts || parts.length === 0) {
|
|
164
|
+
yield {
|
|
165
|
+
content: '',
|
|
166
|
+
finishReason,
|
|
167
|
+
isComplete: true,
|
|
168
|
+
};
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
// Collect text content (excluding thinking parts)
|
|
172
|
+
const textParts = [];
|
|
173
|
+
const functionCalls = [];
|
|
174
|
+
for (const part of parts) {
|
|
175
|
+
const partRecord = part;
|
|
176
|
+
// Skip thinking parts
|
|
177
|
+
if (partRecord.thought === true)
|
|
178
|
+
continue;
|
|
179
|
+
// Collect text
|
|
180
|
+
if (partRecord.text && typeof partRecord.text === 'string') {
|
|
181
|
+
textParts.push(partRecord.text);
|
|
182
|
+
}
|
|
183
|
+
// Collect function calls
|
|
184
|
+
if (partRecord.functionCall) {
|
|
185
|
+
functionCalls.push(partRecord.functionCall);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Yield final content chunk
|
|
189
|
+
yield {
|
|
190
|
+
content: textParts.join('').trimEnd(),
|
|
191
|
+
finishReason,
|
|
192
|
+
isComplete: true,
|
|
193
|
+
toolCalls: functionCalls.length > 0
|
|
194
|
+
? functionCalls.map((fc, index) => ({
|
|
195
|
+
function: {
|
|
196
|
+
arguments: JSON.stringify(fc.args ?? {}),
|
|
197
|
+
name: fc.name ?? '',
|
|
198
|
+
},
|
|
199
|
+
id: `call_${Date.now()}_${index}`,
|
|
200
|
+
type: 'function',
|
|
201
|
+
}))
|
|
202
|
+
: undefined,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Extract thinking/reasoning chunks from a complete response.
|
|
207
|
+
*
|
|
208
|
+
* Looks for parts with `thought: true` and yields them as THINKING chunks.
|
|
209
|
+
*
|
|
210
|
+
* @param response - Complete GenerateContentResponse
|
|
211
|
+
* @yields GenerateContentChunk for each thinking part
|
|
212
|
+
*/
|
|
213
|
+
*extractThinkingFromResponse(response) {
|
|
214
|
+
const { candidates } = response;
|
|
215
|
+
if (!candidates || candidates.length === 0)
|
|
216
|
+
return;
|
|
217
|
+
const parts = candidates[0]?.content?.parts;
|
|
218
|
+
if (!parts)
|
|
219
|
+
return;
|
|
220
|
+
let thinkingSubject;
|
|
221
|
+
for (const part of parts) {
|
|
222
|
+
const partRecord = part;
|
|
223
|
+
// Check for thinking part (thought: true)
|
|
224
|
+
if (partRecord.thought === true && partRecord.text && typeof partRecord.text === 'string') {
|
|
225
|
+
const delta = partRecord.text;
|
|
226
|
+
// Extract subject from **Subject** markdown if not already found
|
|
227
|
+
if (!thinkingSubject && delta) {
|
|
228
|
+
const parsed = ThoughtParser.parse(delta);
|
|
229
|
+
if (parsed.subject) {
|
|
230
|
+
thinkingSubject = parsed.subject;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
yield {
|
|
234
|
+
isComplete: false,
|
|
235
|
+
providerMetadata: {
|
|
236
|
+
subject: thinkingSubject,
|
|
237
|
+
},
|
|
238
|
+
reasoning: delta.trimEnd(),
|
|
239
|
+
type: StreamChunkType.THINKING,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Map provider finish reason to standard format.
|
|
246
|
+
*/
|
|
247
|
+
mapFinishReason(reason) {
|
|
248
|
+
switch (reason.toUpperCase()) {
|
|
249
|
+
case 'FUNCTION_CALL':
|
|
250
|
+
case 'TOOL_CALLS': {
|
|
251
|
+
return 'tool_calls';
|
|
252
|
+
}
|
|
253
|
+
case 'LENGTH':
|
|
254
|
+
case 'MAX_TOKENS': {
|
|
255
|
+
return 'max_tokens';
|
|
256
|
+
}
|
|
257
|
+
case 'STOP': {
|
|
258
|
+
return 'stop';
|
|
259
|
+
}
|
|
260
|
+
default: {
|
|
261
|
+
return 'stop';
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
116
265
|
}
|
|
@@ -72,8 +72,14 @@ export class GeminiMessageFormatter {
|
|
|
72
72
|
}
|
|
73
73
|
const textParts = [];
|
|
74
74
|
const functionCallsWithSignatures = [];
|
|
75
|
-
|
|
75
|
+
let thoughtText;
|
|
76
|
+
// Extract text, thoughts, and function calls from response parts
|
|
76
77
|
for (const part of candidate.content.parts) {
|
|
78
|
+
// Check for thought parts first (Gemini 2.5+ with includeThoughts: true)
|
|
79
|
+
if ('thought' in part && part.thought === true && 'text' in part && part.text) {
|
|
80
|
+
thoughtText = part.text;
|
|
81
|
+
continue; // Don't add thought to textParts
|
|
82
|
+
}
|
|
77
83
|
if ('text' in part && part.text) {
|
|
78
84
|
textParts.push(part.text);
|
|
79
85
|
}
|
|
@@ -102,6 +108,7 @@ export class GeminiMessageFormatter {
|
|
|
102
108
|
{
|
|
103
109
|
content: textParts.join('') || null,
|
|
104
110
|
role: 'assistant',
|
|
111
|
+
thought: thoughtText,
|
|
105
112
|
toolCalls,
|
|
106
113
|
},
|
|
107
114
|
];
|
|
@@ -60,9 +60,8 @@ export declare class ByteRoverContentGenerator implements IContentGenerator {
|
|
|
60
60
|
/**
|
|
61
61
|
* Generate content with streaming.
|
|
62
62
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* True streaming can be implemented when the gRPC service exposes the stream.
|
|
63
|
+
* Uses the HTTP service's streaming endpoint to yield chunks as they arrive.
|
|
64
|
+
* Handles both regular content and thinking/reasoning parts from Gemini models.
|
|
66
65
|
*
|
|
67
66
|
* @param request - Generation request
|
|
68
67
|
* @yields Content chunks as they are generated
|
|
@@ -117,24 +117,33 @@ export class ByteRoverContentGenerator {
|
|
|
117
117
|
/**
|
|
118
118
|
* Generate content with streaming.
|
|
119
119
|
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
* True streaming can be implemented when the gRPC service exposes the stream.
|
|
120
|
+
* Uses the HTTP service's streaming endpoint to yield chunks as they arrive.
|
|
121
|
+
* Handles both regular content and thinking/reasoning parts from Gemini models.
|
|
123
122
|
*
|
|
124
123
|
* @param request - Generation request
|
|
125
124
|
* @yields Content chunks as they are generated
|
|
126
125
|
* @returns Async generator yielding content chunks
|
|
127
126
|
*/
|
|
128
127
|
async *generateContentStream(request) {
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
128
|
+
// Format messages for provider
|
|
129
|
+
let formattedMessages = this.formatter.format(request.contents);
|
|
130
|
+
// For Gemini 3+ models, ensure function calls in the active loop have thought signatures
|
|
131
|
+
if (this.providerType === 'gemini') {
|
|
132
|
+
formattedMessages = ensureActiveLoopHasThoughtSignatures(formattedMessages, this.config.model);
|
|
133
|
+
}
|
|
134
|
+
// Build generation config
|
|
135
|
+
const genConfig = this.buildGenerationConfig(request.tools ?? {}, request.systemPrompt ?? '', formattedMessages);
|
|
136
|
+
// Build execution metadata from request
|
|
137
|
+
const executionMetadata = {
|
|
138
|
+
sessionId: request.taskId,
|
|
139
|
+
taskId: request.taskId,
|
|
140
|
+
...(request.executionContext && { executionContext: request.executionContext }),
|
|
137
141
|
};
|
|
142
|
+
// Determine contents and config based on provider
|
|
143
|
+
const contents = this.providerType === 'claude' ? genConfig : formattedMessages;
|
|
144
|
+
const config = this.providerType === 'claude' ? {} : genConfig;
|
|
145
|
+
// Stream from HTTP service
|
|
146
|
+
yield* this.httpService.generateContentStream(contents, config, this.config.model, executionMetadata);
|
|
138
147
|
}
|
|
139
148
|
/**
|
|
140
149
|
* Build Claude-specific generation configuration.
|
|
@@ -64,6 +64,7 @@ export declare class OpenRouterContentGenerator implements IContentGenerator {
|
|
|
64
64
|
* Generate content with streaming.
|
|
65
65
|
*
|
|
66
66
|
* Uses OpenAI SDK's native streaming support for real-time content generation.
|
|
67
|
+
* Includes rawChunk for native reasoning extraction by the stream transformer.
|
|
67
68
|
*
|
|
68
69
|
* @param request - Generation request
|
|
69
70
|
* @yields Content chunks as they are generated
|
|
@@ -121,6 +121,7 @@ export class OpenRouterContentGenerator {
|
|
|
121
121
|
* Generate content with streaming.
|
|
122
122
|
*
|
|
123
123
|
* Uses OpenAI SDK's native streaming support for real-time content generation.
|
|
124
|
+
* Includes rawChunk for native reasoning extraction by the stream transformer.
|
|
124
125
|
*
|
|
125
126
|
* @param request - Generation request
|
|
126
127
|
* @yields Content chunks as they are generated
|
|
@@ -161,10 +162,35 @@ export class OpenRouterContentGenerator {
|
|
|
161
162
|
const isComplete = choice.finish_reason !== null;
|
|
162
163
|
const finishReason = this.determineFinishReason(choice.finish_reason, isComplete);
|
|
163
164
|
const toolCalls = this.buildToolCallsArray(accumulatedToolCalls, isComplete);
|
|
165
|
+
// Extract native reasoning fields if present (for OpenAI o1/o3, Grok, Gemini)
|
|
166
|
+
// Different providers return reasoning differently:
|
|
167
|
+
// - OpenAI: delta.reasoning
|
|
168
|
+
// - Grok: delta.reasoning_content or delta.reasoning_details
|
|
169
|
+
// - Gemini via OpenRouter: delta.reasoning_details array with {type: 'reasoning.text', text: '...'}
|
|
170
|
+
// The rawChunk allows the stream transformer to extract reasoning using model-specific logic
|
|
171
|
+
const deltaAny = delta;
|
|
172
|
+
// Check for standard reasoning fields first
|
|
173
|
+
let reasoning = (deltaAny.reasoning ?? deltaAny.reasoning_content ?? deltaAny.thoughts);
|
|
174
|
+
// Check for OpenRouter's reasoning_details array format (used for Gemini and some other models)
|
|
175
|
+
if (!reasoning && deltaAny.reasoning_details) {
|
|
176
|
+
const details = deltaAny.reasoning_details;
|
|
177
|
+
if (Array.isArray(details)) {
|
|
178
|
+
const reasoningText = details
|
|
179
|
+
.filter((d) => d.type === 'reasoning.text' && d.text)
|
|
180
|
+
.map((d) => d.text)
|
|
181
|
+
.join('');
|
|
182
|
+
if (reasoningText) {
|
|
183
|
+
reasoning = reasoningText;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
164
187
|
yield {
|
|
165
188
|
content: delta.content ?? undefined,
|
|
166
189
|
finishReason,
|
|
167
190
|
isComplete,
|
|
191
|
+
rawChunk: chunk,
|
|
192
|
+
reasoning,
|
|
193
|
+
reasoningId: reasoning ? chunk.id : undefined,
|
|
168
194
|
toolCalls,
|
|
169
195
|
};
|
|
170
196
|
}
|
|
@@ -218,6 +218,18 @@ export declare class ByteRoverLLMService implements ILLMService {
|
|
|
218
218
|
* @returns Parsed internal message from response
|
|
219
219
|
*/
|
|
220
220
|
private callLLMAndParseResponse;
|
|
221
|
+
/**
|
|
222
|
+
* Streaming variant of callLLMAndParseResponse that:
|
|
223
|
+
* - Uses generateContentStream for real-time chunk delivery
|
|
224
|
+
* - Accumulates content and tool calls from chunks
|
|
225
|
+
* - Emits llmservice:chunk events for thinking/reasoning chunks
|
|
226
|
+
* - Returns complete InternalMessage when stream ends
|
|
227
|
+
*
|
|
228
|
+
* @param request - Generation request
|
|
229
|
+
* @param taskId - Task ID for event emission
|
|
230
|
+
* @returns Parsed internal message from accumulated stream
|
|
231
|
+
*/
|
|
232
|
+
private callLLMAndParseResponseStreaming;
|
|
221
233
|
/**
|
|
222
234
|
* Check for context overflow and trigger compaction if needed.
|
|
223
235
|
* Called after each assistant response and after tool execution batches.
|
|
@@ -262,6 +274,7 @@ export declare class ByteRoverLLMService implements ILLMService {
|
|
|
262
274
|
* @param options.fileData - Optional file data (only used on first iteration)
|
|
263
275
|
* @param options.imageData - Optional image data (only used on first iteration)
|
|
264
276
|
* @param options.iterationCount - Current iteration number
|
|
277
|
+
* @param options.stream - Whether to stream response and emit thinking chunks
|
|
265
278
|
* @param options.taskId - Task ID from usecase for billing tracking
|
|
266
279
|
* @param options.textInput - User input text (only used on first iteration)
|
|
267
280
|
* @param options.tools - Available tools for this iteration
|
|
@@ -2,6 +2,7 @@ import { AgentStateMachine } from '../../../core/domain/cipher/agent/agent-state
|
|
|
2
2
|
import { AgentState, TerminationReason } from '../../../core/domain/cipher/agent/agent-state.js';
|
|
3
3
|
import { LlmGenerationError, LlmMaxIterationsError, LlmResponseParsingError, } from '../../../core/domain/cipher/errors/llm-error.js';
|
|
4
4
|
import { getEffectiveMaxInputTokens, getMaxInputTokensForModel, getProviderFromModel, isValidProviderModel, safeParseLLMConfig, } from '../../../core/domain/cipher/llm/index.js';
|
|
5
|
+
import { StreamChunkType } from '../../../core/interfaces/cipher/i-content-generator.js';
|
|
5
6
|
import { NoOpLogger } from '../../../core/interfaces/cipher/i-logger.js';
|
|
6
7
|
import { getErrorMessage } from '../../../utils/error-helpers.js';
|
|
7
8
|
import { EnvironmentContextBuilder } from '../system-prompt/environment-context-builder.js';
|
|
@@ -148,7 +149,7 @@ export class ByteRoverLLMService {
|
|
|
148
149
|
*/
|
|
149
150
|
async completeTask(textInput, options) {
|
|
150
151
|
// Extract options with defaults
|
|
151
|
-
const { executionContext, fileData, imageData, signal, taskId } = options ?? {};
|
|
152
|
+
const { executionContext, fileData, imageData, signal, stream, taskId } = options ?? {};
|
|
152
153
|
// Get filtered tools based on command type (e.g., only read-only tools for 'query')
|
|
153
154
|
const toolSet = this.toolManager.getToolsForCommand(options?.executionContext?.commandType);
|
|
154
155
|
// Create state machine with configured limits
|
|
@@ -174,6 +175,7 @@ export class ByteRoverLLMService {
|
|
|
174
175
|
fileData,
|
|
175
176
|
imageData,
|
|
176
177
|
iterationCount: stateMachine.getContext().turnCount,
|
|
178
|
+
stream,
|
|
177
179
|
taskId,
|
|
178
180
|
textInput,
|
|
179
181
|
tools: toolSet,
|
|
@@ -340,6 +342,69 @@ export class ByteRoverLLMService {
|
|
|
340
342
|
throw new LlmGenerationError(error instanceof Error ? error.message : String(error), 'byterover', this.config.model);
|
|
341
343
|
}
|
|
342
344
|
}
|
|
345
|
+
/**
|
|
346
|
+
* Streaming variant of callLLMAndParseResponse that:
|
|
347
|
+
* - Uses generateContentStream for real-time chunk delivery
|
|
348
|
+
* - Accumulates content and tool calls from chunks
|
|
349
|
+
* - Emits llmservice:chunk events for thinking/reasoning chunks
|
|
350
|
+
* - Returns complete InternalMessage when stream ends
|
|
351
|
+
*
|
|
352
|
+
* @param request - Generation request
|
|
353
|
+
* @param taskId - Task ID for event emission
|
|
354
|
+
* @returns Parsed internal message from accumulated stream
|
|
355
|
+
*/
|
|
356
|
+
async callLLMAndParseResponseStreaming(request, taskId) {
|
|
357
|
+
try {
|
|
358
|
+
let accumulatedContent = '';
|
|
359
|
+
let accumulatedToolCalls = [];
|
|
360
|
+
// Stream chunks and accumulate content
|
|
361
|
+
for await (const chunk of this.generator.generateContentStream(request)) {
|
|
362
|
+
// Emit thinking/reasoning chunks as events for TUI display
|
|
363
|
+
if (chunk.type === StreamChunkType.THINKING && chunk.reasoning) {
|
|
364
|
+
this.sessionEventBus.emit('llmservice:chunk', {
|
|
365
|
+
content: chunk.reasoning,
|
|
366
|
+
isComplete: chunk.isComplete,
|
|
367
|
+
taskId,
|
|
368
|
+
type: 'reasoning', // Convert THINKING to 'reasoning' for TUI compatibility
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
// Accumulate text content (skip thinking chunks from accumulated content)
|
|
372
|
+
if (chunk.content && chunk.type !== StreamChunkType.THINKING) {
|
|
373
|
+
accumulatedContent += chunk.content;
|
|
374
|
+
// Emit text chunks for TUI display
|
|
375
|
+
this.sessionEventBus.emit('llmservice:chunk', {
|
|
376
|
+
content: chunk.content,
|
|
377
|
+
isComplete: chunk.isComplete,
|
|
378
|
+
taskId,
|
|
379
|
+
type: 'text',
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
// Accumulate tool calls
|
|
383
|
+
if (chunk.toolCalls) {
|
|
384
|
+
accumulatedToolCalls = chunk.toolCalls;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
// Convert accumulated response to InternalMessage format
|
|
388
|
+
const message = {
|
|
389
|
+
content: accumulatedContent || null,
|
|
390
|
+
role: 'assistant',
|
|
391
|
+
toolCalls: accumulatedToolCalls.length > 0 ? accumulatedToolCalls : undefined,
|
|
392
|
+
};
|
|
393
|
+
// Validate the message has content or tool calls
|
|
394
|
+
if (!message.content && (!message.toolCalls || message.toolCalls.length === 0)) {
|
|
395
|
+
throw new LlmResponseParsingError('Response has neither content nor tool calls', 'byterover', this.config.model);
|
|
396
|
+
}
|
|
397
|
+
return message;
|
|
398
|
+
}
|
|
399
|
+
catch (error) {
|
|
400
|
+
// Re-throw LLM errors as-is
|
|
401
|
+
if (error instanceof LlmResponseParsingError || error instanceof LlmGenerationError) {
|
|
402
|
+
throw error;
|
|
403
|
+
}
|
|
404
|
+
// Wrap other errors
|
|
405
|
+
throw new LlmGenerationError(error instanceof Error ? error.message : String(error), 'byterover', this.config.model);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
343
408
|
/**
|
|
344
409
|
* Check for context overflow and trigger compaction if needed.
|
|
345
410
|
* Called after each assistant response and after tool execution batches.
|
|
@@ -484,13 +549,14 @@ export class ByteRoverLLMService {
|
|
|
484
549
|
* @param options.fileData - Optional file data (only used on first iteration)
|
|
485
550
|
* @param options.imageData - Optional image data (only used on first iteration)
|
|
486
551
|
* @param options.iterationCount - Current iteration number
|
|
552
|
+
* @param options.stream - Whether to stream response and emit thinking chunks
|
|
487
553
|
* @param options.taskId - Task ID from usecase for billing tracking
|
|
488
554
|
* @param options.textInput - User input text (only used on first iteration)
|
|
489
555
|
* @param options.tools - Available tools for this iteration
|
|
490
556
|
* @returns Final response string if complete, null if more iterations needed
|
|
491
557
|
*/
|
|
492
558
|
async executeAgenticIteration(options) {
|
|
493
|
-
const { executionContext, fileData, imageData, iterationCount, taskId, textInput, tools } = options;
|
|
559
|
+
const { executionContext, fileData, imageData, iterationCount, stream, taskId, textInput, tools } = options;
|
|
494
560
|
// Build system prompt using SystemPromptManager (before compression for correct token accounting)
|
|
495
561
|
// Use filtered tool names based on command type (e.g., only read-only tools for 'query')
|
|
496
562
|
const availableTools = this.toolManager.getToolNamesForCommand(executionContext?.commandType);
|
|
@@ -549,7 +615,9 @@ export class ByteRoverLLMService {
|
|
|
549
615
|
// Add user message and compress context within mutex lock
|
|
550
616
|
return this.mutex.withLock(async () => {
|
|
551
617
|
// Add user message to context only on the first iteration
|
|
552
|
-
|
|
618
|
+
if (iterationCount === 0) {
|
|
619
|
+
await this.contextManager.addUserMessage(textInput, imageData, fileData);
|
|
620
|
+
}
|
|
553
621
|
const messages = this.contextManager.getMessages();
|
|
554
622
|
const messageTokenCounts = messages.map((msg) => this.generator.estimateTokensSync(typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)));
|
|
555
623
|
const maxMessageTokens = this.config.maxInputTokens - systemPromptTokens;
|
|
@@ -578,7 +646,10 @@ export class ByteRoverLLMService {
|
|
|
578
646
|
tools: toolsForThisIteration,
|
|
579
647
|
});
|
|
580
648
|
// Call LLM via generator (retry + logging handled by decorators)
|
|
581
|
-
|
|
649
|
+
// Use streaming variant if enabled to emit thinking/reasoning chunks
|
|
650
|
+
const lastMessage = stream
|
|
651
|
+
? await this.callLLMAndParseResponseStreaming(request, taskId)
|
|
652
|
+
: await this.callLLMAndParseResponse(request);
|
|
582
653
|
// Check if there are tool calls
|
|
583
654
|
if (!lastMessage.toolCalls || lastMessage.toolCalls.length === 0) {
|
|
584
655
|
const response = await this.handleFinalResponse(lastMessage, taskId);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model Capabilities Detection
|
|
3
|
+
*
|
|
4
|
+
* Detects reasoning/thinking capabilities and format for each model.
|
|
5
|
+
* Following OpenCode's pattern of model-specific capability detection.
|
|
6
|
+
*
|
|
7
|
+
* Different models use different formats for reasoning:
|
|
8
|
+
* - OpenAI (o1, o3, gpt-5): Native `reasoning` field in API response
|
|
9
|
+
* - Grok: `reasoning_content` or `reasoning_details` fields
|
|
10
|
+
* - Gemini via OpenRouter: `reasoning_details` array or `thoughts` field
|
|
11
|
+
* - Claude/DeepSeek/MiniMax: `<think>...</think>` XML tags in content
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Reasoning format types
|
|
15
|
+
*/
|
|
16
|
+
export type ReasoningFormat =
|
|
17
|
+
/** Model uses <think>...</think> XML tags in content */
|
|
18
|
+
'interleaved'
|
|
19
|
+
/** Model uses a native field in the API response */
|
|
20
|
+
| 'native-field'
|
|
21
|
+
/** Model interleaves reasoning in content parts */
|
|
22
|
+
| 'none'
|
|
23
|
+
/** Model does not support reasoning */
|
|
24
|
+
| 'think-tags';
|
|
25
|
+
/**
|
|
26
|
+
* Model capabilities for reasoning/thinking
|
|
27
|
+
*/
|
|
28
|
+
export interface ModelCapabilities {
|
|
29
|
+
/** Additional fields to check for reasoning content */
|
|
30
|
+
alternativeFields?: string[];
|
|
31
|
+
/** Whether the model supports reasoning/thinking output */
|
|
32
|
+
reasoning: boolean;
|
|
33
|
+
/** The field name for native reasoning (e.g., 'reasoning_content', 'reasoning', 'thoughts') */
|
|
34
|
+
reasoningField?: string;
|
|
35
|
+
/** How the model outputs reasoning content */
|
|
36
|
+
reasoningFormat: ReasoningFormat;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Get model capabilities for a given model ID.
|
|
40
|
+
*
|
|
41
|
+
* @param modelId - The model identifier (can be full path like "openai/gpt-5" or short like "gpt-5")
|
|
42
|
+
* @returns Model capabilities including reasoning support and format
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```typescript
|
|
46
|
+
* const caps = getModelCapabilities('openai/o3-mini')
|
|
47
|
+
* // { reasoning: true, reasoningFormat: 'native-field', reasoningField: 'reasoning' }
|
|
48
|
+
*
|
|
49
|
+
* const caps2 = getModelCapabilities('anthropic/claude-3-opus')
|
|
50
|
+
* // { reasoning: true, reasoningFormat: 'think-tags' }
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export declare function getModelCapabilities(modelId: string): ModelCapabilities;
|
|
54
|
+
/**
|
|
55
|
+
* Check if a model supports reasoning.
|
|
56
|
+
*
|
|
57
|
+
* @param modelId - The model identifier
|
|
58
|
+
* @returns True if the model supports reasoning output
|
|
59
|
+
*/
|
|
60
|
+
export declare function supportsReasoning(modelId: string): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Check if a model uses think tags for reasoning.
|
|
63
|
+
*
|
|
64
|
+
* @param modelId - The model identifier
|
|
65
|
+
* @returns True if the model uses <think>...</think> tags
|
|
66
|
+
*/
|
|
67
|
+
export declare function usesThinkTags(modelId: string): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Check if a model uses native reasoning fields.
|
|
70
|
+
*
|
|
71
|
+
* @param modelId - The model identifier
|
|
72
|
+
* @returns True if the model uses native API fields for reasoning
|
|
73
|
+
*/
|
|
74
|
+
export declare function usesNativeReasoning(modelId: string): boolean;
|