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.
Files changed (174) hide show
  1. package/README.md +193 -12
  2. package/dist/core/domain/cipher/process/types.d.ts +1 -1
  3. package/dist/core/domain/entities/provider-config.d.ts +92 -0
  4. package/dist/core/domain/entities/provider-config.js +181 -0
  5. package/dist/core/domain/entities/provider-registry.d.ts +55 -0
  6. package/dist/core/domain/entities/provider-registry.js +74 -0
  7. package/dist/core/domain/errors/headless-prompt-error.d.ts +11 -0
  8. package/dist/core/domain/errors/headless-prompt-error.js +18 -0
  9. package/dist/core/interfaces/cipher/i-content-generator.d.ts +30 -0
  10. package/dist/core/interfaces/cipher/i-content-generator.js +12 -1
  11. package/dist/core/interfaces/cipher/message-factory.d.ts +4 -1
  12. package/dist/core/interfaces/cipher/message-factory.js +5 -0
  13. package/dist/core/interfaces/cipher/message-types.d.ts +19 -1
  14. package/dist/core/interfaces/i-cogit-pull-service.d.ts +0 -1
  15. package/dist/core/interfaces/i-memory-retrieval-service.d.ts +0 -1
  16. package/dist/core/interfaces/i-memory-storage-service.d.ts +0 -2
  17. package/dist/core/interfaces/i-provider-config-store.d.ts +88 -0
  18. package/dist/core/interfaces/i-provider-config-store.js +1 -0
  19. package/dist/core/interfaces/i-provider-keychain-store.d.ts +33 -0
  20. package/dist/core/interfaces/i-provider-keychain-store.js +1 -0
  21. package/dist/core/interfaces/i-space-service.d.ts +1 -2
  22. package/dist/core/interfaces/i-team-service.d.ts +1 -2
  23. package/dist/core/interfaces/i-user-service.d.ts +1 -2
  24. package/dist/core/interfaces/usecase/i-curate-use-case.d.ts +2 -0
  25. package/dist/core/interfaces/usecase/i-init-use-case.d.ts +9 -3
  26. package/dist/core/interfaces/usecase/i-login-use-case.d.ts +4 -1
  27. package/dist/core/interfaces/usecase/i-pull-use-case.d.ts +5 -3
  28. package/dist/core/interfaces/usecase/i-push-use-case.d.ts +6 -4
  29. package/dist/core/interfaces/usecase/i-query-use-case.d.ts +2 -0
  30. package/dist/core/interfaces/usecase/i-status-use-case.d.ts +1 -0
  31. package/dist/infra/cipher/agent/service-initializer.d.ts +1 -1
  32. package/dist/infra/cipher/agent/service-initializer.js +0 -1
  33. package/dist/infra/cipher/file-system/file-system-service.js +5 -5
  34. package/dist/infra/cipher/http/internal-llm-http-service.d.ts +40 -1
  35. package/dist/infra/cipher/http/internal-llm-http-service.js +153 -4
  36. package/dist/infra/cipher/llm/formatters/gemini-formatter.js +8 -1
  37. package/dist/infra/cipher/llm/generators/byterover-content-generator.d.ts +2 -3
  38. package/dist/infra/cipher/llm/generators/byterover-content-generator.js +20 -11
  39. package/dist/infra/cipher/llm/generators/openrouter-content-generator.d.ts +1 -0
  40. package/dist/infra/cipher/llm/generators/openrouter-content-generator.js +26 -0
  41. package/dist/infra/cipher/llm/internal-llm-service.d.ts +13 -0
  42. package/dist/infra/cipher/llm/internal-llm-service.js +75 -4
  43. package/dist/infra/cipher/llm/model-capabilities.d.ts +74 -0
  44. package/dist/infra/cipher/llm/model-capabilities.js +157 -0
  45. package/dist/infra/cipher/llm/openrouter-llm-service.d.ts +35 -1
  46. package/dist/infra/cipher/llm/openrouter-llm-service.js +216 -28
  47. package/dist/infra/cipher/llm/stream-processor.d.ts +22 -2
  48. package/dist/infra/cipher/llm/stream-processor.js +78 -4
  49. package/dist/infra/cipher/llm/thought-parser.d.ts +1 -1
  50. package/dist/infra/cipher/llm/thought-parser.js +5 -5
  51. package/dist/infra/cipher/llm/transformers/openrouter-stream-transformer.d.ts +49 -0
  52. package/dist/infra/cipher/llm/transformers/openrouter-stream-transformer.js +272 -0
  53. package/dist/infra/cipher/llm/transformers/reasoning-extractor.d.ts +71 -0
  54. package/dist/infra/cipher/llm/transformers/reasoning-extractor.js +253 -0
  55. package/dist/infra/cipher/process/process-service.js +1 -1
  56. package/dist/infra/cipher/session/chat-session.d.ts +2 -0
  57. package/dist/infra/cipher/session/chat-session.js +13 -2
  58. package/dist/infra/cipher/storage/message-storage-service.js +4 -0
  59. package/dist/infra/cipher/tools/implementations/bash-exec-tool.js +3 -3
  60. package/dist/infra/cipher/tools/implementations/task-tool.js +1 -1
  61. package/dist/infra/cogit/http-cogit-pull-service.js +1 -1
  62. package/dist/infra/cogit/http-cogit-push-service.js +0 -1
  63. package/dist/infra/http/authenticated-http-client.d.ts +1 -3
  64. package/dist/infra/http/authenticated-http-client.js +1 -5
  65. package/dist/infra/http/openrouter-api-client.d.ts +148 -0
  66. package/dist/infra/http/openrouter-api-client.js +161 -0
  67. package/dist/infra/mcp/tools/task-result-waiter.js +9 -1
  68. package/dist/infra/memory/http-memory-retrieval-service.js +1 -1
  69. package/dist/infra/memory/http-memory-storage-service.js +2 -2
  70. package/dist/infra/process/agent-worker.js +178 -70
  71. package/dist/infra/process/inline-agent-executor.d.ts +32 -0
  72. package/dist/infra/process/inline-agent-executor.js +259 -0
  73. package/dist/infra/process/transport-handlers.d.ts +25 -4
  74. package/dist/infra/process/transport-handlers.js +57 -10
  75. package/dist/infra/repl/commands/connectors-command.js +2 -2
  76. package/dist/infra/repl/commands/index.js +5 -0
  77. package/dist/infra/repl/commands/model-command.d.ts +13 -0
  78. package/dist/infra/repl/commands/model-command.js +212 -0
  79. package/dist/infra/repl/commands/provider-command.d.ts +13 -0
  80. package/dist/infra/repl/commands/provider-command.js +181 -0
  81. package/dist/infra/repl/transport-client-helper.js +6 -2
  82. package/dist/infra/space/http-space-service.d.ts +1 -1
  83. package/dist/infra/space/http-space-service.js +2 -2
  84. package/dist/infra/storage/file-provider-config-store.d.ts +83 -0
  85. package/dist/infra/storage/file-provider-config-store.js +157 -0
  86. package/dist/infra/storage/provider-keychain-store.d.ts +37 -0
  87. package/dist/infra/storage/provider-keychain-store.js +75 -0
  88. package/dist/infra/storage/token-store.d.ts +4 -3
  89. package/dist/infra/storage/token-store.js +6 -5
  90. package/dist/infra/team/http-team-service.d.ts +1 -1
  91. package/dist/infra/team/http-team-service.js +2 -2
  92. package/dist/infra/terminal/headless-terminal.d.ts +91 -0
  93. package/dist/infra/terminal/headless-terminal.js +211 -0
  94. package/dist/infra/transport/socket-io-transport-client.d.ts +20 -0
  95. package/dist/infra/transport/socket-io-transport-client.js +88 -1
  96. package/dist/infra/usecase/curate-use-case.d.ts +40 -1
  97. package/dist/infra/usecase/curate-use-case.js +176 -15
  98. package/dist/infra/usecase/init-use-case.d.ts +27 -5
  99. package/dist/infra/usecase/init-use-case.js +200 -34
  100. package/dist/infra/usecase/login-use-case.d.ts +10 -8
  101. package/dist/infra/usecase/login-use-case.js +35 -2
  102. package/dist/infra/usecase/pull-use-case.d.ts +19 -5
  103. package/dist/infra/usecase/pull-use-case.js +71 -13
  104. package/dist/infra/usecase/push-use-case.d.ts +18 -5
  105. package/dist/infra/usecase/push-use-case.js +81 -14
  106. package/dist/infra/usecase/query-use-case.d.ts +21 -0
  107. package/dist/infra/usecase/query-use-case.js +114 -29
  108. package/dist/infra/usecase/space-list-use-case.js +1 -1
  109. package/dist/infra/usecase/space-switch-use-case.js +2 -2
  110. package/dist/infra/usecase/status-use-case.d.ts +36 -0
  111. package/dist/infra/usecase/status-use-case.js +185 -48
  112. package/dist/infra/user/http-user-service.d.ts +1 -1
  113. package/dist/infra/user/http-user-service.js +2 -2
  114. package/dist/oclif/commands/curate.d.ts +6 -1
  115. package/dist/oclif/commands/curate.js +24 -3
  116. package/dist/oclif/commands/init.d.ts +18 -0
  117. package/dist/oclif/commands/init.js +129 -0
  118. package/dist/oclif/commands/login.d.ts +9 -0
  119. package/dist/oclif/commands/login.js +45 -0
  120. package/dist/oclif/commands/pull.d.ts +16 -0
  121. package/dist/oclif/commands/pull.js +78 -0
  122. package/dist/oclif/commands/push.d.ts +17 -0
  123. package/dist/oclif/commands/push.js +87 -0
  124. package/dist/oclif/commands/query.d.ts +6 -1
  125. package/dist/oclif/commands/query.js +29 -4
  126. package/dist/oclif/commands/status.d.ts +5 -1
  127. package/dist/oclif/commands/status.js +17 -5
  128. package/dist/resources/tools/bash_exec.txt +1 -1
  129. package/dist/tui/components/api-key-dialog.d.ts +39 -0
  130. package/dist/tui/components/api-key-dialog.js +94 -0
  131. package/dist/tui/components/execution/execution-changes.d.ts +3 -1
  132. package/dist/tui/components/execution/execution-changes.js +4 -4
  133. package/dist/tui/components/execution/execution-content.d.ts +1 -1
  134. package/dist/tui/components/execution/execution-content.js +4 -12
  135. package/dist/tui/components/execution/execution-input.js +1 -1
  136. package/dist/tui/components/execution/execution-progress.d.ts +10 -13
  137. package/dist/tui/components/execution/execution-progress.js +70 -17
  138. package/dist/tui/components/execution/execution-reasoning.d.ts +16 -0
  139. package/dist/tui/components/execution/execution-reasoning.js +34 -0
  140. package/dist/tui/components/execution/execution-tool.d.ts +23 -0
  141. package/dist/tui/components/execution/execution-tool.js +125 -0
  142. package/dist/tui/components/execution/expanded-log-view.js +3 -3
  143. package/dist/tui/components/execution/log-item.d.ts +2 -0
  144. package/dist/tui/components/execution/log-item.js +6 -4
  145. package/dist/tui/components/index.d.ts +2 -0
  146. package/dist/tui/components/index.js +2 -0
  147. package/dist/tui/components/inline-prompts/inline-select.js +3 -2
  148. package/dist/tui/components/model-dialog.d.ts +63 -0
  149. package/dist/tui/components/model-dialog.js +89 -0
  150. package/dist/tui/components/onboarding/onboarding-flow.js +8 -2
  151. package/dist/tui/components/provider-dialog.d.ts +27 -0
  152. package/dist/tui/components/provider-dialog.js +31 -0
  153. package/dist/tui/components/reasoning-text.d.ts +26 -0
  154. package/dist/tui/components/reasoning-text.js +49 -0
  155. package/dist/tui/components/selectable-list.d.ts +54 -0
  156. package/dist/tui/components/selectable-list.js +180 -0
  157. package/dist/tui/components/streaming-text.d.ts +30 -0
  158. package/dist/tui/components/streaming-text.js +52 -0
  159. package/dist/tui/contexts/tasks-context.d.ts +15 -0
  160. package/dist/tui/contexts/tasks-context.js +224 -40
  161. package/dist/tui/contexts/theme-context.d.ts +1 -0
  162. package/dist/tui/contexts/theme-context.js +3 -2
  163. package/dist/tui/hooks/use-activity-logs.js +7 -1
  164. package/dist/tui/hooks/use-auth-polling.js +1 -1
  165. package/dist/tui/types/messages.d.ts +32 -5
  166. package/dist/tui/utils/index.d.ts +1 -1
  167. package/dist/tui/utils/index.js +1 -1
  168. package/dist/tui/utils/log.d.ts +0 -9
  169. package/dist/tui/utils/log.js +2 -53
  170. package/dist/tui/views/command-view.js +4 -1
  171. package/dist/utils/environment-detector.d.ts +15 -0
  172. package/dist/utils/environment-detector.js +62 -1
  173. package/oclif.manifest.json +287 -5
  174. 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.accessToken, this.config.sessionKey);
87
- const response = await httpClient.post(url, request, {
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 response.data;
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
- // Extract text and function calls from response parts
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
- * Note: The current gRPC service collects all chunks before returning.
64
- * This implementation yields the complete response as a single chunk.
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
- * Note: The current gRPC service collects all chunks before returning.
121
- * This implementation yields the complete response as a single chunk.
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
- // For now, use non-streaming and yield complete response
130
- // True streaming can be added when gRPC service exposes the stream
131
- const response = await this.generateContent(request);
132
- yield {
133
- content: response.content,
134
- finishReason: response.finishReason,
135
- isComplete: true,
136
- toolCalls: response.toolCalls,
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
- await this.contextManager.addUserMessage(textInput, imageData, fileData);
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
- const lastMessage = await this.callLLMAndParseResponse(request);
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;