@aliou/pi-neuralwatt 0.16.3 → 0.18.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 +0 -5
- package/extensions/provider/api/anthropic-messages.ts +6 -6
- package/extensions/provider/api/openai-completions.ts +6 -6
- package/extensions/provider/api/system-one.ts +242 -0
- package/extensions/provider/api/types.ts +6 -6
- package/extensions/provider/commands/settings/api-base-url-editor.ts +129 -0
- package/extensions/provider/commands/settings/index.ts +42 -21
- package/extensions/provider/index.ts +26 -17
- package/extensions/provider/models/build.ts +19 -3
- package/extensions/provider/models/catalog.ts +68 -8
- package/extensions/provider/models/public-models.ts +10 -42
- package/extensions/provider/models/refresh.ts +14 -10
- package/extensions/provider/provider.ts +15 -2
- package/extensions/provider/stream-simple.ts +34 -65
- package/package.json +10 -10
- package/schema.json +5 -15
- package/src/config/defaults.ts +3 -3
- package/src/config/loader.ts +25 -5
- package/src/config/migration/04-remove-sub-bar-integration.ts +31 -0
- package/src/config/migration/index.ts +4 -5
- package/src/config/types.ts +9 -11
- package/src/events.ts +1 -4
- package/src/lib/neuralwatt-api.ts +2 -2
- package/extensions/sub-bar-integration/index.ts +0 -131
- package/extensions/sub-bar-integration/snapshot.ts +0 -47
package/README.md
CHANGED
|
@@ -75,10 +75,6 @@ https://github.com/user-attachments/assets/a8994940-c467-4744-a0f2-833cb63923ff
|
|
|
75
75
|
|
|
76
76
|
When enabled, the extension notifies you when credits or energy are running low. When you have an active subscription, only energy warnings fire (credits are on-demand top-up only). Warnings use escalation on severity transitions and have a cooldown for `warning` level.
|
|
77
77
|
|
|
78
|
-
### Sub-bar Integration
|
|
79
|
-
|
|
80
|
-
When a Neuralwatt model is active, the footer status bar shows live quota usage (credits and energy). The status updates after each response and on session start.
|
|
81
|
-
|
|
82
78
|
## Settings
|
|
83
79
|
|
|
84
80
|
Configure features with `/neuralwatt:settings`:
|
|
@@ -86,7 +82,6 @@ Configure features with `/neuralwatt:settings`:
|
|
|
86
82
|
- **API** — Choose between `openai-completions` (default) and `anthropic-messages`; applies on `/reload`
|
|
87
83
|
- **Quota command** — Show/hide `/neuralwatt:quota`
|
|
88
84
|
- **Quota warnings** — Enable/disable low quota notifications
|
|
89
|
-
- **Sub-bar integration** — Show/hide usage in status bar
|
|
90
85
|
|
|
91
86
|
The provider itself cannot be disabled — it is always loaded.
|
|
92
87
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { StreamOptions } from "@earendil-works/pi-ai";
|
|
2
2
|
import { stream, streamSimple } from "@earendil-works/pi-ai/compat";
|
|
3
|
+
import { configuredApiBaseUrl } from "../../../src/config/loader";
|
|
3
4
|
import {
|
|
4
|
-
NEURALWATT_BASE_URL,
|
|
5
5
|
NEURALWATT_PROVIDER_ID,
|
|
6
6
|
NEURALWATT_REQUEST_HEADERS,
|
|
7
7
|
} from "../constants";
|
|
8
8
|
import { buildAnthropicThinkingLevelMap } from "../models/build";
|
|
9
|
-
import type {
|
|
9
|
+
import type { NeuralwattChatModel } from "../models/catalog";
|
|
10
10
|
import type { AnyStreamSimple } from "../stream-simple";
|
|
11
11
|
import type { NeuralwattApiHandler } from "./types";
|
|
12
12
|
|
|
@@ -61,7 +61,7 @@ function toMessagesBaseUrl(baseUrl: string): string {
|
|
|
61
61
|
return baseUrl.replace(/\/v1\/?$/, "");
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
function stampAnthropicModels(models:
|
|
64
|
+
function stampAnthropicModels(models: NeuralwattChatModel[]) {
|
|
65
65
|
return models.map((model) => {
|
|
66
66
|
const { reasoningContract, ...compiled } = model;
|
|
67
67
|
// No retained contract: the identity map is the alias-free special case.
|
|
@@ -77,7 +77,7 @@ function stampAnthropicModels(models: NeuralwattModel[]) {
|
|
|
77
77
|
...compiled,
|
|
78
78
|
api: "anthropic-messages" as const,
|
|
79
79
|
provider: NEURALWATT_PROVIDER_ID,
|
|
80
|
-
baseUrl: toMessagesBaseUrl(model.baseUrl ??
|
|
80
|
+
baseUrl: toMessagesBaseUrl(model.baseUrl ?? configuredApiBaseUrl()),
|
|
81
81
|
headers: NEURALWATT_REQUEST_HEADERS,
|
|
82
82
|
compat: {
|
|
83
83
|
forceAdaptiveThinking: true,
|
|
@@ -105,10 +105,10 @@ export function createAnthropicMessagesApi(options?: {
|
|
|
105
105
|
stream: (model, context, streamOptions) =>
|
|
106
106
|
stream(model, context, withReasoning(streamOptions) as never),
|
|
107
107
|
streamSimple: (model, context, simpleOptions) =>
|
|
108
|
-
(options?.streamSimple ?? streamSimple)(
|
|
108
|
+
((options?.streamSimple ?? streamSimple) as AnyStreamSimple)(
|
|
109
109
|
model,
|
|
110
110
|
context,
|
|
111
|
-
withReasoning(simpleOptions)
|
|
111
|
+
withReasoning(simpleOptions),
|
|
112
112
|
),
|
|
113
113
|
};
|
|
114
114
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { StreamOptions } from "@earendil-works/pi-ai";
|
|
2
2
|
import { stream, streamSimple } from "@earendil-works/pi-ai/compat";
|
|
3
|
+
import { configuredApiBaseUrl } from "../../../src/config/loader";
|
|
3
4
|
import {
|
|
4
|
-
NEURALWATT_BASE_URL,
|
|
5
5
|
NEURALWATT_PROVIDER_ID,
|
|
6
6
|
NEURALWATT_REQUEST_HEADERS,
|
|
7
7
|
} from "../constants";
|
|
8
|
-
import type {
|
|
8
|
+
import type { NeuralwattChatModel } from "../models/catalog";
|
|
9
9
|
import type { AnyStreamSimple } from "../stream-simple";
|
|
10
10
|
import type { NeuralwattApiHandler } from "./types";
|
|
11
11
|
|
|
@@ -60,24 +60,24 @@ export function createOpenAiCompletionsApi(options?: {
|
|
|
60
60
|
});
|
|
61
61
|
|
|
62
62
|
return {
|
|
63
|
-
stampModels: (models:
|
|
63
|
+
stampModels: (models: NeuralwattChatModel[]) =>
|
|
64
64
|
models.map((model) => {
|
|
65
65
|
const { reasoningContract: _reasoningContract, ...compiled } = model;
|
|
66
66
|
return {
|
|
67
67
|
...compiled,
|
|
68
68
|
api: "openai-completions" as const,
|
|
69
69
|
provider: NEURALWATT_PROVIDER_ID,
|
|
70
|
-
baseUrl: model.baseUrl ??
|
|
70
|
+
baseUrl: model.baseUrl ?? configuredApiBaseUrl(),
|
|
71
71
|
headers: NEURALWATT_REQUEST_HEADERS,
|
|
72
72
|
};
|
|
73
73
|
}),
|
|
74
74
|
stream: (model, context, streamOptions) =>
|
|
75
75
|
stream(model, context, withReasoningReplay(streamOptions) as never),
|
|
76
76
|
streamSimple: (model, context, simpleOptions) =>
|
|
77
|
-
(options?.streamSimple ?? streamSimple)(
|
|
77
|
+
((options?.streamSimple ?? streamSimple) as AnyStreamSimple)(
|
|
78
78
|
model,
|
|
79
79
|
context,
|
|
80
|
-
withReasoningReplay(simpleOptions)
|
|
80
|
+
withReasoningReplay(simpleOptions),
|
|
81
81
|
),
|
|
82
82
|
};
|
|
83
83
|
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ClassifierAnswer,
|
|
3
|
+
ClassifierApi,
|
|
4
|
+
ClassifierContext,
|
|
5
|
+
ClassifierFunction,
|
|
6
|
+
ClassifierModel,
|
|
7
|
+
ClassifierOptions,
|
|
8
|
+
ClassifierResult,
|
|
9
|
+
} from "@earendil-works/pi-ai";
|
|
10
|
+
import { calculateCost } from "@earendil-works/pi-ai";
|
|
11
|
+
import type { Static } from "typebox";
|
|
12
|
+
import { Type } from "typebox";
|
|
13
|
+
import { Check, Parse, ParseError } from "typebox/value";
|
|
14
|
+
import { configuredApiBaseUrl } from "../../../src/config/loader";
|
|
15
|
+
import {
|
|
16
|
+
NEURALWATT_PROVIDER_ID,
|
|
17
|
+
NEURALWATT_REQUEST_HEADERS,
|
|
18
|
+
} from "../constants";
|
|
19
|
+
import {
|
|
20
|
+
NEURALWATT_SYSTEM_ONE_API,
|
|
21
|
+
type NeuralwattClassifierModel,
|
|
22
|
+
} from "../models/build";
|
|
23
|
+
|
|
24
|
+
const UsageSchema = Type.Object({
|
|
25
|
+
input_tokens: Type.Integer({ minimum: 0 }),
|
|
26
|
+
output_tokens: Type.Integer({ minimum: 0 }),
|
|
27
|
+
});
|
|
28
|
+
const AnswerSchemas = {
|
|
29
|
+
choice: Type.Object({
|
|
30
|
+
type: Type.Literal("choice"),
|
|
31
|
+
choice: Type.String(),
|
|
32
|
+
probabilities: Type.Record(Type.String(), Type.Number()),
|
|
33
|
+
confidence: Type.Number(),
|
|
34
|
+
}),
|
|
35
|
+
score: Type.Object({
|
|
36
|
+
type: Type.Literal("score"),
|
|
37
|
+
score: Type.Number(),
|
|
38
|
+
confidence: Type.Number(),
|
|
39
|
+
}),
|
|
40
|
+
bool: Type.Object({ type: Type.Literal("noul"), noul: Type.Number() }),
|
|
41
|
+
};
|
|
42
|
+
const ErrorSchema = Type.Union([
|
|
43
|
+
Type.Object({
|
|
44
|
+
error: Type.Object({
|
|
45
|
+
message: Type.String(),
|
|
46
|
+
type: Type.String(),
|
|
47
|
+
code: Type.String(),
|
|
48
|
+
}),
|
|
49
|
+
}),
|
|
50
|
+
Type.Object({ detail: Type.String() }),
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
function responseSchema(context: ClassifierContext) {
|
|
54
|
+
return Type.Object({
|
|
55
|
+
answers: Type.Object(
|
|
56
|
+
Object.fromEntries(
|
|
57
|
+
Object.entries(context.questions).map(([id, question]) => [
|
|
58
|
+
id,
|
|
59
|
+
AnswerSchemas[question.type],
|
|
60
|
+
]),
|
|
61
|
+
),
|
|
62
|
+
),
|
|
63
|
+
usage: Type.Optional(UsageSchema),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type SystemOneResponse = Static<ReturnType<typeof responseSchema>>;
|
|
68
|
+
|
|
69
|
+
function mapAnswer(
|
|
70
|
+
value: SystemOneResponse["answers"][string],
|
|
71
|
+
): ClassifierAnswer {
|
|
72
|
+
if (value.type === "noul") return { type: "bool", probability: value.noul };
|
|
73
|
+
if (value.type === "score")
|
|
74
|
+
return { type: "score", score: value.score, confidence: value.confidence };
|
|
75
|
+
return {
|
|
76
|
+
type: "choice",
|
|
77
|
+
choice: value.choice,
|
|
78
|
+
probabilities: value.probabilities,
|
|
79
|
+
confidence: value.confidence,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function mapUsage(
|
|
84
|
+
model: ClassifierModel<ClassifierApi>,
|
|
85
|
+
value: SystemOneResponse["usage"],
|
|
86
|
+
): ClassifierResult["usage"] {
|
|
87
|
+
if (!value) return undefined;
|
|
88
|
+
const { input_tokens: input, output_tokens: output } = value;
|
|
89
|
+
const usage = {
|
|
90
|
+
input,
|
|
91
|
+
output,
|
|
92
|
+
cacheRead: 0,
|
|
93
|
+
cacheWrite: 0,
|
|
94
|
+
totalTokens: input + output,
|
|
95
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
96
|
+
};
|
|
97
|
+
calculateCost(model, usage);
|
|
98
|
+
return usage;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function billedUsageOnInvalidResponse(
|
|
102
|
+
model: ClassifierModel<ClassifierApi>,
|
|
103
|
+
error: ParseError,
|
|
104
|
+
) {
|
|
105
|
+
const usage = (error.cause.value as { usage?: unknown } | null)?.usage;
|
|
106
|
+
return Check(UsageSchema, usage) ? mapUsage(model, usage) : undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function formatRequest(
|
|
110
|
+
model: ClassifierModel<ClassifierApi>,
|
|
111
|
+
context: ClassifierContext,
|
|
112
|
+
) {
|
|
113
|
+
return {
|
|
114
|
+
model: model.id,
|
|
115
|
+
state: context.state,
|
|
116
|
+
questions: Object.fromEntries(
|
|
117
|
+
Object.entries(context.questions).map(([id, question]) => [
|
|
118
|
+
id,
|
|
119
|
+
question.type === "bool" ? { ...question, type: "noul" } : question,
|
|
120
|
+
]),
|
|
121
|
+
),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function requestHeaders(
|
|
126
|
+
model: ClassifierModel<ClassifierApi>,
|
|
127
|
+
options: ClassifierOptions,
|
|
128
|
+
) {
|
|
129
|
+
const headers = new Headers({
|
|
130
|
+
authorization: `Bearer ${options.apiKey || "-"}`,
|
|
131
|
+
"content-type": "application/json",
|
|
132
|
+
});
|
|
133
|
+
const overrides = [model.headers, options.headers].flatMap((source) =>
|
|
134
|
+
Object.entries(source ?? {}),
|
|
135
|
+
);
|
|
136
|
+
for (const [name, value] of overrides) {
|
|
137
|
+
if (value === null) headers.delete(name);
|
|
138
|
+
else headers.set(name, value);
|
|
139
|
+
}
|
|
140
|
+
return Object.fromEntries(headers);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function requestSignal({ signal, timeoutMs }: ClassifierOptions) {
|
|
144
|
+
if (timeoutMs === undefined) return signal;
|
|
145
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
146
|
+
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function request(
|
|
150
|
+
model: ClassifierModel<ClassifierApi>,
|
|
151
|
+
context: ClassifierContext,
|
|
152
|
+
options: ClassifierOptions,
|
|
153
|
+
) {
|
|
154
|
+
if (model.api !== NEURALWATT_SYSTEM_ONE_API) {
|
|
155
|
+
throw new Error(`Unsupported classifier API: ${model.api}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (context.images?.length) {
|
|
159
|
+
throw new Error("Neuralwatt classifier does not support image input");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
options.signal?.throwIfAborted();
|
|
163
|
+
|
|
164
|
+
const rawPayload = formatRequest(model, context);
|
|
165
|
+
const payload = (await options.onPayload?.(rawPayload, model)) ?? rawPayload;
|
|
166
|
+
|
|
167
|
+
const url = new URL("systemone", `${model.baseUrl.replace(/\/+$/u, "")}/`);
|
|
168
|
+
|
|
169
|
+
const requestFetch = options.fetch ?? globalThis.fetch;
|
|
170
|
+
|
|
171
|
+
const response = await requestFetch(url, {
|
|
172
|
+
method: "POST",
|
|
173
|
+
headers: requestHeaders(model, options),
|
|
174
|
+
body: JSON.stringify(payload),
|
|
175
|
+
signal: requestSignal(options),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
await options.onResponse?.(
|
|
179
|
+
{ status: response.status, headers: Object.fromEntries(response.headers) },
|
|
180
|
+
model,
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const body: unknown = await response.json();
|
|
184
|
+
if (!response.ok) {
|
|
185
|
+
const failure = Parse(ErrorSchema, body);
|
|
186
|
+
const message = "error" in failure ? failure.error.message : failure.detail;
|
|
187
|
+
throw new Error(
|
|
188
|
+
`Neuralwatt System One API returned ${response.status}: ${message}`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return Parse(responseSchema(context), body);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export const classify: ClassifierFunction = async (
|
|
196
|
+
model,
|
|
197
|
+
context,
|
|
198
|
+
options = {},
|
|
199
|
+
) => {
|
|
200
|
+
const result: ClassifierResult = {
|
|
201
|
+
api: model.api,
|
|
202
|
+
provider: model.provider,
|
|
203
|
+
model: model.id,
|
|
204
|
+
answers: {},
|
|
205
|
+
stopReason: "stop",
|
|
206
|
+
timestamp: Date.now(),
|
|
207
|
+
};
|
|
208
|
+
try {
|
|
209
|
+
const body = await request(model, context, options);
|
|
210
|
+
result.usage = mapUsage(model, body.usage);
|
|
211
|
+
result.answers = Object.fromEntries(
|
|
212
|
+
Object.keys(context.questions).map((id) => [
|
|
213
|
+
id,
|
|
214
|
+
mapAnswer(body.answers[id]),
|
|
215
|
+
]),
|
|
216
|
+
);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
result.stopReason = options.signal?.aborted ? "aborted" : "error";
|
|
219
|
+
if (error instanceof ParseError) {
|
|
220
|
+
result.usage = billedUsageOnInvalidResponse(model, error);
|
|
221
|
+
result.errorMessage = error.cause.errors
|
|
222
|
+
.map(({ instancePath, message }) => `${instancePath}: ${message}`)
|
|
223
|
+
.join("; ");
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
result.errorMessage =
|
|
227
|
+
error instanceof Error ? error.message : String(error);
|
|
228
|
+
}
|
|
229
|
+
return result;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
export function stampClassifierModels(
|
|
233
|
+
models: NeuralwattClassifierModel[],
|
|
234
|
+
): ClassifierModel<ClassifierApi>[] {
|
|
235
|
+
return models.map((model) => ({
|
|
236
|
+
...model,
|
|
237
|
+
api: NEURALWATT_SYSTEM_ONE_API,
|
|
238
|
+
provider: NEURALWATT_PROVIDER_ID,
|
|
239
|
+
baseUrl: model.baseUrl ?? configuredApiBaseUrl(),
|
|
240
|
+
headers: NEURALWATT_REQUEST_HEADERS,
|
|
241
|
+
}));
|
|
242
|
+
}
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
Api,
|
|
3
3
|
AssistantMessageEventStream,
|
|
4
|
-
Context,
|
|
5
4
|
Model,
|
|
6
5
|
SimpleStreamOptions,
|
|
7
6
|
StreamOptions,
|
|
7
|
+
TranscriptContext,
|
|
8
8
|
} from "@earendil-works/pi-ai";
|
|
9
|
-
import type {
|
|
9
|
+
import type { NeuralwattChatModel } from "../models/catalog";
|
|
10
10
|
|
|
11
|
-
/** One Neuralwatt API surface: model stamping plus submission plumbing. */
|
|
11
|
+
/** One Neuralwatt API surface: model stamping plus chat submission plumbing. */
|
|
12
12
|
export interface NeuralwattApiHandler {
|
|
13
|
-
stampModels(models:
|
|
13
|
+
stampModels(models: NeuralwattChatModel[]): Model<Api>[];
|
|
14
14
|
stream(
|
|
15
15
|
model: Model<Api>,
|
|
16
|
-
context:
|
|
16
|
+
context: TranscriptContext,
|
|
17
17
|
options?: StreamOptions,
|
|
18
18
|
): AssistantMessageEventStream;
|
|
19
19
|
streamSimple(
|
|
20
20
|
model: Model<Api>,
|
|
21
|
-
context:
|
|
21
|
+
context: TranscriptContext,
|
|
22
22
|
options?: SimpleStreamOptions,
|
|
23
23
|
): AssistantMessageEventStream;
|
|
24
24
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { Component, SettingsListTheme } from "@earendil-works/pi-tui";
|
|
2
|
+
import {
|
|
3
|
+
Input,
|
|
4
|
+
Key,
|
|
5
|
+
matchesKey,
|
|
6
|
+
truncateToWidth,
|
|
7
|
+
} from "@earendil-works/pi-tui";
|
|
8
|
+
|
|
9
|
+
export interface ApiBaseUrlEditorOptions {
|
|
10
|
+
initial: string;
|
|
11
|
+
defaultBaseUrl: string;
|
|
12
|
+
theme: SettingsListTheme;
|
|
13
|
+
requestRender: () => void;
|
|
14
|
+
onSubmit: (value: string) => void;
|
|
15
|
+
onCancel: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type Status =
|
|
19
|
+
| { kind: "idle" }
|
|
20
|
+
| { kind: "validating"; url: string }
|
|
21
|
+
| { kind: "error"; message: string };
|
|
22
|
+
|
|
23
|
+
export function normalizeApiBaseUrlInput(value: string): string {
|
|
24
|
+
return value.trim().replace(/\/+$/u, "");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function checkApiBase(
|
|
28
|
+
base: string,
|
|
29
|
+
timeoutMs = 8_000,
|
|
30
|
+
): Promise<string | null> {
|
|
31
|
+
try {
|
|
32
|
+
const response = await fetch(`${base}/models`, {
|
|
33
|
+
headers: { Accept: "application/json" },
|
|
34
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
35
|
+
});
|
|
36
|
+
return response.ok ? null : `HTTP ${response.status} from ${base}/models`;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
return error instanceof Error && error.name === "TimeoutError"
|
|
39
|
+
? `Timed out reaching ${base}/models`
|
|
40
|
+
: `Could not reach ${base}/models`;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class ApiBaseUrlEditor implements Component {
|
|
45
|
+
private readonly input = new Input();
|
|
46
|
+
private readonly defaultBaseUrl: string;
|
|
47
|
+
private readonly theme: SettingsListTheme;
|
|
48
|
+
private readonly requestRender: () => void;
|
|
49
|
+
private readonly onSubmit: (value: string) => void;
|
|
50
|
+
private readonly onCancel: () => void;
|
|
51
|
+
private status: Status = { kind: "idle" };
|
|
52
|
+
|
|
53
|
+
constructor(options: ApiBaseUrlEditorOptions) {
|
|
54
|
+
this.defaultBaseUrl = options.defaultBaseUrl;
|
|
55
|
+
this.theme = options.theme;
|
|
56
|
+
this.requestRender = options.requestRender;
|
|
57
|
+
this.onSubmit = options.onSubmit;
|
|
58
|
+
this.onCancel = options.onCancel;
|
|
59
|
+
this.input.setValue(options.initial);
|
|
60
|
+
this.input.onSubmit = (value) => void this.submit(value);
|
|
61
|
+
this.input.onEscape = () => this.onCancel();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private async submit(raw: string): Promise<void> {
|
|
65
|
+
const base = normalizeApiBaseUrlInput(raw);
|
|
66
|
+
if (base) {
|
|
67
|
+
this.status = { kind: "validating", url: `${base}/models` };
|
|
68
|
+
this.requestRender();
|
|
69
|
+
const error = await checkApiBase(base);
|
|
70
|
+
if (error) {
|
|
71
|
+
this.status = { kind: "error", message: error };
|
|
72
|
+
this.requestRender();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
this.onSubmit(base);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
invalidate(): void {}
|
|
80
|
+
|
|
81
|
+
handleInput(data: string): void {
|
|
82
|
+
if (matchesKey(data, Key.escape)) {
|
|
83
|
+
this.onCancel();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (this.status.kind === "validating") return;
|
|
87
|
+
this.status = { kind: "idle" };
|
|
88
|
+
this.input.handleInput(data);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
render(width: number): string[] {
|
|
92
|
+
const lines: string[] = [];
|
|
93
|
+
lines.push(
|
|
94
|
+
truncateToWidth(this.theme.label("API base URL", true), width, "", true),
|
|
95
|
+
);
|
|
96
|
+
lines.push(...this.input.render(width));
|
|
97
|
+
if (this.status.kind === "validating") {
|
|
98
|
+
lines.push(
|
|
99
|
+
truncateToWidth(
|
|
100
|
+
this.theme.description(`Validating ${this.status.url} …`),
|
|
101
|
+
width,
|
|
102
|
+
"",
|
|
103
|
+
true,
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
} else if (this.status.kind === "error") {
|
|
107
|
+
lines.push(
|
|
108
|
+
truncateToWidth(
|
|
109
|
+
this.theme.value(this.status.message, true),
|
|
110
|
+
width,
|
|
111
|
+
"",
|
|
112
|
+
true,
|
|
113
|
+
),
|
|
114
|
+
);
|
|
115
|
+
} else {
|
|
116
|
+
lines.push(
|
|
117
|
+
truncateToWidth(
|
|
118
|
+
this.theme.description(
|
|
119
|
+
`Enter to save · empty = direct upstream (${this.defaultBaseUrl})`,
|
|
120
|
+
),
|
|
121
|
+
width,
|
|
122
|
+
"",
|
|
123
|
+
true,
|
|
124
|
+
),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
return lines;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
NEURALWATT_CONFIG_UPDATED_EVENT,
|
|
15
15
|
type NeuralwattFeatureId,
|
|
16
16
|
} from "../../../../src/events";
|
|
17
|
+
import { ApiBaseUrlEditor } from "./api-base-url-editor";
|
|
17
18
|
|
|
18
19
|
export interface RegisterNeuralwattSettingsOptions {
|
|
19
20
|
getLoadedFeatures: () => Set<NeuralwattFeatureId>;
|
|
@@ -62,15 +63,16 @@ export function registerNeuralwattSettings(
|
|
|
62
63
|
options: RegisterNeuralwattSettingsOptions,
|
|
63
64
|
): void {
|
|
64
65
|
const { getLoadedFeatures } = options;
|
|
65
|
-
// The provider stamps `provider.api`
|
|
66
|
-
// reaches it after `/reload`.
|
|
66
|
+
// The provider stamps `provider.api` and assembles against the configured
|
|
67
|
+
// base at extension load; a saved change only reaches it after `/reload`.
|
|
67
68
|
let pendingApi: NeuralwattApi | undefined;
|
|
69
|
+
let pendingApiBaseUrl: string | undefined;
|
|
68
70
|
|
|
69
71
|
registerSettingsCommand<NeuralwattConfig, ResolvedNeuralwattConfig>(pi, {
|
|
70
72
|
commandName: "neuralwatt:settings",
|
|
71
73
|
title: "Neuralwatt Settings",
|
|
72
74
|
configStore: configLoader,
|
|
73
|
-
buildSections: (tabConfig, resolved): SettingsSection[] => {
|
|
75
|
+
buildSections: (tabConfig, resolved, buildCtx): SettingsSection[] => {
|
|
74
76
|
const loaded = getLoadedFeatures();
|
|
75
77
|
return [
|
|
76
78
|
{
|
|
@@ -80,10 +82,41 @@ export function registerNeuralwattSettings(
|
|
|
80
82
|
id: "api",
|
|
81
83
|
label: "API",
|
|
82
84
|
description:
|
|
83
|
-
"Serve models via the OpenAI
|
|
85
|
+
"Serve models via the OpenAI completions endpoint or the Anthropic endpoint",
|
|
84
86
|
currentValue: tabConfig?.provider?.api ?? resolved.provider.api,
|
|
85
87
|
values: ["openai-completions", "anthropic-messages"],
|
|
86
88
|
},
|
|
89
|
+
{
|
|
90
|
+
id: "apiBaseUrl",
|
|
91
|
+
label: "API base URL",
|
|
92
|
+
description:
|
|
93
|
+
"Base URL for API requests; empty uses the default upstream",
|
|
94
|
+
currentValue: tabConfig?.provider?.apiBaseUrl || "(not set)",
|
|
95
|
+
submenu: (current, submenuDone, submenuCtx) => {
|
|
96
|
+
const display = (value: string) => value || "(not set)";
|
|
97
|
+
return new ApiBaseUrlEditor({
|
|
98
|
+
initial: current === "(not set)" ? "" : current,
|
|
99
|
+
defaultBaseUrl: resolved.provider.apiBaseUrl,
|
|
100
|
+
theme: buildCtx.theme,
|
|
101
|
+
requestRender: submenuCtx.requestRender,
|
|
102
|
+
onSubmit: (value) => {
|
|
103
|
+
const updated = structuredClone(
|
|
104
|
+
tabConfig ?? {},
|
|
105
|
+
) as NeuralwattConfig;
|
|
106
|
+
updated.provider = { ...updated.provider };
|
|
107
|
+
if (value) {
|
|
108
|
+
updated.provider.apiBaseUrl = value;
|
|
109
|
+
} else {
|
|
110
|
+
delete updated.provider.apiBaseUrl;
|
|
111
|
+
}
|
|
112
|
+
buildCtx.setDraft(updated);
|
|
113
|
+
pendingApiBaseUrl = value;
|
|
114
|
+
submenuDone(display(value));
|
|
115
|
+
},
|
|
116
|
+
onCancel: () => submenuDone(display(current)),
|
|
117
|
+
});
|
|
118
|
+
},
|
|
119
|
+
},
|
|
87
120
|
],
|
|
88
121
|
},
|
|
89
122
|
{
|
|
@@ -109,16 +142,6 @@ export function registerNeuralwattSettings(
|
|
|
109
142
|
),
|
|
110
143
|
loaded.has("quotaWarnings"),
|
|
111
144
|
),
|
|
112
|
-
featureRow(
|
|
113
|
-
"subBarIntegration",
|
|
114
|
-
"Sub-bar integration",
|
|
115
|
-
"Toggle integration with the status bar and sub-core",
|
|
116
|
-
featureValue(
|
|
117
|
-
tabConfig?.subBarIntegration,
|
|
118
|
-
resolved.subBarIntegration.enabled,
|
|
119
|
-
),
|
|
120
|
-
loaded.has("subBarIntegration"),
|
|
121
|
-
),
|
|
122
145
|
],
|
|
123
146
|
},
|
|
124
147
|
];
|
|
@@ -154,20 +177,18 @@ export function registerNeuralwattSettings(
|
|
|
154
177
|
...config,
|
|
155
178
|
quotaWarnings: { ...config.quotaWarnings, enabled },
|
|
156
179
|
};
|
|
157
|
-
case "subBarIntegration":
|
|
158
|
-
return {
|
|
159
|
-
...config,
|
|
160
|
-
subBarIntegration: { ...config.subBarIntegration, enabled },
|
|
161
|
-
};
|
|
162
180
|
default:
|
|
163
181
|
return null;
|
|
164
182
|
}
|
|
165
183
|
},
|
|
166
184
|
onSave: async (ctx) => {
|
|
167
185
|
emitConfigUpdated(pi);
|
|
168
|
-
if (pendingApi === undefined)
|
|
186
|
+
if (pendingApi === undefined && pendingApiBaseUrl === undefined) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
169
189
|
pendingApi = undefined;
|
|
170
|
-
|
|
190
|
+
pendingApiBaseUrl = undefined;
|
|
191
|
+
ctx.ui.notify("Run /reload to apply the new provider settings", "info");
|
|
171
192
|
},
|
|
172
193
|
});
|
|
173
194
|
}
|