@agent-native/core 0.185.0 → 0.186.0-nightly-20260923174435
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/corpus/README.md +1 -1
- package/corpus/templates/calendar/actions/delete-event.ts +6 -4
- package/corpus/templates/calendar/actions/delete-events.ts +10 -11
- package/corpus/templates/calendar/actions/event-action-helpers.ts +53 -0
- package/corpus/templates/calendar/actions/get-event.ts +59 -14
- package/corpus/templates/calendar/actions/rsvp-event.ts +5 -3
- package/corpus/templates/calendar/actions/update-event.ts +15 -7
- package/corpus/templates/calendar/actions/update-events.ts +29 -11
- package/corpus/templates/calendar/changelog/2026-09-21-events-stay-editable-when-their-calendar-is-connected-throug.md +6 -0
- package/corpus/templates/calendar/server/lib/google-calendar.ts +70 -18
- package/corpus/templates/calendar/shared/google-calendar-sources.ts +47 -0
- package/corpus/templates/clips/actions/finalize-recording.ts +35 -1
- package/corpus/templates/clips/app/components/player/delete-recording-menu.tsx +5 -1
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +18 -1
- package/corpus/templates/clips/app/routes/_app.r.$recordingId.tsx +2 -2
- package/corpus/templates/clips/app/routes/record.tsx +1 -0
- package/corpus/templates/clips/changelog/2026-09-23-long-recording-menus-scroll-within-the-visible-space-so-ever.md +6 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +14 -4
- package/corpus/templates/content/app/blocks/contentBlockRegistry.tsx +9 -1
- package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +16 -2
- package/corpus/templates/content/app/components/editor/registrySlashItems.ts +61 -40
- package/corpus/templates/content/app/i18n-data.ts +12 -0
- package/corpus/templates/content/app/routes/_app.settings.tsx +36 -9
- package/corpus/templates/content/docs/product/capabilities/content.renderer.custom-block.md +1 -1
- package/corpus/templates/content/server/plugins/agent-chat.ts +1 -0
- package/corpus/templates/content/shared/labs.ts +35 -1
- package/corpus/templates/slides/actions/get-design-system.ts +4 -2
- package/dist/agent/engine/openai-compatible-endpoint.d.ts +7 -0
- package/dist/agent/engine/openai-compatible-endpoint.js +9 -0
- package/dist/agent/engine/provider-endpoint-validation.d.ts +8 -1
- package/dist/agent/engine/provider-endpoint-validation.js +47 -10
- package/dist/agent/engine/registry.js +8 -5
- package/dist/automation/index.d.ts +1 -1
- package/dist/cli/skills.d.ts +3 -0
- package/dist/cli/skills.js +45 -9
- package/dist/client/agent-engine-key.d.ts +7 -0
- package/dist/client/agent-engine-key.js +37 -0
- package/dist/client/agent-provider-catalog.js +1 -1
- package/dist/client/settings/ProviderSetupForm.js +80 -7
- package/dist/client/settings/SettingsPanel.js +149 -11
- package/dist/client/use-chat-models.js +31 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/labs/actions/set-lab.js +1 -0
- package/dist/labs/registry.js +6 -1
- package/dist/localization/default-messages.d.ts +7 -1
- package/dist/localization/default-messages.js +7 -1
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/org/index.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +6 -6
- package/dist/provider-api/actions/provider-api.d.ts +3 -3
- package/dist/scripts/agent-engines/test-agent-engine.js +9 -7
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.js +8 -5
- package/dist/server/agent-engine-ollama-models-route.d.ts +15 -0
- package/dist/server/agent-engine-ollama-models-route.js +105 -0
- package/dist/server/core-routes-plugin.js +5 -0
- package/dist/server/credential-provider.d.ts +13 -0
- package/dist/server/credential-provider.js +19 -0
- package/package.json +3 -3
|
@@ -4,3 +4,10 @@ export declare const OLLAMA_BASE_URL_ENV_VAR = "OLLAMA_BASE_URL";
|
|
|
4
4
|
export declare function isCustomOpenAiBaseUrl(value: string | undefined): boolean;
|
|
5
5
|
export declare function normalizeProviderBaseUrl(value: string): string;
|
|
6
6
|
export declare function normalizeOpenAiBaseUrl(value: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Ollama's own API has no `/v1` path — that suffix only applies to its
|
|
9
|
+
* OpenAI-compatibility shim, which this integration doesn't use. Docs for
|
|
10
|
+
* other tools commonly show `/v1`, so users copy-paste it in; strip it
|
|
11
|
+
* silently rather than making them notice and remove it themselves.
|
|
12
|
+
*/
|
|
13
|
+
export declare function stripOllamaV1Suffix(value: string): string;
|
|
@@ -28,3 +28,12 @@ export function normalizeProviderBaseUrl(value) {
|
|
|
28
28
|
export function normalizeOpenAiBaseUrl(value) {
|
|
29
29
|
return normalizeProviderBaseUrl(value);
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Ollama's own API has no `/v1` path — that suffix only applies to its
|
|
33
|
+
* OpenAI-compatibility shim, which this integration doesn't use. Docs for
|
|
34
|
+
* other tools commonly show `/v1`, so users copy-paste it in; strip it
|
|
35
|
+
* silently rather than making them notice and remove it themselves.
|
|
36
|
+
*/
|
|
37
|
+
export function stripOllamaV1Suffix(value) {
|
|
38
|
+
return value.replace(/\/v1$/i, "");
|
|
39
|
+
}
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Validate a provider endpoint before a server-side model request can use it.
|
|
3
3
|
* `allowPrivate` is reserved for operator-owned deployment configuration; it
|
|
4
|
-
* must never be enabled for a user- or agent-supplied URL.
|
|
4
|
+
* must never be enabled for a user- or agent-supplied URL. `allowLocalOllama`
|
|
5
|
+
* additionally requires the caller to have already established this is a
|
|
6
|
+
* trusted, self-hosted context (see `isTrustedSelfHostedRuntime`) — it is
|
|
7
|
+
* never safe to grant purely from the endpoint value itself. `isOllama` says
|
|
8
|
+
* this endpoint is for Ollama specifically (independent of trust level), so a
|
|
9
|
+
* copy-pasted trailing `/v1` — meaningless for Ollama's own API — is silently
|
|
10
|
+
* removed instead of left to confuse the address that gets saved and used.
|
|
5
11
|
*/
|
|
6
12
|
export declare function validateProviderBaseUrl(value: string, options?: {
|
|
7
13
|
allowPrivate?: boolean;
|
|
8
14
|
allowLocalOllama?: boolean;
|
|
15
|
+
isOllama?: boolean;
|
|
9
16
|
}): Promise<string>;
|
|
@@ -1,20 +1,57 @@
|
|
|
1
1
|
import { isBlockedExtensionUrlWithDns } from "../../extensions/url-safety.js";
|
|
2
|
-
import { normalizeProviderBaseUrl } from "./openai-compatible-endpoint.js";
|
|
3
|
-
function
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
import { normalizeProviderBaseUrl, stripOllamaV1Suffix, } from "./openai-compatible-endpoint.js";
|
|
3
|
+
function isPrivateIpv4Lan(a, b, c, d) {
|
|
4
|
+
if (![a, b, c, d].every((part) => part >= 0 && part <= 255))
|
|
5
|
+
return false;
|
|
6
|
+
if (a === 127)
|
|
7
|
+
return true; // loopback
|
|
8
|
+
if (a === 10)
|
|
9
|
+
return true; // 10.0.0.0/8
|
|
10
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
11
|
+
return true; // 172.16.0.0/12
|
|
12
|
+
if (a === 192 && b === 168)
|
|
13
|
+
return true; // 192.168.0.0/16
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* True for hosts an Ollama server commonly runs on: loopback, or an RFC1918
|
|
18
|
+
* LAN address such as `192.168.1.123`. Deliberately narrower than the
|
|
19
|
+
* generic SSRF private-address check in `url-safety.ts` — link-local
|
|
20
|
+
* addresses (169.254.x.x, which also covers the cloud metadata endpoint) and
|
|
21
|
+
* other reserved ranges stay blocked even for Ollama. Only literal IPs and
|
|
22
|
+
* `localhost` are recognized; an arbitrary hostname that merely resolves to
|
|
23
|
+
* a LAN address is not, so this never depends on a DNS lookup.
|
|
24
|
+
*/
|
|
25
|
+
function isLocalNetworkOllamaEndpoint(value) {
|
|
26
|
+
const hostname = new URL(value).hostname
|
|
27
|
+
.toLowerCase()
|
|
28
|
+
.replace(/^\[|\]$/g, "");
|
|
29
|
+
if (hostname === "localhost" || hostname === "::1")
|
|
30
|
+
return true;
|
|
31
|
+
const parts = hostname.split(".");
|
|
32
|
+
if (parts.length === 4 && parts.every((p) => /^\d+$/.test(p))) {
|
|
33
|
+
const [a, b, c, d] = parts.map(Number);
|
|
34
|
+
return isPrivateIpv4Lan(a, b, c, d);
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
9
37
|
}
|
|
10
38
|
/**
|
|
11
39
|
* Validate a provider endpoint before a server-side model request can use it.
|
|
12
40
|
* `allowPrivate` is reserved for operator-owned deployment configuration; it
|
|
13
|
-
* must never be enabled for a user- or agent-supplied URL.
|
|
41
|
+
* must never be enabled for a user- or agent-supplied URL. `allowLocalOllama`
|
|
42
|
+
* additionally requires the caller to have already established this is a
|
|
43
|
+
* trusted, self-hosted context (see `isTrustedSelfHostedRuntime`) — it is
|
|
44
|
+
* never safe to grant purely from the endpoint value itself. `isOllama` says
|
|
45
|
+
* this endpoint is for Ollama specifically (independent of trust level), so a
|
|
46
|
+
* copy-pasted trailing `/v1` — meaningless for Ollama's own API — is silently
|
|
47
|
+
* removed instead of left to confuse the address that gets saved and used.
|
|
14
48
|
*/
|
|
15
49
|
export async function validateProviderBaseUrl(value, options = {}) {
|
|
16
|
-
|
|
17
|
-
|
|
50
|
+
let normalized = normalizeProviderBaseUrl(value);
|
|
51
|
+
if (options.isOllama)
|
|
52
|
+
normalized = stripOllamaV1Suffix(normalized);
|
|
53
|
+
const allowLocalOllama = options.allowLocalOllama === true &&
|
|
54
|
+
isLocalNetworkOllamaEndpoint(normalized);
|
|
18
55
|
if (!options.allowPrivate &&
|
|
19
56
|
!allowLocalOllama &&
|
|
20
57
|
(await isBlockedExtensionUrlWithDns(normalized))) {
|
|
@@ -12,7 +12,7 @@ import { getAppConfig } from "../../app-config/index.js";
|
|
|
12
12
|
import { getUserLabs } from "../../labs/store.js";
|
|
13
13
|
import { BUILDER_OAUTH_SCOPE, hasBuilderOAuthSession, resolveBuilderOAuthRequestAccess, } from "../../server/builder-oauth.js";
|
|
14
14
|
import { hasChatGPTSubscriptionCredential } from "../../server/chatgpt-subscription-oauth.js";
|
|
15
|
-
import { assertCredentialStoreReadable, canUseDeployCredentialFallbackForRequest, getBuilderCredentialAuthFailure, getProviderCredentialAuthFailure, prefetchSecrets, readDeployCredentialEnv, resolveBuilderCredentialsDetailed, resolveBuilderGatewayCredentialsDetailed, resolveSecret, } from "../../server/credential-provider.js";
|
|
15
|
+
import { assertCredentialStoreReadable, canUseDeployCredentialFallbackForRequest, getBuilderCredentialAuthFailure, getProviderCredentialAuthFailure, isTrustedSelfHostedRuntime, prefetchSecrets, readDeployCredentialEnv, resolveBuilderCredentialsDetailed, resolveBuilderGatewayCredentialsDetailed, resolveSecret, } from "../../server/credential-provider.js";
|
|
16
16
|
import { getRequestOrgId, getRequestContext, getRequestUserEmail, } from "../../server/request-context.js";
|
|
17
17
|
import { getSetting } from "../../settings/store.js";
|
|
18
18
|
import { getAgentAppModelDefaultForCurrentRequest } from "../app-model-defaults.js";
|
|
@@ -625,11 +625,13 @@ async function resolveProviderBaseUrl(envVar) {
|
|
|
625
625
|
const deployValue = canUseDeployCredentialFallbackForRequest(envVar)
|
|
626
626
|
? readDeployCredentialEnv(envVar)
|
|
627
627
|
: undefined;
|
|
628
|
+
const isOllama = envVar === OLLAMA_BASE_URL_ENV_VAR;
|
|
628
629
|
if (!raw) {
|
|
629
630
|
if (!deployValue)
|
|
630
631
|
return undefined;
|
|
631
632
|
return validateProviderBaseUrl(deployValue, {
|
|
632
633
|
allowPrivate: true,
|
|
634
|
+
isOllama,
|
|
633
635
|
});
|
|
634
636
|
}
|
|
635
637
|
return raw
|
|
@@ -639,8 +641,8 @@ async function resolveProviderBaseUrl(envVar) {
|
|
|
639
641
|
// allowance as the explicit deploy-only branch above without extending
|
|
640
642
|
// it to user-, org-, or workspace-scoped endpoint values.
|
|
641
643
|
allowPrivate: deployValue !== undefined && raw === deployValue,
|
|
642
|
-
allowLocalOllama:
|
|
643
|
-
|
|
644
|
+
allowLocalOllama: isOllama && isTrustedSelfHostedRuntime(),
|
|
645
|
+
isOllama,
|
|
644
646
|
})
|
|
645
647
|
: undefined;
|
|
646
648
|
}
|
|
@@ -815,9 +817,10 @@ async function engineCreateConfigForEntry(entry, apiKey, extra, credentialResolu
|
|
|
815
817
|
}
|
|
816
818
|
if (entry.name === "ai-sdk:openai" || entry.name === "ai-sdk:ollama") {
|
|
817
819
|
if (typeof safeExtra.baseURL === "string" && safeExtra.baseUrl == null) {
|
|
820
|
+
const isOllama = entry.name === "ai-sdk:ollama";
|
|
818
821
|
safeExtra.baseUrl = await validateProviderBaseUrl(safeExtra.baseURL, {
|
|
819
|
-
allowLocalOllama:
|
|
820
|
-
|
|
822
|
+
allowLocalOllama: isOllama && isTrustedSelfHostedRuntime(),
|
|
823
|
+
isOllama,
|
|
821
824
|
});
|
|
822
825
|
}
|
|
823
826
|
if (safeExtra.baseUrl == null) {
|
|
@@ -186,8 +186,8 @@ export declare function createAutomationCallbackHandler(runtime: AutomationRunti
|
|
|
186
186
|
duplicate?: undefined;
|
|
187
187
|
eventId?: undefined;
|
|
188
188
|
} | {
|
|
189
|
-
error?: undefined;
|
|
190
189
|
accepted: boolean;
|
|
191
190
|
duplicate: boolean;
|
|
192
191
|
eventId: string;
|
|
192
|
+
error?: undefined;
|
|
193
193
|
}>;
|
package/dist/cli/skills.d.ts
CHANGED
|
@@ -189,6 +189,7 @@ export type SkillsCatalogMode = "agent-native" | "all";
|
|
|
189
189
|
export interface PublicSkillCatalogEntry {
|
|
190
190
|
name: string;
|
|
191
191
|
description?: string;
|
|
192
|
+
installerGroup?: string;
|
|
192
193
|
}
|
|
193
194
|
interface ConnectSpinner {
|
|
194
195
|
start(message?: string): void;
|
|
@@ -263,7 +264,9 @@ interface SkillsClientPromptContext {
|
|
|
263
264
|
installsMcp: boolean;
|
|
264
265
|
}
|
|
265
266
|
interface SkillsTargetPromptContext {
|
|
267
|
+
message?: string;
|
|
266
268
|
initialTargets: string[];
|
|
269
|
+
required?: boolean;
|
|
267
270
|
options: Array<{
|
|
268
271
|
value: string;
|
|
269
272
|
label: string;
|
package/dist/cli/skills.js
CHANGED
|
@@ -1808,6 +1808,7 @@ function publicSkillEntries(options) {
|
|
|
1808
1808
|
.map((entry) => ({
|
|
1809
1809
|
name: entry.name.trim().toLowerCase(),
|
|
1810
1810
|
description: entry.description,
|
|
1811
|
+
installerGroup: entry.installerGroup?.trim().toLowerCase(),
|
|
1811
1812
|
}))
|
|
1812
1813
|
.filter((entry) => {
|
|
1813
1814
|
if (!entry.name || isKnownSkill(entry.name) || seen.has(entry.name)) {
|
|
@@ -1822,11 +1823,16 @@ function publicSkillNames(options) {
|
|
|
1822
1823
|
return new Set(publicSkillEntries(options).map((entry) => entry.name));
|
|
1823
1824
|
}
|
|
1824
1825
|
function publicSkillPromptOptions(options) {
|
|
1825
|
-
return publicSkillEntries(options).map((entry) =>
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1826
|
+
return publicSkillEntries(options).map((entry) => {
|
|
1827
|
+
const description = entry.description ?? "Public skill from the BuilderIO skills catalog.";
|
|
1828
|
+
return {
|
|
1829
|
+
value: entry.name,
|
|
1830
|
+
label: entry.name,
|
|
1831
|
+
hint: entry.installerGroup === entry.name
|
|
1832
|
+
? `${description} Select to choose the full group or a subset.`
|
|
1833
|
+
: description,
|
|
1834
|
+
};
|
|
1835
|
+
});
|
|
1830
1836
|
}
|
|
1831
1837
|
function skillPromptOptions(options = {}) {
|
|
1832
1838
|
return [
|
|
@@ -1999,11 +2005,12 @@ async function promptForUpdateInstructions() {
|
|
|
1999
2005
|
async function promptForSkills(context) {
|
|
2000
2006
|
const clack = await import("@clack/prompts");
|
|
2001
2007
|
const result = await clack.multiselect({
|
|
2002
|
-
message:
|
|
2003
|
-
"
|
|
2008
|
+
message: context.message ??
|
|
2009
|
+
"Which Agent-Native skills do you want to install?\n" +
|
|
2010
|
+
" (space toggles, enter confirms)",
|
|
2004
2011
|
options: context.options,
|
|
2005
2012
|
initialValues: context.initialTargets,
|
|
2006
|
-
required: true,
|
|
2013
|
+
required: context.required ?? true,
|
|
2007
2014
|
});
|
|
2008
2015
|
if (clack.isCancel(result)) {
|
|
2009
2016
|
clack.cancel("Cancelled.");
|
|
@@ -2149,7 +2156,36 @@ async function resolveSkillTargets(parsed, options) {
|
|
|
2149
2156
|
});
|
|
2150
2157
|
if (!selected || selected.length === 0)
|
|
2151
2158
|
return null;
|
|
2152
|
-
|
|
2159
|
+
const publicEntries = publicSkillEntries(options);
|
|
2160
|
+
const selectedTargets = new Set(selected);
|
|
2161
|
+
for (const group of publicEntries.filter((entry) => entry.installerGroup === entry.name && selected.includes(entry.name))) {
|
|
2162
|
+
const members = publicEntries.filter((entry) => entry.installerGroup === group.name && entry.name !== group.name);
|
|
2163
|
+
if (members.length === 0)
|
|
2164
|
+
continue;
|
|
2165
|
+
const groupLabel = group.name.replace(/^./, (character) => character.toUpperCase());
|
|
2166
|
+
const groupSelection = await prompt({
|
|
2167
|
+
message: `Which ${groupLabel} skills do you want to install?\n` +
|
|
2168
|
+
" (all selected; deselect any you want to skip)",
|
|
2169
|
+
initialTargets: members.map((entry) => entry.name),
|
|
2170
|
+
required: false,
|
|
2171
|
+
options: members.map((entry) => ({
|
|
2172
|
+
value: entry.name,
|
|
2173
|
+
label: entry.name,
|
|
2174
|
+
hint: entry.description ??
|
|
2175
|
+
"Public skill from the BuilderIO skills catalog.",
|
|
2176
|
+
})),
|
|
2177
|
+
});
|
|
2178
|
+
if (!groupSelection)
|
|
2179
|
+
return null;
|
|
2180
|
+
const memberNames = new Set(members.map((entry) => entry.name));
|
|
2181
|
+
for (const memberName of memberNames)
|
|
2182
|
+
selectedTargets.delete(memberName);
|
|
2183
|
+
for (const memberName of groupSelection) {
|
|
2184
|
+
if (memberNames.has(memberName))
|
|
2185
|
+
selectedTargets.add(memberName);
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
return resolveSelectedSkillTargets([...selectedTargets], options);
|
|
2153
2189
|
}
|
|
2154
2190
|
export function parseSkillsArgs(argv) {
|
|
2155
2191
|
const first = argv[0];
|
|
@@ -53,6 +53,13 @@ export declare function saveAgentEngineApiKey({ provider, key, apiKey, scope, }:
|
|
|
53
53
|
* endpoint URLs. Values are stored server-side in scoped secrets.
|
|
54
54
|
*/
|
|
55
55
|
export declare function saveAgentEngineProviderSettings({ provider, key, apiKey, baseUrl, clearBaseUrl, }: SaveAgentEngineProviderSettingsOptions): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* List the models an Ollama server actually has installed, via its native
|
|
58
|
+
* `/api/tags` endpoint. Pass `baseUrl` to check a server before it's saved
|
|
59
|
+
* (e.g. while the user is still typing the endpoint); omit it to use the
|
|
60
|
+
* saved endpoint. Throws a readable Error if the server can't be reached.
|
|
61
|
+
*/
|
|
62
|
+
export declare function fetchOllamaModels(baseUrl?: string): Promise<string[]>;
|
|
56
63
|
/**
|
|
57
64
|
* Select the provider and model for the next conversation. This is separate
|
|
58
65
|
* from saving credentials so keyless local providers such as Ollama can use
|
|
@@ -201,6 +201,43 @@ export async function saveAgentEngineProviderSettings({ provider, key, apiKey, b
|
|
|
201
201
|
}
|
|
202
202
|
dispatchConfiguredChanged();
|
|
203
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* List the models an Ollama server actually has installed, via its native
|
|
206
|
+
* `/api/tags` endpoint. Pass `baseUrl` to check a server before it's saved
|
|
207
|
+
* (e.g. while the user is still typing the endpoint); omit it to use the
|
|
208
|
+
* saved endpoint. Throws a readable Error if the server can't be reached.
|
|
209
|
+
*/
|
|
210
|
+
export async function fetchOllamaModels(baseUrl) {
|
|
211
|
+
const trimmed = baseUrl?.trim() ?? "";
|
|
212
|
+
const path = trimmed
|
|
213
|
+
? `/_agent-native/agent-engine/ollama-models?baseUrl=${encodeURIComponent(trimmed)}`
|
|
214
|
+
: "/_agent-native/agent-engine/ollama-models";
|
|
215
|
+
const response = await fetch(agentNativePath(path), {
|
|
216
|
+
credentials: "include",
|
|
217
|
+
});
|
|
218
|
+
const text = await response.text();
|
|
219
|
+
let payload;
|
|
220
|
+
try {
|
|
221
|
+
payload = text.trim() ? JSON.parse(text) : undefined;
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
throw new Error(`Could not read the Ollama models response (HTTP ${response.status}).`);
|
|
225
|
+
}
|
|
226
|
+
if (!response.ok) {
|
|
227
|
+
const message = payload &&
|
|
228
|
+
typeof payload === "object" &&
|
|
229
|
+
typeof payload.error === "string"
|
|
230
|
+
? payload.error
|
|
231
|
+
: `Could not list Ollama models (HTTP ${response.status}).`;
|
|
232
|
+
throw new Error(message);
|
|
233
|
+
}
|
|
234
|
+
const models = payload && typeof payload === "object"
|
|
235
|
+
? payload.models
|
|
236
|
+
: undefined;
|
|
237
|
+
return Array.isArray(models)
|
|
238
|
+
? models.filter((model) => typeof model === "string")
|
|
239
|
+
: [];
|
|
240
|
+
}
|
|
204
241
|
/**
|
|
205
242
|
* Select the provider and model for the next conversation. This is separate
|
|
206
243
|
* from saving credentials so keyless local providers such as Ollama can use
|
|
@@ -72,7 +72,7 @@ export const AGENT_PROVIDER_CATALOG = PROVIDER_ORDER.map((id) => {
|
|
|
72
72
|
? {
|
|
73
73
|
supportsEndpoint: true,
|
|
74
74
|
endpointPlaceholder: isOllama
|
|
75
|
-
? "http://localhost:11434"
|
|
75
|
+
? "http://localhost:11434 or local network address like http://192.168.1.123:11434"
|
|
76
76
|
: "https://gateway.example/v1",
|
|
77
77
|
}
|
|
78
78
|
: {}),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { IconBolt, IconCheck, IconChevronDown, IconDeviceDesktop, IconExternalLink, IconKey, IconLoader2, IconRoute, IconServer2, } from "@tabler/icons-react";
|
|
2
|
+
import { IconBolt, IconCheck, IconChevronDown, IconDeviceDesktop, IconExternalLink, IconKey, IconLoader2, IconRoute, IconSearch, IconServer2, } from "@tabler/icons-react";
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
|
-
import { deleteAgentEnginePersonalProviderSettings, getAgentEngineProviderKeyStatus, saveAgentEngineProviderSettings, setAgentEngineProvider, } from "../agent-engine-key.js";
|
|
4
|
+
import { deleteAgentEnginePersonalProviderSettings, fetchOllamaModels, getAgentEngineProviderKeyStatus, saveAgentEngineProviderSettings, setAgentEngineProvider, } from "../agent-engine-key.js";
|
|
5
5
|
import { getAgentProviderOption, } from "../agent-provider-catalog.js";
|
|
6
6
|
import { useT } from "../i18n.js";
|
|
7
7
|
import { cn } from "../utils.js";
|
|
@@ -14,13 +14,16 @@ export function AgentProviderSetupForm({ initialProvider = "anthropic", configur
|
|
|
14
14
|
const [apiKey, setApiKey] = useState("");
|
|
15
15
|
const [model, setModel] = useState("");
|
|
16
16
|
const [endpoint, setEndpoint] = useState("");
|
|
17
|
-
const [endpointOpen, setEndpointOpen] = useState(
|
|
17
|
+
const [endpointOpen, setEndpointOpen] = useState(false);
|
|
18
18
|
const [saving, setSaving] = useState(false);
|
|
19
19
|
const [removingPersonalKey, setRemovingPersonalKey] = useState(false);
|
|
20
20
|
const [saved, setSaved] = useState(false);
|
|
21
21
|
const [error, setError] = useState(null);
|
|
22
22
|
const [providerKeyStatus, setProviderKeyStatus] = useState(null);
|
|
23
23
|
const [statusError, setStatusError] = useState(false);
|
|
24
|
+
const [ollamaModels, setOllamaModels] = useState(null);
|
|
25
|
+
const [ollamaModelsError, setOllamaModelsError] = useState(null);
|
|
26
|
+
const [ollamaModelsLoading, setOllamaModelsLoading] = useState(false);
|
|
24
27
|
const active = getAgentProviderOption(provider);
|
|
25
28
|
const refreshProviderKeyStatus = async () => {
|
|
26
29
|
if (!active.key) {
|
|
@@ -66,10 +69,54 @@ export function AgentProviderSetupForm({ initialProvider = "anthropic", configur
|
|
|
66
69
|
setModel(active.defaultModel);
|
|
67
70
|
setApiKey("");
|
|
68
71
|
setEndpoint("");
|
|
69
|
-
setEndpointOpen(
|
|
72
|
+
setEndpointOpen(false);
|
|
70
73
|
setError(null);
|
|
71
74
|
setSaved(false);
|
|
75
|
+
setOllamaModels(null);
|
|
76
|
+
setOllamaModelsError(null);
|
|
72
77
|
}, [active.defaultModel, provider]);
|
|
78
|
+
// Ask the Ollama server itself which models it has pulled, instead of only
|
|
79
|
+
// offering the static suggestion list. Triggered explicitly by the "Find
|
|
80
|
+
// models" button rather than on every keystroke, so the request always
|
|
81
|
+
// uses the address the user actually meant to check.
|
|
82
|
+
const handleFindOllamaModels = () => {
|
|
83
|
+
setOllamaModelsLoading(true);
|
|
84
|
+
setOllamaModelsError(null);
|
|
85
|
+
const typedEndpoint = endpoint.trim();
|
|
86
|
+
void fetchOllamaModels(typedEndpoint || undefined)
|
|
87
|
+
.then(async (models) => {
|
|
88
|
+
setOllamaModels(models);
|
|
89
|
+
setOllamaModelsError(null);
|
|
90
|
+
// A successful check is the only signal this address actually works.
|
|
91
|
+
// Persist it as soon as it's confirmed — other surfaces that read
|
|
92
|
+
// the saved Ollama endpoint (like the chat composer's model picker)
|
|
93
|
+
// have no address field of their own, so without this they keep
|
|
94
|
+
// falling back to the http://localhost:11434 default until the main
|
|
95
|
+
// "Use Ollama" button below is also clicked.
|
|
96
|
+
if (typedEndpoint && active.endpointKey) {
|
|
97
|
+
try {
|
|
98
|
+
await saveAgentEngineProviderSettings({
|
|
99
|
+
provider,
|
|
100
|
+
key: active.endpointKey,
|
|
101
|
+
baseUrl: typedEndpoint,
|
|
102
|
+
});
|
|
103
|
+
void refreshProviderKeyStatus();
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// coercion-ok: the connectivity check itself still succeeded and
|
|
107
|
+
// the found models are shown; the address just wasn't persisted
|
|
108
|
+
// (e.g. a dropped session). The main submit button below retries
|
|
109
|
+
// the save, so this is never reported to the user as a clean
|
|
110
|
+
// success — it's a silent retry opportunity, not a lost error.
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
.catch((err) => {
|
|
115
|
+
setOllamaModels(null);
|
|
116
|
+
setOllamaModelsError(err instanceof Error ? err.message : String(err));
|
|
117
|
+
})
|
|
118
|
+
.finally(() => setOllamaModelsLoading(false));
|
|
119
|
+
};
|
|
73
120
|
const handleProviderChange = (nextProvider) => {
|
|
74
121
|
setProvider(nextProvider);
|
|
75
122
|
};
|
|
@@ -142,6 +189,7 @@ export function AgentProviderSetupForm({ initialProvider = "anthropic", configur
|
|
|
142
189
|
// newly released provider models usable before the catalog is refreshed.
|
|
143
190
|
const modelInputVisible = Boolean(active.key) || active.supportsCustomModel;
|
|
144
191
|
const endpointVisible = active.supportsEndpoint;
|
|
192
|
+
const isOllama = provider === "ollama";
|
|
145
193
|
return (_jsxs("form", { className: cn("space-y-3", className), onSubmit: (event) => {
|
|
146
194
|
event.preventDefault();
|
|
147
195
|
void handleSave();
|
|
@@ -159,9 +207,34 @@ export function AgentProviderSetupForm({ initialProvider = "anthropic", configur
|
|
|
159
207
|
setError(null);
|
|
160
208
|
}, className: cn("w-full rounded-md border border-input bg-background text-foreground outline-none transition-colors hover:bg-accent/40 focus:ring-1 focus:ring-ring focus:ring-offset-1 focus:ring-offset-background placeholder:text-muted-foreground/50", isPage ? "h-10 px-3 text-sm" : "h-8 px-2.5 text-[12px]") })] })) : (_jsxs("div", { className: "flex items-start gap-2 rounded-md border border-border/70 bg-background/60 px-2.5 py-2", children: [_jsx(IconServer2, { size: isPage ? 15 : 13, className: "mt-0.5 shrink-0 text-muted-foreground" }), _jsx("p", { className: "text-[11px] leading-relaxed text-muted-foreground", children: t("agentPanel.noApiKeyNeeded", {
|
|
161
209
|
defaultValue: "No API key required.",
|
|
162
|
-
}) })] })),
|
|
163
|
-
|
|
164
|
-
|
|
210
|
+
}) })] })), isOllama && endpointVisible ? (_jsxs("div", { className: "space-y-1.5", children: [_jsx("span", { className: cn("font-medium text-foreground", isPage ? "text-xs" : "text-[11px]"), children: t("agentPanel.endpointUrl", { defaultValue: "Endpoint URL" }) }), _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("input", { type: "url", value: endpoint, disabled: saving, spellCheck: false, autoComplete: "off", placeholder: active.endpointPlaceholder, onChange: (event) => {
|
|
211
|
+
setEndpoint(event.target.value);
|
|
212
|
+
setOllamaModels(null);
|
|
213
|
+
setOllamaModelsError(null);
|
|
214
|
+
}, className: cn("min-w-0 flex-1 rounded-md border border-input bg-background text-foreground outline-none transition-colors hover:bg-accent/40 focus:ring-1 focus:ring-ring focus:ring-offset-1 focus:ring-offset-background placeholder:text-muted-foreground/50", isPage ? "h-10 px-3 text-sm" : "h-8 px-2.5 text-[12px]") }), _jsxs("button", { type: "button", disabled: saving || ollamaModelsLoading, onClick: handleFindOllamaModels, className: cn("inline-flex shrink-0 items-center gap-1.5 rounded-md border border-input bg-background font-medium text-foreground transition-colors hover:bg-accent/40 disabled:cursor-not-allowed disabled:opacity-50", isPage ? "h-10 px-3 text-xs" : "h-8 px-2.5 text-[11px]"), children: [ollamaModelsLoading ? (_jsx(IconLoader2, { size: 13, className: "animate-spin" })) : (_jsx(IconSearch, { size: 13 })), t("agentPanel.findModels", { defaultValue: "Find models" })] })] })] })) : null, modelInputVisible ? (_jsxs("label", { className: "block space-y-1.5", children: [_jsx("span", { className: cn("font-medium text-foreground", isPage ? "text-xs" : "text-[11px]"), children: t("agentPanel.modelId", { defaultValue: "Model ID" }) }), _jsx("input", { type: "text", value: model, list: isOllama ? undefined : `agent-provider-models-${provider}`, disabled: saving, spellCheck: false, autoComplete: "off", placeholder: active.defaultModel, onChange: (event) => setModel(event.target.value), className: cn("w-full rounded-md border border-input bg-background text-foreground outline-none transition-colors hover:bg-accent/40 focus:ring-1 focus:ring-ring focus:ring-offset-1 focus:ring-offset-background placeholder:text-muted-foreground/50", isPage ? "h-10 px-3 text-sm" : "h-8 px-2.5 text-[12px]") }), isOllama ? null : (_jsx("datalist", { id: `agent-provider-models-${provider}`, children: active.supportedModels.map((modelOption) => (_jsx("option", { value: modelOption }, modelOption))) })), isOllama ? (_jsxs("div", { className: "space-y-1.5", children: [ollamaModels && ollamaModels.length > 0 ? (_jsx("div", { className: "flex flex-wrap gap-1.5", children: ollamaModels.map((modelOption) => (_jsx("button", { type: "button", disabled: saving, onClick: () => setModel(modelOption), "aria-pressed": model === modelOption, className: cn("rounded-md border px-2 py-1 text-[11px] font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50", model === modelOption
|
|
215
|
+
? "border-primary bg-primary/10 text-primary"
|
|
216
|
+
: "border-border bg-background text-foreground hover:bg-accent/40"), children: modelOption }, modelOption))) })) : null, _jsx("p", { className: "text-[10px] leading-relaxed text-muted-foreground", children: ollamaModelsLoading
|
|
217
|
+
? t("agentPanel.ollamaModelsChecking", {
|
|
218
|
+
defaultValue: "Checking installed models…",
|
|
219
|
+
})
|
|
220
|
+
: ollamaModels && ollamaModels.length > 0
|
|
221
|
+
? t("agentPanel.ollamaModelsFound", {
|
|
222
|
+
count: ollamaModels.length,
|
|
223
|
+
})
|
|
224
|
+
: ollamaModels
|
|
225
|
+
? t("agentPanel.ollamaModelsNone", {
|
|
226
|
+
defaultValue: "Connected, but no models are pulled yet — run `ollama pull llama3.1`.",
|
|
227
|
+
})
|
|
228
|
+
: ollamaModelsError
|
|
229
|
+
? t("agentPanel.ollamaModelsError", {
|
|
230
|
+
error: ollamaModelsError,
|
|
231
|
+
defaultValue: `${ollamaModelsError} Showing example model names below.`,
|
|
232
|
+
})
|
|
233
|
+
: t("agentPanel.ollamaModelsPrompt", {
|
|
234
|
+
defaultValue: 'Click "Find models" above to list what your Ollama server actually has installed.',
|
|
235
|
+
}) })] })) : null] })) : null, !isOllama && endpointVisible ? (_jsxs("div", { className: "border-t border-border/70 pt-2", children: [_jsxs("button", { type: "button", onClick: () => setEndpointOpen((open) => !open), className: "flex w-full items-center justify-between gap-2 text-start text-[11px] font-medium text-foreground", "aria-expanded": endpointOpen, title: t("agentPanel.compatibleEndpointHint", {
|
|
236
|
+
defaultValue: "Use this for LiteLLM or another OpenAI-compatible gateway.",
|
|
237
|
+
}), children: [_jsxs("span", { className: "inline-flex items-center gap-1.5", children: [_jsx(IconChevronDown, { size: 13, className: cn("text-muted-foreground transition-transform", !endpointOpen && "-rotate-90") }), t("agentPanel.endpointUrl", {
|
|
165
238
|
defaultValue: "Endpoint URL",
|
|
166
239
|
})] }), _jsx("span", { className: "text-[10px] font-normal text-muted-foreground", children: t("agentPanel.optional", { defaultValue: "Optional" }) })] }), endpointOpen ? (_jsx("div", { className: "mt-2 space-y-1.5", children: _jsx("input", { type: "url", value: endpoint, disabled: saving, spellCheck: false, autoComplete: "off", placeholder: active.endpointPlaceholder, onChange: (event) => setEndpoint(event.target.value), className: cn("w-full rounded-md border border-input bg-background text-foreground outline-none transition-colors hover:bg-accent/40 focus:ring-1 focus:ring-ring focus:ring-offset-1 focus:ring-offset-background placeholder:text-muted-foreground/50", isPage ? "h-10 px-3 text-sm" : "h-8 px-2.5 text-[12px]") }) })) : null] })) : null, _jsxs("div", { className: "flex flex-wrap items-center gap-2 pt-0.5", children: [_jsx("button", { type: "submit", disabled: saving || Boolean(active.key && !apiKey.trim()), className: cn("inline-flex items-center justify-center gap-1.5 rounded-md bg-foreground font-medium text-background transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50", isPage ? "h-9 px-3 text-xs" : "h-8 px-3 text-[11px]"), children: saving ? (_jsxs(_Fragment, { children: [_jsx(IconLoader2, { size: isPage ? 14 : 11, className: "animate-spin" }), t("agentPanel.savingProvider", {
|
|
167
240
|
defaultValue: "Saving...",
|