goose2 distribution bundling (#8911)
This commit is contained in:
@@ -6,6 +6,9 @@ import { discoverAcpProvidersFromEntries } from "@/shared/api/acp";
|
||||
import { setNotificationHandler, getClient } from "@/shared/api/acpConnection";
|
||||
import notificationHandler from "@/shared/api/acpNotificationHandler";
|
||||
import { perfLog } from "@/shared/lib/perfLog";
|
||||
import { parseProviderAllowlist } from "@/features/providers/distroProviderConstraints";
|
||||
import { getModelProviders } from "@/features/providers/providerCatalog";
|
||||
import { useDistroStore } from "@/features/settings/stores/distroStore";
|
||||
|
||||
export function useAppStartup() {
|
||||
useEffect(() => {
|
||||
@@ -25,6 +28,18 @@ export function useAppStartup() {
|
||||
|
||||
const store = useAgentStore.getState();
|
||||
const inventoryStore = useProviderInventoryStore.getState();
|
||||
const distroStore = useDistroStore.getState();
|
||||
const loadDistroBundle = async () => {
|
||||
try {
|
||||
const { getDistroBundle } = await import("@/shared/api/distro");
|
||||
const manifest = await getDistroBundle();
|
||||
distroStore.setManifest(manifest);
|
||||
} catch (err) {
|
||||
console.error("Failed to load distro bundle on startup:", err);
|
||||
distroStore.setManifest({ present: false });
|
||||
}
|
||||
};
|
||||
|
||||
const loadPersonas = async () => {
|
||||
const t0 = performance.now();
|
||||
store.setPersonasLoading(true);
|
||||
@@ -57,7 +72,22 @@ export function useAppStartup() {
|
||||
|
||||
// Derive ACP providers from the same response
|
||||
const providers = discoverAcpProvidersFromEntries(entries);
|
||||
store.setProviders(providers);
|
||||
const providerAllowlist = parseProviderAllowlist(
|
||||
useDistroStore.getState().manifest,
|
||||
);
|
||||
if (!providerAllowlist) {
|
||||
store.setProviders(providers);
|
||||
} else {
|
||||
const hasAllowedModelProvider = getModelProviders().some(
|
||||
(provider) => providerAllowlist.has(provider.id),
|
||||
);
|
||||
store.setProviders(
|
||||
providers.filter(
|
||||
(provider) =>
|
||||
provider.id !== "goose" || hasAllowedModelProvider,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
perfLog(
|
||||
`[perf:startup] loadProvidersAndInventory done in ${(performance.now() - t0).toFixed(1)}ms (entries=${entries.length}, providers=${providers.length})`,
|
||||
@@ -87,6 +117,8 @@ export function useAppStartup() {
|
||||
setActiveSession(null);
|
||||
};
|
||||
|
||||
await loadDistroBundle();
|
||||
|
||||
const providersAndInventoryLoad = loadProvidersAndInventory();
|
||||
|
||||
await Promise.allSettled([
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { filterModelProvidersForDistro } from "./distroProviderConstraints";
|
||||
|
||||
describe("filterModelProvidersForDistro", () => {
|
||||
const providers = [
|
||||
{
|
||||
id: "anthropic",
|
||||
displayName: "Anthropic",
|
||||
category: "model",
|
||||
description: "Claude models",
|
||||
setupMethod: "single_api_key",
|
||||
tier: "promoted",
|
||||
},
|
||||
{
|
||||
id: "openai",
|
||||
displayName: "OpenAI",
|
||||
category: "model",
|
||||
description: "GPT models",
|
||||
setupMethod: "single_api_key",
|
||||
tier: "promoted",
|
||||
},
|
||||
{
|
||||
id: "ollama",
|
||||
displayName: "Ollama",
|
||||
category: "model",
|
||||
description: "Local models",
|
||||
setupMethod: "local",
|
||||
tier: "promoted",
|
||||
},
|
||||
] as const;
|
||||
|
||||
it("returns all providers when no distro is present", () => {
|
||||
expect(
|
||||
filterModelProvidersForDistro([...providers], { present: false }),
|
||||
).toEqual(providers);
|
||||
});
|
||||
|
||||
it("returns all providers when no allowlist is configured", () => {
|
||||
expect(
|
||||
filterModelProvidersForDistro([...providers], {
|
||||
present: true,
|
||||
}),
|
||||
).toEqual(providers);
|
||||
});
|
||||
|
||||
it("filters providers to the configured allowlist", () => {
|
||||
expect(
|
||||
filterModelProvidersForDistro([...providers], {
|
||||
present: true,
|
||||
providerAllowlist: "openai, ollama",
|
||||
}),
|
||||
).toEqual([providers[1], providers[2]]);
|
||||
});
|
||||
|
||||
it("ignores whitespace and empty allowlist items", () => {
|
||||
expect(
|
||||
filterModelProvidersForDistro([...providers], {
|
||||
present: true,
|
||||
providerAllowlist: " anthropic ,, openai ",
|
||||
}),
|
||||
).toEqual([providers[0], providers[1]]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { ProviderCatalogEntry } from "@/shared/types/providers";
|
||||
import type { DistroBundleInfo } from "@/shared/types/distro";
|
||||
|
||||
export function parseProviderAllowlist(
|
||||
distro: DistroBundleInfo | null | undefined,
|
||||
): Set<string> | null {
|
||||
if (!distro?.present) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const raw = distro.providerAllowlist?.trim();
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const providerIds = raw
|
||||
.split(",")
|
||||
.map((providerId) => providerId.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
return providerIds.length > 0 ? new Set(providerIds) : null;
|
||||
}
|
||||
|
||||
export function filterModelProvidersForDistro(
|
||||
providers: ProviderCatalogEntry[],
|
||||
distro: DistroBundleInfo | null | undefined,
|
||||
): ProviderCatalogEntry[] {
|
||||
const allowlist = parseProviderAllowlist(distro);
|
||||
if (!allowlist) {
|
||||
return providers;
|
||||
}
|
||||
|
||||
return providers.filter((provider) => allowlist.has(provider.id));
|
||||
}
|
||||
@@ -6,27 +6,22 @@ import type {
|
||||
ProviderInventoryModelDto,
|
||||
} from "@aaif/goose-sdk";
|
||||
import { getModelProviders } from "../providerCatalog";
|
||||
|
||||
const MODEL_PROVIDER_IDS = new Set(getModelProviders().map((p) => p.id));
|
||||
import { useDistroStore } from "@/features/settings/stores/distroStore";
|
||||
import { filterModelProvidersForDistro } from "../distroProviderConstraints";
|
||||
|
||||
function isConfiguredGooseModelProvider(
|
||||
entry: ProviderInventoryEntryDto,
|
||||
modelProviderIds: Set<string>,
|
||||
): boolean {
|
||||
if (!entry.configured) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isCuratedModelProvider = MODEL_PROVIDER_IDS.has(entry.providerId);
|
||||
|
||||
if (entry.providerType === "Custom") {
|
||||
return entry.providerId.startsWith("custom_");
|
||||
}
|
||||
|
||||
if (entry.providerType === "Declarative") {
|
||||
return isCuratedModelProvider;
|
||||
}
|
||||
|
||||
return isCuratedModelProvider;
|
||||
return modelProviderIds.has(entry.providerId);
|
||||
}
|
||||
|
||||
function inventoryModelToOption(
|
||||
@@ -48,6 +43,7 @@ function inventoryModelToOption(
|
||||
export function useProviderInventory() {
|
||||
const entries = useProviderInventoryStore((s) => s.entries);
|
||||
const loading = useProviderInventoryStore((s) => s.loading);
|
||||
const distro = useDistroStore((s) => s.manifest);
|
||||
|
||||
const getEntry = useCallback(
|
||||
(providerId: string) => entries.get(providerId),
|
||||
@@ -63,9 +59,22 @@ export function useProviderInventory() {
|
||||
[entries],
|
||||
);
|
||||
|
||||
const modelProviderIds = useMemo(
|
||||
() =>
|
||||
new Set(
|
||||
filterModelProvidersForDistro(getModelProviders(), distro).map(
|
||||
(provider) => provider.id,
|
||||
),
|
||||
),
|
||||
[distro],
|
||||
);
|
||||
|
||||
const configuredModelProviderEntries = useMemo(
|
||||
() => [...entries.values()].filter(isConfiguredGooseModelProvider),
|
||||
[entries],
|
||||
() =>
|
||||
[...entries.values()].filter((entry) =>
|
||||
isConfiguredGooseModelProvider(entry, modelProviderIds),
|
||||
),
|
||||
[entries, modelProviderIds],
|
||||
);
|
||||
|
||||
const getModelsForAgent = useCallback(
|
||||
@@ -84,8 +93,8 @@ export function useProviderInventory() {
|
||||
const configuredProviderIds = useMemo(
|
||||
() =>
|
||||
[...entries.values()]
|
||||
.filter((e) => e.configured)
|
||||
.map((e) => e.providerId),
|
||||
.filter((entry) => entry.configured)
|
||||
.map((entry) => entry.providerId),
|
||||
[entries],
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { create } from "zustand";
|
||||
import type { DistroBundleInfo } from "@/shared/types/distro";
|
||||
|
||||
interface DistroState {
|
||||
loaded: boolean;
|
||||
manifest: DistroBundleInfo;
|
||||
setManifest: (manifest: DistroBundleInfo) => void;
|
||||
}
|
||||
|
||||
const EMPTY_DISTRO: DistroBundleInfo = {
|
||||
present: false,
|
||||
};
|
||||
|
||||
export const useDistroStore = create<DistroState>((set) => ({
|
||||
loaded: false,
|
||||
manifest: EMPTY_DISTRO,
|
||||
setManifest: (manifest) => set({ manifest, loaded: true }),
|
||||
}));
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
getModelProviders,
|
||||
} from "@/features/providers/providerCatalog";
|
||||
import { useCredentials } from "@/features/providers/hooks/useCredentials";
|
||||
import { useDistroStore } from "@/features/settings/stores/distroStore";
|
||||
import { filterModelProvidersForDistro } from "@/features/providers/distroProviderConstraints";
|
||||
import { useCustomProviders } from "@/features/providers/hooks/useCustomProviders";
|
||||
import {
|
||||
CustomProviderChoice,
|
||||
@@ -100,6 +102,7 @@ interface PendingCustomProviderDelete {
|
||||
|
||||
export function ProvidersSettings() {
|
||||
const { t } = useTranslation(["settings", "common"]);
|
||||
const distro = useDistroStore((state) => state.manifest);
|
||||
const [showAllModels, setShowAllModels] = useState(false);
|
||||
const [modelOrder, setModelOrder] = useState<string[] | null>(null);
|
||||
const [customDialogOpen, setCustomDialogOpen] = useState(false);
|
||||
@@ -137,8 +140,12 @@ export function ProvidersSettings() {
|
||||
);
|
||||
|
||||
const allModels = useMemo(
|
||||
() => toDisplayInfo(getModelProviders(), configuredIds),
|
||||
[configuredIds],
|
||||
() =>
|
||||
toDisplayInfo(
|
||||
filterModelProvidersForDistro(getModelProviders(), distro),
|
||||
configuredIds,
|
||||
),
|
||||
[configuredIds, distro],
|
||||
);
|
||||
|
||||
const sortedModels = useMemo(() => {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type { DistroBundleInfo } from "@/shared/types/distro";
|
||||
|
||||
export async function getDistroBundle(): Promise<DistroBundleInfo> {
|
||||
return invoke("get_distro_bundle");
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from "./agents";
|
||||
export * from "./acp";
|
||||
export * from "./distro";
|
||||
export * from "./git";
|
||||
export * from "./pathResolver";
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface DistroBundleInfo {
|
||||
present: boolean;
|
||||
appVersion?: string;
|
||||
featureToggles?: Record<string, boolean>;
|
||||
extensionAllowlist?: string;
|
||||
providerAllowlist?: string;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./distro";
|
||||
export * from "./messages";
|
||||
export * from "./agents";
|
||||
export * from "./chat";
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { ComponentProps } from "react";
|
||||
import { createContext, useContext, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getUsage } from "tokenlens";
|
||||
import { useDistroStore } from "@/features/settings/stores/distroStore";
|
||||
|
||||
const PERCENT_MAX = 100;
|
||||
const ICON_RADIUS = 10;
|
||||
@@ -61,6 +62,13 @@ export const Context = ({
|
||||
);
|
||||
};
|
||||
|
||||
function useCostTrackingEnabled() {
|
||||
const featureToggles = useDistroStore(
|
||||
(state) => state.manifest.featureToggles,
|
||||
);
|
||||
return featureToggles?.costTracking !== false;
|
||||
}
|
||||
|
||||
const ContextIcon = () => {
|
||||
const { t } = useTranslation("common");
|
||||
const { usedTokens, maxTokens } = useContextValue();
|
||||
@@ -199,6 +207,7 @@ export const ContextContentFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: ContextContentFooterProps) => {
|
||||
const costTrackingEnabled = useCostTrackingEnabled();
|
||||
const { t } = useTranslation("common");
|
||||
const { formatNumber } = useLocaleFormatting();
|
||||
const { modelId, usage } = useContextValue();
|
||||
@@ -216,6 +225,10 @@ export const ContextContentFooter = ({
|
||||
style: "currency",
|
||||
});
|
||||
|
||||
if (!costTrackingEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -241,6 +254,7 @@ const TokensWithCost = ({
|
||||
tokens?: number;
|
||||
costText?: string;
|
||||
}) => {
|
||||
const costTrackingEnabled = useCostTrackingEnabled();
|
||||
const { formatNumber } = useLocaleFormatting();
|
||||
|
||||
return (
|
||||
@@ -250,7 +264,7 @@ const TokensWithCost = ({
|
||||
: formatNumber(tokens, {
|
||||
notation: "compact",
|
||||
})}
|
||||
{costText ? (
|
||||
{costTrackingEnabled && costText ? (
|
||||
<span className="ml-2 text-muted-foreground">• {costText}</span>
|
||||
) : null}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user