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
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A model reference: one string that names a model and, optionally, the
|
|
3
|
+
* provider that runs it.
|
|
4
|
+
*
|
|
5
|
+
* Written `<providerId>:<modelId>` — `ollama:qwen3:27b`,
|
|
6
|
+
* `openrouter:anthropic/claude-opus-5` — and split on the **first** colon,
|
|
7
|
+
* the convention the AI SDK's provider registry uses. The part before it
|
|
8
|
+
* qualifies the reference only when it is the id of a provider this build
|
|
9
|
+
* has; anything else is the whole string, a model of the provider in use,
|
|
10
|
+
* exactly as a bare id always was. So `qwen3:27b` with Ollama in use is still
|
|
11
|
+
* Ollama's `qwen3:27b`, and nothing stored before references existed changes
|
|
12
|
+
* meaning.
|
|
13
|
+
*
|
|
14
|
+
* One consequence, stated rather than hidden: a model of the provider in use
|
|
15
|
+
* whose own id begins with another provider's id and a colon cannot be reached
|
|
16
|
+
* by its bare id, because the prefix is read as that other provider. Written
|
|
17
|
+
* qualified — `<its own provider>:<id>` — it is reached, since only the first
|
|
18
|
+
* colon splits.
|
|
19
|
+
*
|
|
20
|
+
* Shared code, importing nothing, so the browser's pickers and the host's
|
|
21
|
+
* resolver read a reference the same way.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** A reference, split. `provider` is null when the reference names none. */
|
|
25
|
+
export interface ModelRef {
|
|
26
|
+
provider: string | null;
|
|
27
|
+
modelId: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Split a reference into the provider it names and the provider's own model
|
|
32
|
+
* id. `providerIds` are the ids of the providers this build has; a prefix that
|
|
33
|
+
* is not one of them, or an empty half on either side of the colon, leaves the
|
|
34
|
+
* reference unqualified and whole.
|
|
35
|
+
*/
|
|
36
|
+
export function parseModelRef(ref: string, providerIds: readonly string[]): ModelRef {
|
|
37
|
+
const colon = ref.indexOf(':');
|
|
38
|
+
if (colon <= 0 || colon === ref.length - 1) return { provider: null, modelId: ref };
|
|
39
|
+
const provider = ref.slice(0, colon);
|
|
40
|
+
if (!providerIds.includes(provider)) return { provider: null, modelId: ref };
|
|
41
|
+
return { provider, modelId: ref.slice(colon + 1) };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Write a reference that names its provider. `parseModelRef` reads it back. */
|
|
45
|
+
export function formatModelRef(provider: string, modelId: string): string {
|
|
46
|
+
return `${provider}:${modelId}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** What {@link findModel} found for a reference. */
|
|
50
|
+
export interface FoundModel<M> {
|
|
51
|
+
/** The provider the reference runs on: the one it names, else the one in use. */
|
|
52
|
+
provider: string | null;
|
|
53
|
+
/** The provider's own id for the model. */
|
|
54
|
+
modelId: string;
|
|
55
|
+
/** The listed model, or `null` when the list does not offer it. */
|
|
56
|
+
model: M | null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Find a stored reference in a model list.
|
|
61
|
+
*
|
|
62
|
+
* Qualified, it matches that provider's model; bare, it matches the provider
|
|
63
|
+
* in use — the one in use *now*, so a bare id stored while another provider
|
|
64
|
+
* was in use never matches that other provider's model by accident. Every
|
|
65
|
+
* place that shows a stored reference goes through here, so a picker, a tier
|
|
66
|
+
* row and a task row cannot disagree about what it names.
|
|
67
|
+
*
|
|
68
|
+
* `providerIds` are every provider in the build, so a reference to one that is
|
|
69
|
+
* off (and so absent from the list) still reads as naming it. Without them,
|
|
70
|
+
* the providers the list and the one in use name are all that is known.
|
|
71
|
+
*/
|
|
72
|
+
export function findModel<M extends { provider: string; modelId: string }>(
|
|
73
|
+
ref: string,
|
|
74
|
+
models: readonly M[],
|
|
75
|
+
activeProvider: string | null,
|
|
76
|
+
providerIds: readonly string[] = [],
|
|
77
|
+
): FoundModel<M> {
|
|
78
|
+
const known = new Set(providerIds);
|
|
79
|
+
for (const model of models) known.add(model.provider);
|
|
80
|
+
if (activeProvider !== null) known.add(activeProvider);
|
|
81
|
+
const parsed = parseModelRef(ref, [...known]);
|
|
82
|
+
const provider = parsed.provider ?? activeProvider;
|
|
83
|
+
const model = models.find((entry) => entry.provider === provider && entry.modelId === parsed.modelId) ?? null;
|
|
84
|
+
return { provider, modelId: parsed.modelId, model };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Where a provider runs, as the browser learns it from `ai.providersList`. */
|
|
88
|
+
export interface ProviderPlace {
|
|
89
|
+
id: string;
|
|
90
|
+
label: string;
|
|
91
|
+
local: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The words that say where a provider runs: `on this computer`, or
|
|
96
|
+
* `sent to <label>`. Words, not a colour or an icon alone, wherever a model is
|
|
97
|
+
* chosen or named.
|
|
98
|
+
*/
|
|
99
|
+
export function whereItRuns(provider: Pick<ProviderPlace, 'label' | 'local'>): string {
|
|
100
|
+
return provider.local ? 'on this computer' : `sent to ${provider.label}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** A stored reference, described for a person. */
|
|
104
|
+
export interface ModelDescription {
|
|
105
|
+
/** The model's name when the list has it, else the reference's own model id. */
|
|
106
|
+
name: string;
|
|
107
|
+
/** `on this computer` or `sent to <label>`, or `null` when the provider is not known. */
|
|
108
|
+
where: string | null;
|
|
109
|
+
/** Said after the name when it cannot run: `not offered`, or `<label> is off`. */
|
|
110
|
+
problem: string | null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Describe a stored reference: its name, where it runs, and what is wrong with
|
|
115
|
+
* it, if anything. `enabled` lists the providers turned on in Settings.
|
|
116
|
+
*/
|
|
117
|
+
export function describeModel<M extends { provider: string; modelId: string; label: string }>(
|
|
118
|
+
ref: string,
|
|
119
|
+
context: {
|
|
120
|
+
readonly models: readonly M[];
|
|
121
|
+
readonly providers: readonly ProviderPlace[];
|
|
122
|
+
readonly enabled: readonly string[];
|
|
123
|
+
readonly activeProvider: string | null;
|
|
124
|
+
},
|
|
125
|
+
): ModelDescription {
|
|
126
|
+
const found = findModel(
|
|
127
|
+
ref,
|
|
128
|
+
context.models,
|
|
129
|
+
context.activeProvider,
|
|
130
|
+
context.providers.map((provider) => provider.id),
|
|
131
|
+
);
|
|
132
|
+
const place = context.providers.find((provider) => provider.id === found.provider) ?? null;
|
|
133
|
+
const name = found.model?.label ?? found.modelId;
|
|
134
|
+
const where = place === null ? null : whereItRuns(place);
|
|
135
|
+
if (place !== null && !context.enabled.includes(place.id)) {
|
|
136
|
+
return { name, where, problem: `${place.label} is off` };
|
|
137
|
+
}
|
|
138
|
+
return { name, where, problem: found.model === null ? 'not offered' : null };
|
|
139
|
+
}
|
|
@@ -18,8 +18,10 @@ import type {
|
|
|
18
18
|
ChatEvent,
|
|
19
19
|
ChatFile,
|
|
20
20
|
ProviderInfo,
|
|
21
|
+
ProviderSettings,
|
|
21
22
|
StoredMessage,
|
|
22
23
|
Thread,
|
|
24
|
+
UnavailableProvider,
|
|
23
25
|
} from './types.ts';
|
|
24
26
|
|
|
25
27
|
type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2
|
|
@@ -29,6 +31,12 @@ type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ?
|
|
|
29
31
|
const settingsMatch: Equal<OperationOutput<AiContract, 'ai.settingsGet'>, AiSettings> = true;
|
|
30
32
|
void settingsMatch;
|
|
31
33
|
|
|
34
|
+
const providerSettingsMatch: Equal<
|
|
35
|
+
OperationOutput<AiContract, 'ai.settingsGet'>['providers'][number],
|
|
36
|
+
ProviderSettings
|
|
37
|
+
> = true;
|
|
38
|
+
void providerSettingsMatch;
|
|
39
|
+
|
|
32
40
|
const settingsUpdateReturnsSettings: Equal<
|
|
33
41
|
OperationOutput<AiContract, 'ai.settingsUpdate'>,
|
|
34
42
|
AiSettings
|
|
@@ -41,6 +49,12 @@ const modelMatch: Equal<
|
|
|
41
49
|
> = true;
|
|
42
50
|
void modelMatch;
|
|
43
51
|
|
|
52
|
+
const unavailableMatch: Equal<
|
|
53
|
+
OperationOutput<AiContract, 'ai.modelsList'>['unavailable'][number],
|
|
54
|
+
UnavailableProvider
|
|
55
|
+
> = true;
|
|
56
|
+
void unavailableMatch;
|
|
57
|
+
|
|
44
58
|
const providerMatch: Equal<
|
|
45
59
|
OperationOutput<AiContract, 'ai.providersList'>['providers'][number],
|
|
46
60
|
ProviderInfo
|
package/src/ai/shared/types.ts
CHANGED
|
@@ -25,6 +25,15 @@ export interface BroappModel {
|
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* A provider whose models are missing from `ai.modelsList`, or cut short, and
|
|
30
|
+
* why, in a sentence a person can read.
|
|
31
|
+
*/
|
|
32
|
+
export interface UnavailableProvider {
|
|
33
|
+
provider: string;
|
|
34
|
+
message: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
28
37
|
/** A provider compiled into this application, as the browser sees it. */
|
|
29
38
|
export interface ProviderInfo {
|
|
30
39
|
id: string;
|
|
@@ -38,7 +47,33 @@ export interface ProviderInfo {
|
|
|
38
47
|
defaultBaseUrl: string | null;
|
|
39
48
|
}
|
|
40
49
|
|
|
41
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* One provider's own settings, whether or not it is the one in use. Never
|
|
52
|
+
* contains the key itself.
|
|
53
|
+
*/
|
|
54
|
+
export interface ProviderSettings {
|
|
55
|
+
id: string;
|
|
56
|
+
baseUrl: string | null;
|
|
57
|
+
modelId: string | null;
|
|
58
|
+
/** Whether it may be sent anything. The provider in use always may. */
|
|
59
|
+
enabled: boolean;
|
|
60
|
+
hasKey: boolean;
|
|
61
|
+
keyHint: string | null;
|
|
62
|
+
/**
|
|
63
|
+
* True when its key and address are what it needs: a model reference naming
|
|
64
|
+
* it would run, given a model, once it is enabled. Neither the model nor
|
|
65
|
+
* `enabled` is part of this; each has its own field.
|
|
66
|
+
*/
|
|
67
|
+
configured: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* What the settings route returns. Never contains a key itself.
|
|
72
|
+
*
|
|
73
|
+
* The top-level fields are the provider in use — the one a turn runs on when
|
|
74
|
+
* nothing names another — so an application written against one provider
|
|
75
|
+
* reads what it always read.
|
|
76
|
+
*/
|
|
42
77
|
export interface AiSettings {
|
|
43
78
|
provider: string | null;
|
|
44
79
|
modelId: string | null;
|
|
@@ -46,10 +81,12 @@ export interface AiSettings {
|
|
|
46
81
|
hasKey: boolean;
|
|
47
82
|
/** Last four characters of the key, for the UI to show which key is set. */
|
|
48
83
|
keyHint: string | null;
|
|
49
|
-
/** False means
|
|
84
|
+
/** False means every key is held in memory only and forgotten on exit. */
|
|
50
85
|
remember: boolean;
|
|
51
86
|
/** True when provider and model are both set and the provider's needs are met. */
|
|
52
87
|
configured: boolean;
|
|
88
|
+
/** Every provider in this build, in the build's order. */
|
|
89
|
+
providers: ProviderSettings[];
|
|
53
90
|
}
|
|
54
91
|
|
|
55
92
|
/** How much ceremony a tool call needs before it runs. */
|
|
@@ -86,10 +123,13 @@ export interface ChatFile {
|
|
|
86
123
|
/**
|
|
87
124
|
* A stored conversation, without its messages.
|
|
88
125
|
*
|
|
89
|
-
* `modelId` is null for a conversation that follows Settings, and a model
|
|
90
|
-
* for one that has been pinned to a model of its own
|
|
91
|
-
*
|
|
92
|
-
*
|
|
126
|
+
* `modelId` is null for a conversation that follows Settings, and a model
|
|
127
|
+
* reference for one that has been pinned to a model of its own: bare, a model
|
|
128
|
+
* of the provider in use; `<provider>:<model>`, a model of any provider the
|
|
129
|
+
* person turned on (see `model-ref.ts`). It once could not name a provider,
|
|
130
|
+
* because changing provider changes which key is used and whether anything
|
|
131
|
+
* leaves the computer. That reason stands, and is why wherever a model is
|
|
132
|
+
* chosen the person must be told whether it runs on this computer.
|
|
93
133
|
*/
|
|
94
134
|
export interface Thread {
|
|
95
135
|
id: string;
|