btca-server 1.0.89 → 1.0.91
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 +3 -2
- package/package.json +2 -1
- package/src/agent/loop.ts +16 -3
- package/src/agent/service.ts +24 -12
- package/src/collections/service.ts +53 -14
- package/src/collections/types.ts +1 -0
- package/src/config/config.test.ts +7 -5
- package/src/config/index.ts +7 -24
- package/src/config/remote.ts +7 -16
- package/src/errors.ts +1 -1
- package/src/index.ts +33 -5
- package/src/metrics/index.ts +7 -15
- package/src/pricing/models-dev.ts +170 -0
- package/src/providers/auth.ts +7 -3
- package/src/providers/minimax.ts +28 -0
- package/src/providers/registry.ts +5 -1
- package/src/resources/impls/git.ts +81 -31
- package/src/resources/service.test.ts +64 -0
- package/src/resources/service.ts +49 -8
- package/src/resources/types.ts +3 -0
- package/src/stream/service.test.ts +89 -0
- package/src/stream/service.ts +144 -4
- package/src/stream/types.ts +48 -1
- package/src/validation/index.test.ts +46 -0
- package/src/validation/index.ts +13 -1
package/README.md
CHANGED
|
@@ -102,13 +102,14 @@ Clear all locally cloned resources.
|
|
|
102
102
|
POST /question
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
Ask a question (non-streaming response).
|
|
105
|
+
Ask a question (non-streaming response). The `resources` field accepts configured resource names or HTTPS Git URLs.
|
|
106
106
|
|
|
107
107
|
```
|
|
108
108
|
POST /question/stream
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
-
Ask a question with streaming SSE response.
|
|
111
|
+
Ask a question with streaming SSE response. The `resources` field accepts configured resource names or HTTPS Git URLs.
|
|
112
|
+
The final `done` SSE event may include optional usage/metrics (tokens, timing, throughput, and best-effort pricing).
|
|
112
113
|
|
|
113
114
|
### Model Configuration
|
|
114
115
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "btca-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.91",
|
|
4
4
|
"description": "BTCA server for answering questions about your codebase using OpenCode AI",
|
|
5
5
|
"author": "Ben Davis",
|
|
6
6
|
"license": "MIT",
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"hono": "^4.7.11",
|
|
62
62
|
"just-bash": "^2.7.0",
|
|
63
63
|
"opencode-ai": "^1.1.36",
|
|
64
|
+
"vercel-minimax-ai-provider": "^0.0.2",
|
|
64
65
|
"zod": "^3.25.76"
|
|
65
66
|
}
|
|
66
67
|
}
|
package/src/agent/loop.ts
CHANGED
|
@@ -18,7 +18,12 @@ export namespace AgentLoop {
|
|
|
18
18
|
| {
|
|
19
19
|
type: 'finish';
|
|
20
20
|
finishReason: string;
|
|
21
|
-
usage?: {
|
|
21
|
+
usage?: {
|
|
22
|
+
inputTokens?: number;
|
|
23
|
+
outputTokens?: number;
|
|
24
|
+
reasoningTokens?: number;
|
|
25
|
+
totalTokens?: number;
|
|
26
|
+
};
|
|
22
27
|
}
|
|
23
28
|
| { type: 'error'; error: Error };
|
|
24
29
|
|
|
@@ -213,7 +218,11 @@ export namespace AgentLoop {
|
|
|
213
218
|
finishReason: part.finishReason ?? 'unknown',
|
|
214
219
|
usage: {
|
|
215
220
|
inputTokens: part.totalUsage?.inputTokens,
|
|
216
|
-
outputTokens: part.totalUsage?.outputTokens
|
|
221
|
+
outputTokens: part.totalUsage?.outputTokens,
|
|
222
|
+
reasoningTokens:
|
|
223
|
+
part.totalUsage?.outputTokenDetails?.reasoningTokens ??
|
|
224
|
+
part.totalUsage?.reasoningTokens,
|
|
225
|
+
totalTokens: part.totalUsage?.totalTokens
|
|
217
226
|
}
|
|
218
227
|
});
|
|
219
228
|
break;
|
|
@@ -322,7 +331,11 @@ export namespace AgentLoop {
|
|
|
322
331
|
finishReason: part.finishReason ?? 'unknown',
|
|
323
332
|
usage: {
|
|
324
333
|
inputTokens: part.totalUsage?.inputTokens,
|
|
325
|
-
outputTokens: part.totalUsage?.outputTokens
|
|
334
|
+
outputTokens: part.totalUsage?.outputTokens,
|
|
335
|
+
reasoningTokens:
|
|
336
|
+
part.totalUsage?.outputTokenDetails?.reasoningTokens ??
|
|
337
|
+
part.totalUsage?.reasoningTokens,
|
|
338
|
+
totalTokens: part.totalUsage?.totalTokens
|
|
326
339
|
}
|
|
327
340
|
};
|
|
328
341
|
break;
|
package/src/agent/service.ts
CHANGED
|
@@ -119,10 +119,16 @@ export namespace Agent {
|
|
|
119
119
|
questionLength: question.length
|
|
120
120
|
});
|
|
121
121
|
|
|
122
|
-
const cleanup = () => {
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
|
|
122
|
+
const cleanup = async () => {
|
|
123
|
+
if (collection.vfsId) {
|
|
124
|
+
VirtualFs.dispose(collection.vfsId);
|
|
125
|
+
clearVirtualCollectionMetadata(collection.vfsId);
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
await collection.cleanup?.();
|
|
129
|
+
} catch {
|
|
130
|
+
// cleanup should never fail user-visible operations
|
|
131
|
+
}
|
|
126
132
|
};
|
|
127
133
|
|
|
128
134
|
// Validate provider is authenticated
|
|
@@ -130,7 +136,7 @@ export namespace Agent {
|
|
|
130
136
|
const requiresAuth = config.provider !== 'opencode' && config.provider !== 'openai-compat';
|
|
131
137
|
if (!isAuthed && requiresAuth) {
|
|
132
138
|
const authenticated = await Auth.getAuthenticatedProviders();
|
|
133
|
-
cleanup();
|
|
139
|
+
await cleanup();
|
|
134
140
|
throw new ProviderNotConnectedError({
|
|
135
141
|
providerId: config.provider,
|
|
136
142
|
connectedProviders: authenticated
|
|
@@ -153,7 +159,7 @@ export namespace Agent {
|
|
|
153
159
|
yield event;
|
|
154
160
|
}
|
|
155
161
|
} finally {
|
|
156
|
-
cleanup();
|
|
162
|
+
await cleanup();
|
|
157
163
|
}
|
|
158
164
|
})();
|
|
159
165
|
|
|
@@ -173,10 +179,16 @@ export namespace Agent {
|
|
|
173
179
|
questionLength: question.length
|
|
174
180
|
});
|
|
175
181
|
|
|
176
|
-
const cleanup = () => {
|
|
177
|
-
if (
|
|
178
|
-
|
|
179
|
-
|
|
182
|
+
const cleanup = async () => {
|
|
183
|
+
if (collection.vfsId) {
|
|
184
|
+
VirtualFs.dispose(collection.vfsId);
|
|
185
|
+
clearVirtualCollectionMetadata(collection.vfsId);
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
await collection.cleanup?.();
|
|
189
|
+
} catch {
|
|
190
|
+
// cleanup should never fail user-visible operations
|
|
191
|
+
}
|
|
180
192
|
};
|
|
181
193
|
|
|
182
194
|
// Validate provider is authenticated
|
|
@@ -184,7 +196,7 @@ export namespace Agent {
|
|
|
184
196
|
const requiresAuth = config.provider !== 'opencode' && config.provider !== 'openai-compat';
|
|
185
197
|
if (!isAuthed && requiresAuth) {
|
|
186
198
|
const authenticated = await Auth.getAuthenticatedProviders();
|
|
187
|
-
cleanup();
|
|
199
|
+
await cleanup();
|
|
188
200
|
throw new ProviderNotConnectedError({
|
|
189
201
|
providerId: config.provider,
|
|
190
202
|
connectedProviders: authenticated
|
|
@@ -203,7 +215,7 @@ export namespace Agent {
|
|
|
203
215
|
})
|
|
204
216
|
);
|
|
205
217
|
|
|
206
|
-
cleanup();
|
|
218
|
+
await cleanup();
|
|
207
219
|
|
|
208
220
|
return runResult.match({
|
|
209
221
|
ok: (result) => {
|
|
@@ -135,13 +135,37 @@ export namespace Collections {
|
|
|
135
135
|
});
|
|
136
136
|
};
|
|
137
137
|
|
|
138
|
+
const getGitHeadBranch = async (resourcePath: string) => {
|
|
139
|
+
const result = await Result.tryPromise(async () => {
|
|
140
|
+
const proc = Bun.spawn(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
141
|
+
cwd: resourcePath,
|
|
142
|
+
stdout: 'pipe',
|
|
143
|
+
stderr: 'pipe'
|
|
144
|
+
});
|
|
145
|
+
const stdout = await new Response(proc.stdout).text();
|
|
146
|
+
const exitCode = await proc.exited;
|
|
147
|
+
if (exitCode !== 0) return undefined;
|
|
148
|
+
const trimmed = stdout.trim();
|
|
149
|
+
if (!trimmed || trimmed === 'HEAD') return undefined;
|
|
150
|
+
return trimmed;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return result.match({
|
|
154
|
+
ok: (value) => value,
|
|
155
|
+
err: () => undefined
|
|
156
|
+
});
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const ANON_PREFIX = 'anonymous:';
|
|
160
|
+
const getAnonymousUrlFromName = (name: string) =>
|
|
161
|
+
name.startsWith(ANON_PREFIX) ? name.slice(ANON_PREFIX.length) : undefined;
|
|
162
|
+
|
|
138
163
|
const buildVirtualMetadata = async (args: {
|
|
139
164
|
resource: BtcaFsResource;
|
|
140
165
|
resourcePath: string;
|
|
141
166
|
loadedAt: string;
|
|
142
167
|
definition?: ReturnType<Config.Service['getResource']>;
|
|
143
168
|
}) => {
|
|
144
|
-
if (!args.definition) return null;
|
|
145
169
|
const base = {
|
|
146
170
|
name: args.resource.name,
|
|
147
171
|
fsName: args.resource.fsName,
|
|
@@ -150,13 +174,19 @@ export namespace Collections {
|
|
|
150
174
|
repoSubPaths: args.resource.repoSubPaths,
|
|
151
175
|
loadedAt: args.loadedAt
|
|
152
176
|
};
|
|
153
|
-
if (
|
|
177
|
+
if (args.resource.type !== 'git') return base;
|
|
178
|
+
|
|
179
|
+
const configuredDefinition =
|
|
180
|
+
args.definition && isGitResource(args.definition) ? args.definition : null;
|
|
181
|
+
const url = configuredDefinition?.url ?? getAnonymousUrlFromName(args.resource.name);
|
|
182
|
+
const branch = configuredDefinition?.branch ?? (await getGitHeadBranch(args.resourcePath));
|
|
154
183
|
const commit = await getGitHeadHash(args.resourcePath);
|
|
184
|
+
|
|
155
185
|
return {
|
|
156
186
|
...base,
|
|
157
|
-
url:
|
|
158
|
-
branch:
|
|
159
|
-
commit
|
|
187
|
+
...(url ? { url } : {}),
|
|
188
|
+
...(branch ? { branch } : {}),
|
|
189
|
+
...(commit ? { commit } : {})
|
|
160
190
|
};
|
|
161
191
|
};
|
|
162
192
|
|
|
@@ -184,11 +214,18 @@ export namespace Collections {
|
|
|
184
214
|
VirtualFs.dispose(vfsId);
|
|
185
215
|
clearVirtualCollectionMetadata(vfsId);
|
|
186
216
|
};
|
|
217
|
+
const cleanupResources = (resources: BtcaFsResource[]) =>
|
|
218
|
+
Promise.all(
|
|
219
|
+
resources.map(async (resource) => {
|
|
220
|
+
if (!resource.cleanup) return;
|
|
221
|
+
await ignoreErrors(() => resource.cleanup!());
|
|
222
|
+
})
|
|
223
|
+
);
|
|
187
224
|
|
|
225
|
+
const loadedResources: BtcaFsResource[] = [];
|
|
188
226
|
const result = await Result.gen(async function* () {
|
|
189
227
|
yield* Result.await(initVirtualRoot(collectionPath, vfsId));
|
|
190
228
|
|
|
191
|
-
const loadedResources: BtcaFsResource[] = [];
|
|
192
229
|
for (const name of sortedNames) {
|
|
193
230
|
const resource = yield* Result.await(loadResource(args.resources, name, quiet));
|
|
194
231
|
loadedResources.push(resource);
|
|
@@ -235,17 +272,19 @@ export namespace Collections {
|
|
|
235
272
|
return Result.ok({
|
|
236
273
|
path: collectionPath,
|
|
237
274
|
agentInstructions: instructionBlocks.join('\n\n'),
|
|
238
|
-
vfsId
|
|
275
|
+
vfsId,
|
|
276
|
+
cleanup: async () => {
|
|
277
|
+
await cleanupResources(loadedResources);
|
|
278
|
+
}
|
|
239
279
|
});
|
|
240
280
|
});
|
|
241
281
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
});
|
|
282
|
+
if (!Result.isOk(result)) {
|
|
283
|
+
cleanupVirtual();
|
|
284
|
+
await cleanupResources(loadedResources);
|
|
285
|
+
throw result.error;
|
|
286
|
+
}
|
|
287
|
+
return result.value;
|
|
249
288
|
})
|
|
250
289
|
};
|
|
251
290
|
};
|
package/src/collections/types.ts
CHANGED
|
@@ -443,15 +443,17 @@ describe('Config', () => {
|
|
|
443
443
|
const config = await Config.load();
|
|
444
444
|
|
|
445
445
|
// Update the model
|
|
446
|
-
|
|
446
|
+
const nextProvider = 'openrouter';
|
|
447
|
+
const nextModel = 'openai/gpt-4o-mini';
|
|
448
|
+
await config.updateModel(nextProvider, nextModel);
|
|
447
449
|
|
|
448
|
-
expect(config.provider).toBe(
|
|
449
|
-
expect(config.model).toBe(
|
|
450
|
+
expect(config.provider).toBe(nextProvider);
|
|
451
|
+
expect(config.model).toBe(nextModel);
|
|
450
452
|
|
|
451
453
|
// CRITICAL: Verify project config was updated
|
|
452
454
|
const savedProjectConfig = JSON.parse(await fs.readFile(projectConfigPath, 'utf-8'));
|
|
453
|
-
expect(savedProjectConfig.provider).toBe(
|
|
454
|
-
expect(savedProjectConfig.model).toBe(
|
|
455
|
+
expect(savedProjectConfig.provider).toBe(nextProvider);
|
|
456
|
+
expect(savedProjectConfig.model).toBe(nextModel);
|
|
455
457
|
// Global resources should NOT have leaked into project config
|
|
456
458
|
expect(savedProjectConfig.resources.length).toBe(0);
|
|
457
459
|
|
package/src/config/index.ts
CHANGED
|
@@ -464,12 +464,8 @@ export namespace Config {
|
|
|
464
464
|
return Result.ok(migrated);
|
|
465
465
|
});
|
|
466
466
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
err: (error) => {
|
|
470
|
-
throw error;
|
|
471
|
-
}
|
|
472
|
-
});
|
|
467
|
+
if (!Result.isOk(result)) throw result.error;
|
|
468
|
+
const saved = result.value;
|
|
473
469
|
|
|
474
470
|
Metrics.info('config.legacy.migrated', {
|
|
475
471
|
newPath: newConfigPath,
|
|
@@ -498,12 +494,8 @@ export namespace Config {
|
|
|
498
494
|
return Result.ok(stored);
|
|
499
495
|
});
|
|
500
496
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
err: (error) => {
|
|
504
|
-
throw error;
|
|
505
|
-
}
|
|
506
|
-
});
|
|
497
|
+
if (!Result.isOk(result)) throw result.error;
|
|
498
|
+
return result.value;
|
|
507
499
|
};
|
|
508
500
|
|
|
509
501
|
const createDefaultConfig = async (configPath: string): Promise<StoredConfig> => {
|
|
@@ -540,12 +532,8 @@ export namespace Config {
|
|
|
540
532
|
return Result.ok(defaultStored);
|
|
541
533
|
});
|
|
542
534
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
err: (error) => {
|
|
546
|
-
throw error;
|
|
547
|
-
}
|
|
548
|
-
});
|
|
535
|
+
if (!Result.isOk(result)) throw result.error;
|
|
536
|
+
return result.value;
|
|
549
537
|
};
|
|
550
538
|
|
|
551
539
|
const saveConfig = async (configPath: string, stored: StoredConfig): Promise<void> => {
|
|
@@ -555,12 +543,7 @@ export namespace Config {
|
|
|
555
543
|
`Failed to save config to: "${configPath}"`,
|
|
556
544
|
'Check that you have write permissions and the disk is not full.'
|
|
557
545
|
);
|
|
558
|
-
result.
|
|
559
|
-
ok: () => undefined,
|
|
560
|
-
err: (error) => {
|
|
561
|
-
throw error;
|
|
562
|
-
}
|
|
563
|
-
});
|
|
546
|
+
if (!Result.isOk(result)) throw result.error;
|
|
564
547
|
};
|
|
565
548
|
|
|
566
549
|
/**
|
package/src/config/remote.ts
CHANGED
|
@@ -308,12 +308,8 @@ export namespace RemoteConfigService {
|
|
|
308
308
|
return Result.ok(undefined);
|
|
309
309
|
});
|
|
310
310
|
|
|
311
|
-
result.
|
|
312
|
-
|
|
313
|
-
err: (error) => {
|
|
314
|
-
throw error;
|
|
315
|
-
}
|
|
316
|
-
});
|
|
311
|
+
if (!Result.isOk(result)) throw result.error;
|
|
312
|
+
Metrics.info('remote.auth.saved', { path: authPath });
|
|
317
313
|
}
|
|
318
314
|
|
|
319
315
|
/**
|
|
@@ -394,16 +390,11 @@ export namespace RemoteConfigService {
|
|
|
394
390
|
})
|
|
395
391
|
});
|
|
396
392
|
|
|
397
|
-
result.
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
resourceCount: config.resources.length
|
|
403
|
-
}),
|
|
404
|
-
err: (error) => {
|
|
405
|
-
throw error;
|
|
406
|
-
}
|
|
393
|
+
if (!Result.isOk(result)) throw result.error;
|
|
394
|
+
Metrics.info('remote.config.saved', {
|
|
395
|
+
path: configPath,
|
|
396
|
+
project: config.project,
|
|
397
|
+
resourceCount: config.resources.length
|
|
407
398
|
});
|
|
408
399
|
}
|
|
409
400
|
|
package/src/errors.ts
CHANGED
|
@@ -61,7 +61,7 @@ export const CommonHints = {
|
|
|
61
61
|
CHECK_NETWORK: 'Check your internet connection and try again.',
|
|
62
62
|
CHECK_URL: 'Verify the URL is correct and the repository exists.',
|
|
63
63
|
CHECK_BRANCH:
|
|
64
|
-
'Verify the branch name exists in the repository. Common branches are "main", "master", or "dev".',
|
|
64
|
+
'Verify the branch name exists in the repository. Common branches are "main", "master", "trunk", or "dev".',
|
|
65
65
|
CHECK_CONFIG: 'Check your btca config file for errors.',
|
|
66
66
|
CHECK_PERMISSIONS:
|
|
67
67
|
'Ensure you have access to the repository. Private repos require authentication.',
|
package/src/index.ts
CHANGED
|
@@ -10,11 +10,17 @@ import { Config } from './config/index.ts';
|
|
|
10
10
|
import { Context } from './context/index.ts';
|
|
11
11
|
import { getErrorMessage, getErrorTag, getErrorHint } from './errors.ts';
|
|
12
12
|
import { Metrics } from './metrics/index.ts';
|
|
13
|
+
import { ModelsDevPricing } from './pricing/models-dev.ts';
|
|
13
14
|
import { Resources } from './resources/service.ts';
|
|
14
15
|
import { GitResourceSchema, LocalResourceSchema } from './resources/schema.ts';
|
|
15
16
|
import { StreamService } from './stream/service.ts';
|
|
16
17
|
import type { BtcaStreamMetaEvent } from './stream/types.ts';
|
|
17
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
LIMITS,
|
|
20
|
+
normalizeGitHubUrl,
|
|
21
|
+
validateGitUrl,
|
|
22
|
+
validateResourceReference
|
|
23
|
+
} from './validation/index.ts';
|
|
18
24
|
import { clearAllVirtualCollectionMetadata } from './collections/virtual-metadata.ts';
|
|
19
25
|
import { VirtualFs } from './vfs/virtual-fs.ts';
|
|
20
26
|
|
|
@@ -37,6 +43,8 @@ import { VirtualFs } from './vfs/virtual-fs.ts';
|
|
|
37
43
|
const DEFAULT_PORT = 8080;
|
|
38
44
|
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : DEFAULT_PORT;
|
|
39
45
|
|
|
46
|
+
const modelsDevPricing = ModelsDevPricing.create();
|
|
47
|
+
|
|
40
48
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
41
49
|
// Request Schemas
|
|
42
50
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -63,6 +71,22 @@ const ResourceNameField = z
|
|
|
63
71
|
.refine((name) => !name.includes('//'), 'Resource name must not contain "//"')
|
|
64
72
|
.refine((name) => !name.endsWith('/'), 'Resource name must not end with "/"');
|
|
65
73
|
|
|
74
|
+
const ResourceReferenceField = z.string().superRefine((value, ctx) => {
|
|
75
|
+
const result = validateResourceReference(value);
|
|
76
|
+
if (!result.valid) {
|
|
77
|
+
ctx.addIssue({
|
|
78
|
+
code: 'custom',
|
|
79
|
+
message: result.error
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const normalizeQuestionResourceReference = (reference: string): string => {
|
|
85
|
+
const gitUrlResult = validateGitUrl(reference);
|
|
86
|
+
if (gitUrlResult.valid) return gitUrlResult.value;
|
|
87
|
+
return reference;
|
|
88
|
+
};
|
|
89
|
+
|
|
66
90
|
const QuestionRequestSchema = z.object({
|
|
67
91
|
question: z
|
|
68
92
|
.string()
|
|
@@ -72,7 +96,7 @@ const QuestionRequestSchema = z.object({
|
|
|
72
96
|
`Question too long (max ${LIMITS.QUESTION_MAX.toLocaleString()} chars). This includes conversation history - try starting a new thread or clearing the chat.`
|
|
73
97
|
),
|
|
74
98
|
resources: z
|
|
75
|
-
.array(
|
|
99
|
+
.array(ResourceReferenceField)
|
|
76
100
|
.max(
|
|
77
101
|
LIMITS.MAX_RESOURCES_PER_REQUEST,
|
|
78
102
|
`Too many resources (max ${LIMITS.MAX_RESOURCES_PER_REQUEST})`
|
|
@@ -295,7 +319,7 @@ const createApp = (deps: {
|
|
|
295
319
|
const decoded = await decodeJson(c.req.raw, QuestionRequestSchema);
|
|
296
320
|
const resourceNames =
|
|
297
321
|
decoded.resources && decoded.resources.length > 0
|
|
298
|
-
? decoded.resources
|
|
322
|
+
? Array.from(new Set(decoded.resources.map(normalizeQuestionResourceReference)))
|
|
299
323
|
: config.resources.map((r) => r.name);
|
|
300
324
|
|
|
301
325
|
const collectionKey = getCollectionKey(resourceNames);
|
|
@@ -327,10 +351,11 @@ const createApp = (deps: {
|
|
|
327
351
|
|
|
328
352
|
// POST /question/stream
|
|
329
353
|
.post('/question/stream', async (c: HonoContext) => {
|
|
354
|
+
const requestStartMs = performance.now();
|
|
330
355
|
const decoded = await decodeJson(c.req.raw, QuestionRequestSchema);
|
|
331
356
|
const resourceNames =
|
|
332
357
|
decoded.resources && decoded.resources.length > 0
|
|
333
|
-
? decoded.resources
|
|
358
|
+
? Array.from(new Set(decoded.resources.map(normalizeQuestionResourceReference)))
|
|
334
359
|
: config.resources.map((r) => r.name);
|
|
335
360
|
|
|
336
361
|
const collectionKey = getCollectionKey(resourceNames);
|
|
@@ -361,10 +386,13 @@ const createApp = (deps: {
|
|
|
361
386
|
} satisfies BtcaStreamMetaEvent;
|
|
362
387
|
|
|
363
388
|
Metrics.info('question.stream.start', { collectionKey });
|
|
389
|
+
modelsDevPricing.prefetch();
|
|
364
390
|
const stream = StreamService.createSseStream({
|
|
365
391
|
meta,
|
|
366
392
|
eventStream,
|
|
367
|
-
question: decoded.question
|
|
393
|
+
question: decoded.question,
|
|
394
|
+
requestStartMs,
|
|
395
|
+
pricing: modelsDevPricing
|
|
368
396
|
});
|
|
369
397
|
|
|
370
398
|
return new Response(stream, {
|
package/src/metrics/index.ts
CHANGED
|
@@ -46,20 +46,12 @@ export namespace Metrics {
|
|
|
46
46
|
): Promise<T> => {
|
|
47
47
|
const start = performance.now();
|
|
48
48
|
const result = await Result.tryPromise(fn);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
name,
|
|
57
|
-
ms: Math.round(performance.now() - start),
|
|
58
|
-
...fields,
|
|
59
|
-
error: errorInfo(cause)
|
|
60
|
-
});
|
|
61
|
-
throw cause;
|
|
62
|
-
}
|
|
63
|
-
});
|
|
49
|
+
const ms = Math.round(performance.now() - start);
|
|
50
|
+
if (!Result.isOk(result)) {
|
|
51
|
+
error('span.err', { name, ms, ...fields, error: errorInfo(result.error) });
|
|
52
|
+
throw result.error;
|
|
53
|
+
}
|
|
54
|
+
info('span.ok', { name, ms, ...fields });
|
|
55
|
+
return result.value;
|
|
64
56
|
};
|
|
65
57
|
}
|