berget 2.0.6 → 2.1.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/AGENTS.md +163 -2
- package/dist/package.json +1 -1
- package/dist/src/commands/chat.js +1 -2
- package/dist/src/commands/code.js +122 -25
- package/dist/src/services/chat-service.js +77 -5
- package/dist/src/utils/config-loader.js +83 -27
- package/dist/tests/commands/chat.test.js +2 -2
- package/dist/tests/commands/code.test.js +4 -4
- package/dist/tests/utils/config-loader.test.js +248 -163
- package/opencode.json +31 -71
- package/package.json +1 -1
- package/src/commands/chat.ts +68 -61
- package/src/commands/code.ts +283 -124
- package/src/services/chat-service.ts +86 -5
- package/src/utils/config-loader.ts +113 -38
- package/tests/commands/chat.test.ts +2 -2
- package/tests/commands/code.test.ts +4 -4
- package/tests/utils/config-loader.test.ts +320 -0
- package/blog-post.md +0 -176
|
@@ -386,6 +386,7 @@ export class ChatService {
|
|
|
386
386
|
const decoder = new TextDecoder()
|
|
387
387
|
let fullContent = ''
|
|
388
388
|
let fullResponse: any = null
|
|
389
|
+
let buffer = '' // Buffer to accumulate partial JSON data
|
|
389
390
|
|
|
390
391
|
while (true) {
|
|
391
392
|
const { done, value } = await reader.read()
|
|
@@ -394,17 +395,76 @@ export class ChatService {
|
|
|
394
395
|
const chunk = decoder.decode(value, { stream: true })
|
|
395
396
|
logger.debug(`Received chunk: ${chunk.length} bytes`)
|
|
396
397
|
|
|
397
|
-
//
|
|
398
|
-
|
|
399
|
-
|
|
398
|
+
// Add chunk to buffer
|
|
399
|
+
buffer += chunk
|
|
400
|
+
logger.debug(`Added chunk to buffer. Buffer length: ${buffer.length}`)
|
|
401
|
+
|
|
402
|
+
// Process the buffer - it may contain multiple SSE events
|
|
403
|
+
const lines = buffer.split('\n')
|
|
404
|
+
logger.debug(`Processing ${lines.length} lines from buffer`)
|
|
405
|
+
|
|
406
|
+
// Keep track of processed lines to update buffer
|
|
407
|
+
let processedLines = 0
|
|
408
|
+
|
|
409
|
+
for (let i = 0; i < lines.length; i++) {
|
|
410
|
+
const line = lines[i]
|
|
411
|
+
logger.debug(`Line ${i}: "${line}"`)
|
|
412
|
+
|
|
400
413
|
if (line.startsWith('data:')) {
|
|
401
414
|
const jsonData = line.slice(5).trim()
|
|
415
|
+
logger.debug(`Extracted JSON data: "${jsonData}"`)
|
|
402
416
|
|
|
403
417
|
// Skip empty data or [DONE] marker
|
|
404
|
-
if (jsonData === '' || jsonData === '[DONE]')
|
|
418
|
+
if (jsonData === '' || jsonData === '[DONE]') {
|
|
419
|
+
logger.debug(`Skipping empty data or [DONE] marker`)
|
|
420
|
+
processedLines = i + 1
|
|
421
|
+
continue
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Check if JSON looks complete (basic validation)
|
|
425
|
+
if (!jsonData.startsWith('{')) {
|
|
426
|
+
logger.warn(`JSON data doesn't start with '{', might be partial: "${jsonData.substring(0, 50)}..."`)
|
|
427
|
+
// Don't process this line yet, keep it in buffer
|
|
428
|
+
break
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Count braces to check if JSON is complete
|
|
432
|
+
let braceCount = 0
|
|
433
|
+
let inString = false
|
|
434
|
+
let escaped = false
|
|
435
|
+
|
|
436
|
+
for (let j = 0; j < jsonData.length; j++) {
|
|
437
|
+
const char = jsonData[j]
|
|
438
|
+
if (escaped) {
|
|
439
|
+
escaped = false
|
|
440
|
+
continue
|
|
441
|
+
}
|
|
442
|
+
if (char === '\\') {
|
|
443
|
+
escaped = true
|
|
444
|
+
continue
|
|
445
|
+
}
|
|
446
|
+
if (char === '"') {
|
|
447
|
+
inString = !inString
|
|
448
|
+
continue
|
|
449
|
+
}
|
|
450
|
+
if (!inString && char === '{') {
|
|
451
|
+
braceCount++
|
|
452
|
+
} else if (!inString && char === '}') {
|
|
453
|
+
braceCount--
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (braceCount !== 0) {
|
|
458
|
+
logger.warn(`JSON braces don't balance (${braceCount}), treating as partial: "${jsonData.substring(0, 50)}..."`)
|
|
459
|
+
// Don't process this line yet, keep it in buffer
|
|
460
|
+
break
|
|
461
|
+
}
|
|
405
462
|
|
|
406
463
|
try {
|
|
464
|
+
logger.debug(`Attempting to parse JSON of length: ${jsonData.length}`)
|
|
407
465
|
const parsedData = JSON.parse(jsonData)
|
|
466
|
+
logger.debug(`Successfully parsed JSON: ${JSON.stringify(parsedData, null, 2)}`)
|
|
467
|
+
processedLines = i + 1 // Mark this line as processed
|
|
408
468
|
|
|
409
469
|
// Call the onChunk callback with the parsed data
|
|
410
470
|
if (options.onChunk) {
|
|
@@ -426,10 +486,31 @@ export class ChatService {
|
|
|
426
486
|
}
|
|
427
487
|
} catch (e) {
|
|
428
488
|
logger.error(`Error parsing chunk: ${e}`)
|
|
429
|
-
logger.
|
|
489
|
+
logger.error(`JSON parse error at position ${(e as any).message?.match(/position (\d+)/)?.[1] || 'unknown'}`)
|
|
490
|
+
logger.error(`Problematic chunk length: ${jsonData.length}`)
|
|
491
|
+
logger.error(`Problematic chunk content: "${jsonData}"`)
|
|
492
|
+
logger.error(`Chunk starts with: "${jsonData.substring(0, 50)}..."`)
|
|
493
|
+
logger.error(`Chunk ends with: "...${jsonData.substring(jsonData.length - 50)}"`)
|
|
494
|
+
|
|
495
|
+
// Show character codes around the error position
|
|
496
|
+
const errorPos = parseInt((e as any).message?.match(/position (\d+)/)?.[1] || '0')
|
|
497
|
+
if (errorPos > 0) {
|
|
498
|
+
const start = Math.max(0, errorPos - 20)
|
|
499
|
+
const end = Math.min(jsonData.length, errorPos + 20)
|
|
500
|
+
logger.error(`Context around error position ${errorPos}:`)
|
|
501
|
+
logger.error(`"${jsonData.substring(start, end)}"`)
|
|
502
|
+
logger.error(`Character codes: ${Array.from(jsonData.substring(start, end)).map(c => c.charCodeAt(0)).join(' ')}`)
|
|
503
|
+
}
|
|
430
504
|
}
|
|
431
505
|
}
|
|
432
506
|
}
|
|
507
|
+
|
|
508
|
+
// Update buffer to only contain unprocessed lines
|
|
509
|
+
if (processedLines > 0) {
|
|
510
|
+
const remainingLines = lines.slice(processedLines)
|
|
511
|
+
buffer = remainingLines.join('\n')
|
|
512
|
+
logger.debug(`Updated buffer. Remaining lines: ${remainingLines.length}, Buffer length: ${buffer.length}`)
|
|
513
|
+
}
|
|
433
514
|
}
|
|
434
515
|
|
|
435
516
|
// Construct the final response object similar to non-streaming response
|
|
@@ -33,6 +33,10 @@ export interface ProviderModelConfig {
|
|
|
33
33
|
output: number
|
|
34
34
|
context: number
|
|
35
35
|
}
|
|
36
|
+
modalities?: {
|
|
37
|
+
input: string[]
|
|
38
|
+
output: string[]
|
|
39
|
+
}
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
export interface OpenCodeConfig {
|
|
@@ -66,6 +70,13 @@ export class ConfigLoader {
|
|
|
66
70
|
return ConfigLoader.instance
|
|
67
71
|
}
|
|
68
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Clear the singleton instance (for testing purposes)
|
|
75
|
+
*/
|
|
76
|
+
public static clearInstance(): void {
|
|
77
|
+
ConfigLoader.instance = null as any
|
|
78
|
+
}
|
|
79
|
+
|
|
69
80
|
/**
|
|
70
81
|
* Load configuration from opencode.json
|
|
71
82
|
*/
|
|
@@ -81,12 +92,19 @@ export class ConfigLoader {
|
|
|
81
92
|
|
|
82
93
|
const configContent = fs.readFileSync(this.configPath, 'utf8')
|
|
83
94
|
this.config = JSON.parse(configContent) as OpenCodeConfig
|
|
84
|
-
|
|
95
|
+
|
|
85
96
|
logger.debug(`Loaded configuration from ${this.configPath}`)
|
|
86
97
|
return this.config
|
|
87
98
|
} catch (error) {
|
|
88
|
-
logger.error(
|
|
89
|
-
|
|
99
|
+
logger.error(
|
|
100
|
+
`Failed to load configuration from ${this.configPath}:`,
|
|
101
|
+
error
|
|
102
|
+
)
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Failed to load configuration: ${
|
|
105
|
+
error instanceof Error ? error.message : String(error)
|
|
106
|
+
}`
|
|
107
|
+
)
|
|
90
108
|
}
|
|
91
109
|
}
|
|
92
110
|
|
|
@@ -94,56 +112,85 @@ export class ConfigLoader {
|
|
|
94
112
|
* Get agent configuration by name
|
|
95
113
|
*/
|
|
96
114
|
public getAgentConfig(agentName: string): AgentConfig | null {
|
|
97
|
-
|
|
98
|
-
|
|
115
|
+
try {
|
|
116
|
+
const config = this.loadConfig()
|
|
117
|
+
return config.agent?.[agentName] || null
|
|
118
|
+
} catch (error) {
|
|
119
|
+
// Config file doesn't exist, return null
|
|
120
|
+
return null
|
|
121
|
+
}
|
|
99
122
|
}
|
|
100
123
|
|
|
101
124
|
/**
|
|
102
125
|
* Get all agent configurations
|
|
103
126
|
*/
|
|
104
127
|
public getAllAgentConfigs(): Record<string, AgentConfig> {
|
|
105
|
-
|
|
106
|
-
|
|
128
|
+
try {
|
|
129
|
+
const config = this.loadConfig()
|
|
130
|
+
return config.agent || {}
|
|
131
|
+
} catch (error) {
|
|
132
|
+
// Config file doesn't exist, return empty object
|
|
133
|
+
return {}
|
|
134
|
+
}
|
|
107
135
|
}
|
|
108
136
|
|
|
109
137
|
/**
|
|
110
138
|
* Get model configuration
|
|
111
139
|
*/
|
|
112
140
|
public getModelConfig(): ModelConfig {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
141
|
+
try {
|
|
142
|
+
const config = this.loadConfig()
|
|
143
|
+
|
|
144
|
+
// Extract from config or fall back to defaults
|
|
145
|
+
const primary = config.model || 'berget/glm-4.7'
|
|
146
|
+
const small = config.small_model || 'berget/gpt-oss'
|
|
147
|
+
|
|
148
|
+
return { primary, small }
|
|
149
|
+
} catch (error) {
|
|
150
|
+
// Fallback to defaults when no config exists (init scenario)
|
|
151
|
+
return {
|
|
152
|
+
primary: 'berget/glm-4.7',
|
|
153
|
+
small: 'berget/gpt-oss',
|
|
154
|
+
}
|
|
155
|
+
}
|
|
120
156
|
}
|
|
121
157
|
|
|
122
158
|
/**
|
|
123
159
|
* Get provider model configuration
|
|
124
160
|
*/
|
|
125
161
|
public getProviderModels(): Record<string, ProviderModelConfig> {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
162
|
+
try {
|
|
163
|
+
const config = this.loadConfig()
|
|
164
|
+
|
|
165
|
+
// Extract from provider configuration
|
|
166
|
+
if (config.provider?.berget?.models) {
|
|
167
|
+
return config.provider.berget.models as Record<
|
|
168
|
+
string,
|
|
169
|
+
ProviderModelConfig
|
|
170
|
+
>
|
|
171
|
+
}
|
|
172
|
+
} catch (error) {
|
|
173
|
+
// Config file doesn't exist, use fallback defaults
|
|
131
174
|
}
|
|
132
|
-
|
|
175
|
+
|
|
133
176
|
// Fallback to defaults
|
|
134
177
|
return {
|
|
135
|
-
'
|
|
136
|
-
name: 'GLM-4.
|
|
137
|
-
limit: { output: 4000, context: 90000 }
|
|
178
|
+
'glm-4.7': {
|
|
179
|
+
name: 'GLM-4.7',
|
|
180
|
+
limit: { output: 4000, context: 90000 },
|
|
138
181
|
},
|
|
139
182
|
'gpt-oss': {
|
|
140
183
|
name: 'GPT-OSS',
|
|
141
|
-
limit: { output: 4000, context: 128000 }
|
|
184
|
+
limit: { output: 4000, context: 128000 },
|
|
185
|
+
modalities: {
|
|
186
|
+
input: ['text', 'image'],
|
|
187
|
+
output: ['text'],
|
|
188
|
+
},
|
|
142
189
|
},
|
|
143
190
|
'llama-8b': {
|
|
144
191
|
name: 'llama-3.1-8b',
|
|
145
|
-
limit: { output: 4000, context: 128000 }
|
|
146
|
-
}
|
|
192
|
+
limit: { output: 4000, context: 128000 },
|
|
193
|
+
},
|
|
147
194
|
}
|
|
148
195
|
}
|
|
149
196
|
|
|
@@ -151,24 +198,43 @@ export class ConfigLoader {
|
|
|
151
198
|
* Get command configurations
|
|
152
199
|
*/
|
|
153
200
|
public getCommandConfigs(): Record<string, any> {
|
|
154
|
-
|
|
155
|
-
|
|
201
|
+
try {
|
|
202
|
+
const config = this.loadConfig()
|
|
203
|
+
return config.command || {}
|
|
204
|
+
} catch (error) {
|
|
205
|
+
// Config file doesn't exist, return empty object
|
|
206
|
+
return {}
|
|
207
|
+
}
|
|
156
208
|
}
|
|
157
209
|
|
|
158
210
|
/**
|
|
159
211
|
* Get watcher configuration
|
|
160
212
|
*/
|
|
161
213
|
public getWatcherConfig(): Record<string, any> {
|
|
162
|
-
|
|
163
|
-
|
|
214
|
+
try {
|
|
215
|
+
const config = this.loadConfig()
|
|
216
|
+
return (
|
|
217
|
+
config.watcher || {
|
|
218
|
+
ignore: ['node_modules', 'dist', '.git', 'coverage'],
|
|
219
|
+
}
|
|
220
|
+
)
|
|
221
|
+
} catch (error) {
|
|
222
|
+
// Config file doesn't exist, return default watcher config
|
|
223
|
+
return { ignore: ['node_modules', 'dist', '.git', 'coverage'] }
|
|
224
|
+
}
|
|
164
225
|
}
|
|
165
226
|
|
|
166
227
|
/**
|
|
167
228
|
* Get provider configuration
|
|
168
229
|
*/
|
|
169
230
|
public getProviderConfig(): Record<string, any> {
|
|
170
|
-
|
|
171
|
-
|
|
231
|
+
try {
|
|
232
|
+
const config = this.loadConfig()
|
|
233
|
+
return config.provider || {}
|
|
234
|
+
} catch (error) {
|
|
235
|
+
// Config file doesn't exist, return empty object
|
|
236
|
+
return {}
|
|
237
|
+
}
|
|
172
238
|
}
|
|
173
239
|
|
|
174
240
|
/**
|
|
@@ -190,7 +256,7 @@ export class ConfigLoader {
|
|
|
190
256
|
*/
|
|
191
257
|
public getPrimaryAgentNames(): string[] {
|
|
192
258
|
const agents = this.getAllAgentConfigs()
|
|
193
|
-
return Object.keys(agents).filter(name => agents[name].mode === 'primary')
|
|
259
|
+
return Object.keys(agents).filter((name) => agents[name].mode === 'primary')
|
|
194
260
|
}
|
|
195
261
|
|
|
196
262
|
/**
|
|
@@ -198,7 +264,9 @@ export class ConfigLoader {
|
|
|
198
264
|
*/
|
|
199
265
|
public getSubagentNames(): string[] {
|
|
200
266
|
const agents = this.getAllAgentConfigs()
|
|
201
|
-
return Object.keys(agents).filter(
|
|
267
|
+
return Object.keys(agents).filter(
|
|
268
|
+
(name) => agents[name].mode === 'subagent'
|
|
269
|
+
)
|
|
202
270
|
}
|
|
203
271
|
|
|
204
272
|
/**
|
|
@@ -235,14 +303,19 @@ export function getConfigLoader(configPath?: string): ConfigLoader {
|
|
|
235
303
|
/**
|
|
236
304
|
* Convenience function to get agent configuration
|
|
237
305
|
*/
|
|
238
|
-
export function getAgentConfig(
|
|
306
|
+
export function getAgentConfig(
|
|
307
|
+
agentName: string,
|
|
308
|
+
configPath?: string
|
|
309
|
+
): AgentConfig | null {
|
|
239
310
|
return getConfigLoader(configPath).getAgentConfig(agentName)
|
|
240
311
|
}
|
|
241
312
|
|
|
242
313
|
/**
|
|
243
314
|
* Convenience function to get all agent configurations
|
|
244
315
|
*/
|
|
245
|
-
export function getAllAgentConfigs(
|
|
316
|
+
export function getAllAgentConfigs(
|
|
317
|
+
configPath?: string
|
|
318
|
+
): Record<string, AgentConfig> {
|
|
246
319
|
return getConfigLoader(configPath).getAllAgentConfigs()
|
|
247
320
|
}
|
|
248
321
|
|
|
@@ -256,6 +329,8 @@ export function getModelConfig(configPath?: string): ModelConfig {
|
|
|
256
329
|
/**
|
|
257
330
|
* Convenience function to get provider models
|
|
258
331
|
*/
|
|
259
|
-
export function getProviderModels(
|
|
332
|
+
export function getProviderModels(
|
|
333
|
+
configPath?: string
|
|
334
|
+
): Record<string, ProviderModelConfig> {
|
|
260
335
|
return getConfigLoader(configPath).getProviderModels()
|
|
261
|
-
}
|
|
336
|
+
}
|
|
@@ -46,7 +46,7 @@ describe('Chat Commands', () => {
|
|
|
46
46
|
})
|
|
47
47
|
|
|
48
48
|
describe('chat run command', () => {
|
|
49
|
-
it('should use
|
|
49
|
+
it('should use berget/glm-4.7 as default model', () => {
|
|
50
50
|
const chatCommand = program.commands.find((cmd) => cmd.name() === 'chat')
|
|
51
51
|
const runCommand = chatCommand?.commands.find(
|
|
52
52
|
(cmd) => cmd.name() === 'run',
|
|
@@ -56,7 +56,7 @@ describe('Chat Commands', () => {
|
|
|
56
56
|
|
|
57
57
|
// Check the help text which contains the default model
|
|
58
58
|
const helpText = runCommand?.helpInformation()
|
|
59
|
-
expect(helpText).toContain('
|
|
59
|
+
expect(helpText).toContain('glm-4.7')
|
|
60
60
|
})
|
|
61
61
|
|
|
62
62
|
it('should have streaming enabled by default', () => {
|
|
@@ -194,7 +194,7 @@ describe('Code Commands', () => {
|
|
|
194
194
|
it('should create opencode.json with correct structure', async () => {
|
|
195
195
|
// This tests the expected config structure
|
|
196
196
|
const expectedConfig = {
|
|
197
|
-
model: 'berget/
|
|
197
|
+
model: 'berget/glm-4-6',
|
|
198
198
|
apiKey: 'test-api-key',
|
|
199
199
|
projectName: 'testproject',
|
|
200
200
|
provider: 'berget',
|
|
@@ -202,7 +202,7 @@ describe('Code Commands', () => {
|
|
|
202
202
|
version: '1.0.0',
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
expect(expectedConfig.model).toBe('berget/
|
|
205
|
+
expect(expectedConfig.model).toBe('berget/glm-4-6')
|
|
206
206
|
expect(expectedConfig.provider).toBe('berget')
|
|
207
207
|
expect(expectedConfig.version).toBe('1.0.0')
|
|
208
208
|
})
|
|
@@ -259,7 +259,7 @@ describe('Code Commands', () => {
|
|
|
259
259
|
|
|
260
260
|
it('should load configuration from opencode.json', async () => {
|
|
261
261
|
const mockConfig = {
|
|
262
|
-
model: 'berget/
|
|
262
|
+
model: 'berget/glm-4-6',
|
|
263
263
|
apiKey: 'test-api-key',
|
|
264
264
|
projectName: 'testproject',
|
|
265
265
|
provider: 'berget',
|
|
@@ -284,7 +284,7 @@ describe('Code Commands', () => {
|
|
|
284
284
|
})
|
|
285
285
|
|
|
286
286
|
// Verify config structure expectations
|
|
287
|
-
expect(mockConfig.model).toBe('berget/
|
|
287
|
+
expect(mockConfig.model).toBe('berget/glm-4-6')
|
|
288
288
|
expect(mockConfig.apiKey).toBe('test-api-key')
|
|
289
289
|
expect(mockConfig.projectName).toBe('testproject')
|
|
290
290
|
})
|