btca-server 1.0.82 → 1.0.84
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/package.json +1 -1
- package/src/agent/loop.ts +16 -4
- package/src/agent/service.ts +8 -4
- package/src/config/index.ts +76 -4
- package/src/index.ts +14 -3
- package/src/providers/auth.ts +3 -0
- package/src/providers/model.ts +35 -3
- package/src/providers/openai-compat.ts +24 -0
- package/src/providers/registry.ts +3 -0
package/package.json
CHANGED
package/src/agent/loop.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { streamText, tool, stepCountIs, type ModelMessage } from 'ai';
|
|
6
6
|
|
|
7
7
|
import { Model } from '../providers/index.ts';
|
|
8
|
+
import type { ProviderOptions } from '../providers/registry.ts';
|
|
8
9
|
import { ReadTool, GrepTool, GlobTool, ListTool } from '../tools/index.ts';
|
|
9
10
|
|
|
10
11
|
export namespace AgentLoop {
|
|
@@ -30,6 +31,7 @@ export namespace AgentLoop {
|
|
|
30
31
|
agentInstructions: string;
|
|
31
32
|
question: string;
|
|
32
33
|
maxSteps?: number;
|
|
34
|
+
providerOptions?: Partial<ProviderOptions>;
|
|
33
35
|
};
|
|
34
36
|
|
|
35
37
|
// Result type
|
|
@@ -135,10 +137,15 @@ export namespace AgentLoop {
|
|
|
135
137
|
const systemPrompt = buildSystemPrompt(agentInstructions);
|
|
136
138
|
const sessionId = crypto.randomUUID();
|
|
137
139
|
|
|
140
|
+
const mergedProviderOptions =
|
|
141
|
+
providerId === 'openai'
|
|
142
|
+
? { ...options.providerOptions, instructions: systemPrompt, sessionId }
|
|
143
|
+
: options.providerOptions;
|
|
144
|
+
|
|
138
145
|
// Get the model
|
|
139
146
|
const model = await Model.getModel(providerId, modelId, {
|
|
140
|
-
providerOptions:
|
|
141
|
-
|
|
147
|
+
providerOptions: mergedProviderOptions,
|
|
148
|
+
allowMissingAuth: providerId === 'openai-compat'
|
|
142
149
|
});
|
|
143
150
|
|
|
144
151
|
// Get initial context
|
|
@@ -244,10 +251,15 @@ export namespace AgentLoop {
|
|
|
244
251
|
const systemPrompt = buildSystemPrompt(agentInstructions);
|
|
245
252
|
const sessionId = crypto.randomUUID();
|
|
246
253
|
|
|
254
|
+
const mergedProviderOptions =
|
|
255
|
+
providerId === 'openai'
|
|
256
|
+
? { ...options.providerOptions, instructions: systemPrompt, sessionId }
|
|
257
|
+
: options.providerOptions;
|
|
258
|
+
|
|
247
259
|
// Get the model
|
|
248
260
|
const model = await Model.getModel(providerId, modelId, {
|
|
249
|
-
providerOptions:
|
|
250
|
-
|
|
261
|
+
providerOptions: mergedProviderOptions,
|
|
262
|
+
allowMissingAuth: providerId === 'openai-compat'
|
|
251
263
|
});
|
|
252
264
|
|
|
253
265
|
// Get initial context
|
package/src/agent/service.ts
CHANGED
|
@@ -127,7 +127,8 @@ export namespace Agent {
|
|
|
127
127
|
|
|
128
128
|
// Validate provider is authenticated
|
|
129
129
|
const isAuthed = await Auth.isAuthenticated(config.provider);
|
|
130
|
-
|
|
130
|
+
const requiresAuth = config.provider !== 'opencode' && config.provider !== 'openai-compat';
|
|
131
|
+
if (!isAuthed && requiresAuth) {
|
|
131
132
|
const authenticated = await Auth.getAuthenticatedProviders();
|
|
132
133
|
cleanup();
|
|
133
134
|
throw new ProviderNotConnectedError({
|
|
@@ -145,7 +146,8 @@ export namespace Agent {
|
|
|
145
146
|
collectionPath: collection.path,
|
|
146
147
|
vfsId: collection.vfsId,
|
|
147
148
|
agentInstructions: collection.agentInstructions,
|
|
148
|
-
question
|
|
149
|
+
question,
|
|
150
|
+
providerOptions: config.getProviderOptions(config.provider)
|
|
149
151
|
});
|
|
150
152
|
for await (const event of stream) {
|
|
151
153
|
yield event;
|
|
@@ -179,7 +181,8 @@ export namespace Agent {
|
|
|
179
181
|
|
|
180
182
|
// Validate provider is authenticated
|
|
181
183
|
const isAuthed = await Auth.isAuthenticated(config.provider);
|
|
182
|
-
|
|
184
|
+
const requiresAuth = config.provider !== 'opencode' && config.provider !== 'openai-compat';
|
|
185
|
+
if (!isAuthed && requiresAuth) {
|
|
183
186
|
const authenticated = await Auth.getAuthenticatedProviders();
|
|
184
187
|
cleanup();
|
|
185
188
|
throw new ProviderNotConnectedError({
|
|
@@ -195,7 +198,8 @@ export namespace Agent {
|
|
|
195
198
|
collectionPath: collection.path,
|
|
196
199
|
vfsId: collection.vfsId,
|
|
197
200
|
agentInstructions: collection.agentInstructions,
|
|
198
|
-
question
|
|
201
|
+
question,
|
|
202
|
+
providerOptions: config.getProviderOptions(config.provider)
|
|
199
203
|
})
|
|
200
204
|
);
|
|
201
205
|
|
package/src/config/index.ts
CHANGED
|
@@ -50,6 +50,13 @@ export const DEFAULT_RESOURCES: ResourceDefinition[] = [
|
|
|
50
50
|
}
|
|
51
51
|
];
|
|
52
52
|
|
|
53
|
+
const ProviderOptionsSchema = z.object({
|
|
54
|
+
baseURL: z.string().optional(),
|
|
55
|
+
name: z.string().optional()
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const ProviderOptionsMapSchema = z.record(z.string(), ProviderOptionsSchema);
|
|
59
|
+
|
|
53
60
|
const StoredConfigSchema = z.object({
|
|
54
61
|
$schema: z.string().optional(),
|
|
55
62
|
dataDirectory: z.string().optional(),
|
|
@@ -57,10 +64,13 @@ const StoredConfigSchema = z.object({
|
|
|
57
64
|
resources: z.array(ResourceDefinitionSchema),
|
|
58
65
|
// Provider and model are optional - defaults are applied when loading
|
|
59
66
|
model: z.string().optional(),
|
|
60
|
-
provider: z.string().optional()
|
|
67
|
+
provider: z.string().optional(),
|
|
68
|
+
providerOptions: ProviderOptionsMapSchema.optional()
|
|
61
69
|
});
|
|
62
70
|
|
|
63
71
|
type StoredConfig = z.infer<typeof StoredConfigSchema>;
|
|
72
|
+
type ProviderOptionsConfig = z.infer<typeof ProviderOptionsSchema>;
|
|
73
|
+
type ProviderOptionsMap = z.infer<typeof ProviderOptionsMapSchema>;
|
|
64
74
|
|
|
65
75
|
// Legacy config schemas (btca.json format from old CLI)
|
|
66
76
|
// There are two legacy formats:
|
|
@@ -121,8 +131,13 @@ export namespace Config {
|
|
|
121
131
|
provider: string;
|
|
122
132
|
providerTimeoutMs?: number;
|
|
123
133
|
configPath: string;
|
|
134
|
+
getProviderOptions: (providerId: string) => ProviderOptionsConfig | undefined;
|
|
124
135
|
getResource: (name: string) => ResourceDefinition | undefined;
|
|
125
|
-
updateModel: (
|
|
136
|
+
updateModel: (
|
|
137
|
+
provider: string,
|
|
138
|
+
model: string,
|
|
139
|
+
providerOptions?: ProviderOptionsConfig
|
|
140
|
+
) => Promise<{ provider: string; model: string }>;
|
|
126
141
|
addResource: (resource: ResourceDefinition) => Promise<ResourceDefinition>;
|
|
127
142
|
removeResource: (name: string) => Promise<void>;
|
|
128
143
|
clearResources: () => Promise<{ cleared: number }>;
|
|
@@ -585,6 +600,28 @@ export namespace Config {
|
|
|
585
600
|
return Array.from(resourceMap.values());
|
|
586
601
|
};
|
|
587
602
|
|
|
603
|
+
const mergeProviderOptions = (
|
|
604
|
+
globalConfigValue: StoredConfig,
|
|
605
|
+
projectConfigValue: StoredConfig | null
|
|
606
|
+
): ProviderOptionsMap => {
|
|
607
|
+
const merged: ProviderOptionsMap = {};
|
|
608
|
+
const globalOptions = globalConfigValue.providerOptions ?? {};
|
|
609
|
+
const projectOptions = projectConfigValue?.providerOptions ?? {};
|
|
610
|
+
|
|
611
|
+
for (const [providerId, options] of Object.entries(globalOptions)) {
|
|
612
|
+
merged[providerId] = { ...options };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
for (const [providerId, options] of Object.entries(projectOptions)) {
|
|
616
|
+
merged[providerId] = { ...(merged[providerId] ?? {}), ...options };
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return merged;
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
const getMergedProviderOptions = (): ProviderOptionsMap =>
|
|
623
|
+
mergeProviderOptions(currentGlobalConfig, currentProjectConfig);
|
|
624
|
+
|
|
588
625
|
// Get the config that should be used for model/provider
|
|
589
626
|
const getActiveConfig = (): StoredConfig => {
|
|
590
627
|
return currentProjectConfig ?? currentGlobalConfig;
|
|
@@ -619,9 +656,14 @@ export namespace Config {
|
|
|
619
656
|
get providerTimeoutMs() {
|
|
620
657
|
return getActiveConfig().providerTimeoutMs;
|
|
621
658
|
},
|
|
659
|
+
getProviderOptions: (providerId: string) => getMergedProviderOptions()[providerId],
|
|
622
660
|
getResource: (name: string) => getMergedResources().find((r) => r.name === name),
|
|
623
661
|
|
|
624
|
-
updateModel: async (
|
|
662
|
+
updateModel: async (
|
|
663
|
+
provider: string,
|
|
664
|
+
model: string,
|
|
665
|
+
providerOptions?: ProviderOptionsConfig
|
|
666
|
+
) => {
|
|
625
667
|
if (!isProviderSupported(provider)) {
|
|
626
668
|
const available = getSupportedProviders();
|
|
627
669
|
throw new ConfigError({
|
|
@@ -630,7 +672,37 @@ export namespace Config {
|
|
|
630
672
|
});
|
|
631
673
|
}
|
|
632
674
|
const mutableConfig = getMutableConfig();
|
|
633
|
-
const
|
|
675
|
+
const existingProviderOptions = mutableConfig.providerOptions ?? {};
|
|
676
|
+
const nextProviderOptions = providerOptions
|
|
677
|
+
? {
|
|
678
|
+
...existingProviderOptions,
|
|
679
|
+
[provider]: {
|
|
680
|
+
...(existingProviderOptions[provider] ?? {}),
|
|
681
|
+
...providerOptions
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
: existingProviderOptions;
|
|
685
|
+
const updated = {
|
|
686
|
+
...mutableConfig,
|
|
687
|
+
provider,
|
|
688
|
+
model,
|
|
689
|
+
...(providerOptions ? { providerOptions: nextProviderOptions } : {})
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
if (provider === 'openai-compat') {
|
|
693
|
+
const merged = currentProjectConfig
|
|
694
|
+
? mergeProviderOptions(currentGlobalConfig, updated)
|
|
695
|
+
: mergeProviderOptions(updated, null);
|
|
696
|
+
const compat = merged['openai-compat'];
|
|
697
|
+
const baseURL = compat?.baseURL?.trim();
|
|
698
|
+
const name = compat?.name?.trim();
|
|
699
|
+
if (!baseURL || !name) {
|
|
700
|
+
throw new ConfigError({
|
|
701
|
+
message: 'openai-compat requires baseURL and name',
|
|
702
|
+
hint: 'Run "btca connect -p openai-compat" to configure baseURL and name.'
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
}
|
|
634
706
|
setMutableConfig(updated);
|
|
635
707
|
await saveConfig(configPath, updated);
|
|
636
708
|
Metrics.info('config.model.updated', { provider, model });
|
package/src/index.ts
CHANGED
|
@@ -91,7 +91,13 @@ const UpdateModelRequestSchema = z.object({
|
|
|
91
91
|
.string()
|
|
92
92
|
.min(1, 'Model name cannot be empty')
|
|
93
93
|
.max(LIMITS.MODEL_NAME_MAX)
|
|
94
|
-
.regex(SAFE_NAME_REGEX, 'Invalid model name format')
|
|
94
|
+
.regex(SAFE_NAME_REGEX, 'Invalid model name format'),
|
|
95
|
+
providerOptions: z
|
|
96
|
+
.object({
|
|
97
|
+
baseURL: z.string().optional(),
|
|
98
|
+
name: z.string().optional()
|
|
99
|
+
})
|
|
100
|
+
.optional()
|
|
95
101
|
});
|
|
96
102
|
|
|
97
103
|
/**
|
|
@@ -212,7 +218,8 @@ const createApp = (deps: {
|
|
|
212
218
|
tag === 'ConfigError' ||
|
|
213
219
|
tag === 'InvalidProviderError' ||
|
|
214
220
|
tag === 'InvalidModelError' ||
|
|
215
|
-
tag === 'ProviderNotConnectedError'
|
|
221
|
+
tag === 'ProviderNotConnectedError' ||
|
|
222
|
+
tag === 'ProviderOptionsError'
|
|
216
223
|
? 400
|
|
217
224
|
: 500;
|
|
218
225
|
return c.json({ error: message, tag, ...(hint && { hint }) }, status);
|
|
@@ -372,7 +379,11 @@ const createApp = (deps: {
|
|
|
372
379
|
// PUT /config/model - Update model configuration
|
|
373
380
|
.put('/config/model', async (c: HonoContext) => {
|
|
374
381
|
const decoded = await decodeJson(c.req.raw, UpdateModelRequestSchema);
|
|
375
|
-
const result = await config.updateModel(
|
|
382
|
+
const result = await config.updateModel(
|
|
383
|
+
decoded.provider,
|
|
384
|
+
decoded.model,
|
|
385
|
+
decoded.providerOptions
|
|
386
|
+
);
|
|
376
387
|
return c.json(result);
|
|
377
388
|
})
|
|
378
389
|
|
package/src/providers/auth.ts
CHANGED
|
@@ -25,6 +25,7 @@ export namespace Auth {
|
|
|
25
25
|
'github-copilot': ['oauth'],
|
|
26
26
|
openrouter: ['api'],
|
|
27
27
|
openai: ['oauth'],
|
|
28
|
+
'openai-compat': ['api'],
|
|
28
29
|
anthropic: ['api'],
|
|
29
30
|
google: ['api', 'oauth']
|
|
30
31
|
};
|
|
@@ -164,6 +165,8 @@ export namespace Auth {
|
|
|
164
165
|
return 'Run "btca connect -p github-copilot" and complete device flow OAuth.';
|
|
165
166
|
case 'openai':
|
|
166
167
|
return 'Run "opencode auth --provider openai" and complete OAuth.';
|
|
168
|
+
case 'openai-compat':
|
|
169
|
+
return 'Set baseURL + name via "btca connect" and optionally add an API key.';
|
|
167
170
|
case 'anthropic':
|
|
168
171
|
return 'Run "opencode auth --provider anthropic" and enter an API key.';
|
|
169
172
|
case 'google':
|
package/src/providers/model.ts
CHANGED
|
@@ -52,11 +52,25 @@ export namespace Model {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
export class ProviderOptionsError extends Error {
|
|
56
|
+
readonly _tag = 'ProviderOptionsError';
|
|
57
|
+
readonly providerId: string;
|
|
58
|
+
readonly hint: string;
|
|
59
|
+
|
|
60
|
+
constructor(args: { providerId: string; message: string; hint: string }) {
|
|
61
|
+
super(args.message);
|
|
62
|
+
this.providerId = args.providerId;
|
|
63
|
+
this.hint = args.hint;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
55
67
|
export type ModelOptions = {
|
|
56
68
|
/** Additional provider options */
|
|
57
69
|
providerOptions?: Partial<ProviderOptions>;
|
|
58
70
|
/** Skip authentication check (useful for providers with wellknown auth) */
|
|
59
71
|
skipAuth?: boolean;
|
|
72
|
+
/** Allow missing auth for providers that can be used without credentials */
|
|
73
|
+
allowMissingAuth?: boolean;
|
|
60
74
|
};
|
|
61
75
|
|
|
62
76
|
/**
|
|
@@ -92,13 +106,17 @@ export namespace Model {
|
|
|
92
106
|
if (!options.skipAuth) {
|
|
93
107
|
const status = await Auth.getAuthStatus(normalizedProviderId);
|
|
94
108
|
if (status.status === 'missing') {
|
|
95
|
-
|
|
109
|
+
if (!options.allowMissingAuth) {
|
|
110
|
+
throw new ProviderNotAuthenticatedError(providerId);
|
|
111
|
+
}
|
|
96
112
|
}
|
|
97
113
|
if (status.status === 'invalid') {
|
|
98
114
|
throw new ProviderAuthTypeError({ providerId, authType: status.authType });
|
|
99
115
|
}
|
|
100
|
-
|
|
101
|
-
|
|
116
|
+
if (status.status === 'ok') {
|
|
117
|
+
apiKey = status.apiKey;
|
|
118
|
+
accountId = status.accountId;
|
|
119
|
+
}
|
|
102
120
|
}
|
|
103
121
|
|
|
104
122
|
// Build provider options
|
|
@@ -111,6 +129,20 @@ export namespace Model {
|
|
|
111
129
|
providerOptions.apiKey = apiKey;
|
|
112
130
|
}
|
|
113
131
|
|
|
132
|
+
if (normalizedProviderId === 'openai-compat') {
|
|
133
|
+
const baseURL = providerOptions.baseURL?.trim();
|
|
134
|
+
const name = providerOptions.name?.trim();
|
|
135
|
+
if (!baseURL || !name) {
|
|
136
|
+
throw new ProviderOptionsError({
|
|
137
|
+
providerId: normalizedProviderId,
|
|
138
|
+
message: 'openai-compat requires baseURL and name',
|
|
139
|
+
hint: 'Run "btca connect -p openai-compat" to configure baseURL and name.'
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
providerOptions.baseURL = baseURL;
|
|
143
|
+
providerOptions.name = name;
|
|
144
|
+
}
|
|
145
|
+
|
|
114
146
|
// Create the provider and get the model
|
|
115
147
|
const provider = factory(providerOptions);
|
|
116
148
|
const model = provider(modelId);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
|
2
|
+
|
|
3
|
+
export const createOpenAICompat = (
|
|
4
|
+
options: {
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
baseURL?: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
} = {}
|
|
9
|
+
) => {
|
|
10
|
+
const baseURL = options.baseURL?.trim();
|
|
11
|
+
const name = options.name?.trim();
|
|
12
|
+
|
|
13
|
+
if (!baseURL || !name) {
|
|
14
|
+
throw new Error('openai-compat requires baseURL and name');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const provider = createOpenAICompatible({
|
|
18
|
+
apiKey: options.apiKey,
|
|
19
|
+
baseURL,
|
|
20
|
+
name
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return (modelId: string) => provider.chatModel(modelId);
|
|
24
|
+
};
|
|
@@ -8,6 +8,7 @@ import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
|
8
8
|
import { createCopilotProvider } from './copilot.ts';
|
|
9
9
|
import { createOpenCodeZen } from './opencode.ts';
|
|
10
10
|
import { createOpenAICodex } from './openai.ts';
|
|
11
|
+
import { createOpenAICompat } from './openai-compat.ts';
|
|
11
12
|
import { createOpenRouter } from './openrouter.ts';
|
|
12
13
|
|
|
13
14
|
// Type for provider factory options
|
|
@@ -37,6 +38,8 @@ export const PROVIDER_REGISTRY: Record<string, ProviderFactory> = {
|
|
|
37
38
|
|
|
38
39
|
// OpenAI
|
|
39
40
|
openai: createOpenAICodex as ProviderFactory,
|
|
41
|
+
// OpenAI-compatible
|
|
42
|
+
'openai-compat': createOpenAICompat as ProviderFactory,
|
|
40
43
|
// GitHub Copilot
|
|
41
44
|
'github-copilot': createCopilotProvider as ProviderFactory,
|
|
42
45
|
// Google
|