broapp 0.4.5 → 0.4.7
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/ai/host/create-ai.ts +151 -15
- package/src/ai/host/registry.ts +167 -57
- package/src/ai/host/settings.ts +69 -20
- package/src/ai/react/AiSettings.tsx +452 -157
- package/src/ai/react/ai.css +386 -10
- package/src/ai/react/use-ai-models.ts +24 -13
- package/src/ai/react/use-ai-settings.ts +18 -11
- package/src/ai/shared/contract.ts +39 -7
- package/src/ai/shared/index.ts +4 -0
- package/src/ai/shared/model-ref.ts +139 -0
- package/src/ai/shared/types.check.ts +14 -0
- package/src/ai/shared/types.ts +46 -6
package/src/ai/host/settings.ts
CHANGED
|
@@ -1,23 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Where the AI layer's non-secret settings live.
|
|
3
3
|
*
|
|
4
|
-
* `<dataDir>/ai/settings.json` holds which provider
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* stops being true.
|
|
4
|
+
* `<dataDir>/ai/settings.json` holds which provider is in use, and for every
|
|
5
|
+
* provider the person has set up, where to reach it, which model it runs and
|
|
6
|
+
* whether it is turned on. It never holds an API key — that is `secrets.ts` —
|
|
7
|
+
* and a test asserts the string does not appear in the file, because "we do
|
|
8
|
+
* not write it there" is the kind of promise that quietly stops being true.
|
|
9
9
|
*/
|
|
10
10
|
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
|
|
13
|
-
/**
|
|
14
|
-
export interface
|
|
15
|
-
version: 1;
|
|
16
|
-
provider: string | null;
|
|
17
|
-
modelId: string | null;
|
|
13
|
+
/** One provider's own settings, kept whether or not it is the one in use. */
|
|
14
|
+
export interface ProviderEntry {
|
|
18
15
|
baseUrl: string | null;
|
|
19
|
-
|
|
16
|
+
modelId: string | null;
|
|
17
|
+
/**
|
|
18
|
+
* Whether this provider may be sent anything. The provider in use always
|
|
19
|
+
* is; any other only when a person turned it on, so a model reference in a
|
|
20
|
+
* file cannot send their work somewhere they never chose.
|
|
21
|
+
*/
|
|
22
|
+
enabled: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The settings file's shape.
|
|
27
|
+
*
|
|
28
|
+
* Version 1 held one `provider`, `modelId` and `baseUrl`, so changing provider
|
|
29
|
+
* lost the other's address and model. Version 2 keeps an entry per provider;
|
|
30
|
+
* a version-1 file is read as version 2 and left as it is until the next write.
|
|
31
|
+
*/
|
|
32
|
+
export interface StoredSettings {
|
|
33
|
+
version: 2;
|
|
34
|
+
/** The provider a turn runs on when nothing names another. */
|
|
35
|
+
active: string | null;
|
|
36
|
+
/** False means every key is held in memory only and forgotten on exit. */
|
|
20
37
|
remember: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Keyed by provider id. An entry for an id this build has no adapter for is
|
|
40
|
+
* kept on write and ignored on read: another build may own it.
|
|
41
|
+
*/
|
|
42
|
+
providers: Record<string, ProviderEntry>;
|
|
21
43
|
}
|
|
22
44
|
|
|
23
45
|
/** Reads and writes {@link StoredSettings}. */
|
|
@@ -28,22 +50,49 @@ export interface SettingsStore {
|
|
|
28
50
|
|
|
29
51
|
/** What a fresh installation has. No provider, so the layer is off. */
|
|
30
52
|
export function defaultSettings(): StoredSettings {
|
|
31
|
-
return { version:
|
|
53
|
+
return { version: 2, active: null, remember: true, providers: {} };
|
|
32
54
|
}
|
|
33
55
|
|
|
34
|
-
function
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
56
|
+
function text(raw: Record<string, unknown>, key: string): string | null {
|
|
57
|
+
const value = raw[key];
|
|
58
|
+
return typeof value === 'string' ? value : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A version-1 file, as version 2: the one provider it named, turned on. */
|
|
62
|
+
function fromVersion1(raw: Record<string, unknown>): StoredSettings {
|
|
63
|
+
const active = text(raw, 'provider');
|
|
38
64
|
return {
|
|
39
|
-
version:
|
|
40
|
-
|
|
41
|
-
modelId: text('modelId'),
|
|
42
|
-
baseUrl: text('baseUrl'),
|
|
65
|
+
version: 2,
|
|
66
|
+
active,
|
|
43
67
|
remember: raw['remember'] !== false,
|
|
68
|
+
providers:
|
|
69
|
+
active === null
|
|
70
|
+
? {}
|
|
71
|
+
: { [active]: { baseUrl: text(raw, 'baseUrl'), modelId: text(raw, 'modelId'), enabled: true } },
|
|
44
72
|
};
|
|
45
73
|
}
|
|
46
74
|
|
|
75
|
+
function coerce(value: unknown): StoredSettings | null {
|
|
76
|
+
if (typeof value !== 'object' || value === null) return null;
|
|
77
|
+
const raw = value as Record<string, unknown>;
|
|
78
|
+
// A file written before `version` existed, or by version 1, has the old shape.
|
|
79
|
+
if (raw['version'] === undefined || raw['version'] === 1) return fromVersion1(raw);
|
|
80
|
+
if (raw['version'] !== 2) return null;
|
|
81
|
+
const listed = raw['providers'];
|
|
82
|
+
if (typeof listed !== 'object' || listed === null || Array.isArray(listed)) return null;
|
|
83
|
+
const providers: Record<string, ProviderEntry> = {};
|
|
84
|
+
for (const [id, entry] of Object.entries(listed as Record<string, unknown>)) {
|
|
85
|
+
if (typeof entry !== 'object' || entry === null) continue;
|
|
86
|
+
const fields = entry as Record<string, unknown>;
|
|
87
|
+
providers[id] = {
|
|
88
|
+
baseUrl: text(fields, 'baseUrl'),
|
|
89
|
+
modelId: text(fields, 'modelId'),
|
|
90
|
+
enabled: fields['enabled'] === true,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return { version: 2, active: text(raw, 'active'), remember: raw['remember'] !== false, providers };
|
|
94
|
+
}
|
|
95
|
+
|
|
47
96
|
/** Open the settings store for one data directory. */
|
|
48
97
|
export function createSettingsStore(dataDir: string): SettingsStore {
|
|
49
98
|
const directory = join(dataDir, 'ai');
|