diff --git a/src/api/generated/types.gen.ts b/src/api/generated/types.gen.ts index 2a202061..388b8a8f 100644 --- a/src/api/generated/types.gen.ts +++ b/src/api/generated/types.gen.ts @@ -19,10 +19,28 @@ export type AddProviderEndpointRequest = { description?: string; provider_type: ProviderType; endpoint?: string; - auth_type?: ProviderAuthType | null; + auth_type?: ProviderAuthType; api_key?: string | null; }; +/** + * Represents an alert. + */ +export type Alert = { + id: string; + prompt_id: string; + code_snippet: CodeSnippet | null; + trigger_string: + | string + | { + [key: string]: unknown; + } + | null; + trigger_type: string; + trigger_category: string | null; + timestamp: string; +}; + /** * Represents an alert with it's respective conversation. */ @@ -50,11 +68,19 @@ export type ChatMessage = { message_id: string; }; +/** + * Represents a code snippet with its programming language. + * + * Args: + * language: The programming language identifier (e.g., 'python', 'javascript') + * code: The actual code content + */ export type CodeSnippet = { code: string; language: string | null; filepath: string | null; libraries?: Array; + file_extension?: string | null; }; /** @@ -75,6 +101,7 @@ export type Conversation = { chat_id: string; conversation_timestamp: string; token_usage_agg: TokenUsageAggregate | null; + alerts?: Array; }; export type CreateOrRenameWorkspaceRequest = { @@ -146,7 +173,7 @@ export type ProviderEndpoint = { description?: string; provider_type: ProviderType; endpoint?: string; - auth_type?: ProviderAuthType | null; + auth_type?: ProviderAuthType; }; /** @@ -159,6 +186,7 @@ export enum ProviderType { OLLAMA = "ollama", LM_STUDIO = "lm_studio", LLAMACPP = "llamacpp", + OPENROUTER = "openrouter", } /** diff --git a/src/api/openapi.json b/src/api/openapi.json index 6536af0f..1969d932 100644 --- a/src/api/openapi.json +++ b/src/api/openapi.json @@ -1145,14 +1145,7 @@ "default": "" }, "auth_type": { - "anyOf": [ - { - "$ref": "#/components/schemas/ProviderAuthType" - }, - { - "type": "null" - } - ], + "$ref": "#/components/schemas/ProviderAuthType", "default": "none" }, "api_key": { @@ -1175,6 +1168,74 @@ "title": "AddProviderEndpointRequest", "description": "Represents a request to add a provider endpoint." }, + "Alert": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "prompt_id": { + "type": "string", + "title": "Prompt Id" + }, + "code_snippet": { + "anyOf": [ + { + "$ref": "#/components/schemas/CodeSnippet" + }, + { + "type": "null" + } + ] + }, + "trigger_string": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Trigger String" + }, + "trigger_type": { + "type": "string", + "title": "Trigger Type" + }, + "trigger_category": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Trigger Category" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "title": "Timestamp" + } + }, + "type": "object", + "required": [ + "id", + "prompt_id", + "code_snippet", + "trigger_string", + "trigger_type", + "trigger_category", + "timestamp" + ], + "title": "Alert", + "description": "Represents an alert." + }, "AlertConversation": { "properties": { "conversation": { @@ -1300,7 +1361,19 @@ "type": "string" }, "type": "array", - "title": "Libraries" + "title": "Libraries", + "default": [] + }, + "file_extension": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "File Extension" } }, "type": "object", @@ -1309,7 +1382,8 @@ "language", "filepath" ], - "title": "CodeSnippet" + "title": "CodeSnippet", + "description": "Represents a code snippet with its programming language.\n\nArgs:\n language: The programming language identifier (e.g., 'python', 'javascript')\n code: The actual code content" }, "ConfigureAuthMaterial": { "properties": { @@ -1376,6 +1450,14 @@ "type": "null" } ] + }, + "alerts": { + "items": { + "$ref": "#/components/schemas/Alert" + }, + "type": "array", + "title": "Alerts", + "default": [] } }, "type": "object", @@ -1580,14 +1662,7 @@ "default": "" }, "auth_type": { - "anyOf": [ - { - "$ref": "#/components/schemas/ProviderAuthType" - }, - { - "type": "null" - } - ], + "$ref": "#/components/schemas/ProviderAuthType", "default": "none" } }, @@ -1607,7 +1682,8 @@ "vllm", "ollama", "lm_studio", - "llamacpp" + "llamacpp", + "openrouter" ], "title": "ProviderType", "description": "Represents the different types of providers we support." diff --git a/src/features/providers/components/provider-form.tsx b/src/features/providers/components/provider-form.tsx index 0614af40..d3531d1a 100644 --- a/src/features/providers/components/provider-form.tsx +++ b/src/features/providers/components/provider-form.tsx @@ -8,6 +8,8 @@ import { } from "@stacklok/ui-kit"; import { getAuthTypeOptions, + getProviderAuthByType, + getProviderEndpointByAuthType, getProviderType, isProviderAuthType, isProviderType, @@ -19,6 +21,19 @@ interface Props { } export function ProviderForm({ provider, setProvider }: Props) { + const providerAuthType = + provider.auth_type || getProviderAuthByType(provider.provider_type); + const providerEndpoint = + provider.endpoint || getProviderEndpointByAuthType(provider.provider_type); + + const handleProviderType = (provider: AddProviderEndpointRequest) => { + setProvider({ + ...provider, + auth_type: getProviderAuthByType(provider.provider_type), + endpoint: getProviderEndpointByAuthType(provider.provider_type), + }); + }; + return (
@@ -45,7 +60,7 @@ export function ProviderForm({ provider, setProvider }: Props) { items={getProviderType()} onSelectionChange={(provider_type) => { if (isProviderType(provider_type)) { - setProvider({ + handleProviderType({ ...provider, provider_type, }); @@ -78,7 +93,7 @@ export function ProviderForm({ provider, setProvider }: Props) { onChange={(endpoint) => setProvider({ ...provider, endpoint })} > - +
@@ -86,7 +101,7 @@ export function ProviderForm({ provider, setProvider }: Props) { ["mutateAsync"]; -}): OptionsSchema<"menu">[] => [ - { - textValue: "Edit", - icon: , - id: "edit", - href: `/providers/${provider.id}`, - }, - { - textValue: "Delete", - icon: , - id: "delete", - onAction: () => - deleteProvider({ path: { provider_id: provider.id as string } }), - }, -]; - -export function TableActions({ provider }: { provider: ProviderEndpoint }) { - const deleteProvider = useConfirmDeleteProvider(); - - return ( - - - - - - - ); -} diff --git a/src/features/providers/components/table-providers.tsx b/src/features/providers/components/table-providers.tsx index 197f5c37..8b8a886a 100644 --- a/src/features/providers/components/table-providers.tsx +++ b/src/features/providers/components/table-providers.tsx @@ -8,14 +8,15 @@ import { LinkButton, TableHeader, ResizableTableContainer, + Button, } from "@stacklok/ui-kit"; -import { Globe02, Tool01 } from "@untitled-ui/icons-react"; +import { Globe02, Tool01, Trash01 } from "@untitled-ui/icons-react"; import { PROVIDER_AUTH_TYPE_MAP } from "../lib/utils"; -import { TableActions } from "./table-actions"; import { useProviders } from "../hooks/use-providers"; import { match } from "ts-pattern"; import { ComponentProps } from "react"; import { ProviderEndpoint } from "@/api/generated"; +import { useConfirmDeleteProvider } from "../hooks/use-confirm-delete-provider"; const COLUMN_MAP = { provider: "provider", @@ -35,7 +36,7 @@ const COLUMNS: Column[] = [ width: "40%", }, { id: "type", children: "Provider", width: "10%", className: "capitalize" }, - { id: "endpoint", children: "Endpoint", width: "20%" }, + { id: "endpoint", children: "Endpoint", width: "20%", minWidth: 250 }, { id: "auth", children: "Authentication", width: "20%" }, { id: "configuration", alignment: "end", width: "10%", children: "" }, ]; @@ -43,9 +44,11 @@ const COLUMNS: Column[] = [ function CellRenderer({ column, row, + deleteProvider, }: { column: Column; row: ProviderEndpoint; + deleteProvider: () => void; }) { return match(column.id) .with(COLUMN_MAP.provider, () => ( @@ -79,12 +82,17 @@ function CellRenderer({
)) - .with(COLUMN_MAP.configuration, () => ) + .with(COLUMN_MAP.configuration, () => ( + + )) .exhaustive(); } export function TableProviders() { const { data: providers = [] } = useProviders(); + const deleteProvider = useConfirmDeleteProvider(); return ( @@ -102,7 +110,15 @@ export function TableProviders() { id={column.id} alignment={column.alignment} > - + { + deleteProvider({ + path: { provider_id: row.id as string }, + }); + }} + /> )} diff --git a/src/features/providers/hooks/use-mutation-create-provider.ts b/src/features/providers/hooks/use-mutation-create-provider.ts index b420d940..563af163 100644 --- a/src/features/providers/hooks/use-mutation-create-provider.ts +++ b/src/features/providers/hooks/use-mutation-create-provider.ts @@ -8,6 +8,7 @@ export function useMutationCreateProvider() { const invalidate = useInvalidateProvidersQueries(); return useToastMutation({ ...v1AddProviderEndpointMutation(), + successMsg: "Successfully added provider", onSuccess: async () => { await invalidate(); navigate("/providers"); diff --git a/src/features/providers/hooks/use-mutation-update-provider.ts b/src/features/providers/hooks/use-mutation-update-provider.ts index b8c987bf..9ad43429 100644 --- a/src/features/providers/hooks/use-mutation-update-provider.ts +++ b/src/features/providers/hooks/use-mutation-update-provider.ts @@ -12,24 +12,33 @@ export function useMutationUpdateProvider() { const navigate = useNavigate(); const invalidate = useInvalidateProvidersQueries(); - const mutationFn = ({ api_key, ...rest }: AddProviderEndpointRequest) => { + const mutationFn = async ({ + api_key, + ...rest + }: AddProviderEndpointRequest) => { const provider_id = rest.id; if (!provider_id) throw new Error("Provider is missing"); - return Promise.all([ - v1ConfigureAuthMaterial({ - path: { provider_id }, - body: { - api_key: api_key, - auth_type: rest.auth_type as ProviderAuthType, - }, - throwOnError: true, - }), - v1UpdateProviderEndpoint({ - path: { provider_id }, - body: rest, - }), - ]); + const updateProviderPromise = v1UpdateProviderEndpoint({ + path: { provider_id }, + body: rest, + }); + + // don't update the api key if it's not updated + if (!api_key && rest.auth_type === ProviderAuthType.API_KEY) { + return updateProviderPromise; + } + + const updateApiKey = v1ConfigureAuthMaterial({ + path: { provider_id }, + body: { + api_key: rest.auth_type === ProviderAuthType.API_KEY ? api_key : null, + auth_type: rest.auth_type as ProviderAuthType, + }, + throwOnError: true, + }); + + return Promise.all([updateApiKey, updateProviderPromise]); }; return useToastMutation({ diff --git a/src/features/providers/hooks/use-provider.ts b/src/features/providers/hooks/use-provider.ts index 87e5044c..76fd2de2 100644 --- a/src/features/providers/hooks/use-provider.ts +++ b/src/features/providers/hooks/use-provider.ts @@ -7,7 +7,7 @@ export function useProvider(providerId: string) { const [provider, setProvider] = useState({ name: "", description: "", - auth_type: null, + auth_type: undefined, provider_type: ProviderType.OPENAI, endpoint: "", api_key: "", diff --git a/src/features/providers/lib/utils.ts b/src/features/providers/lib/utils.ts index 80a2e7df..47280549 100644 --- a/src/features/providers/lib/utils.ts +++ b/src/features/providers/lib/utils.ts @@ -1,4 +1,5 @@ import { ProviderAuthType, ProviderType } from "@/api/generated"; +import { match } from "ts-pattern"; export const PROVIDER_AUTH_TYPE_MAP = { [ProviderAuthType.NONE]: "None", @@ -27,3 +28,23 @@ export function isProviderType(value: unknown): value is ProviderType { export function isProviderAuthType(value: unknown): value is ProviderAuthType { return Object.values(ProviderAuthType).includes(value as ProviderAuthType); } + +export function getProviderEndpointByAuthType(provider_type: ProviderType) { + return match(provider_type) + .with(ProviderType.OPENAI, () => "https://api.openai.com") + .with(ProviderType.ANTHROPIC, () => "https://api.anthropic.com") + .with(ProviderType.OPENROUTER, () => "https://openrouter.ai/api") + .with(ProviderType.OLLAMA, () => "http://host.docker.internal:11434") + .with(ProviderType.LM_STUDIO, () => "http://host.docker.internal:1234") + .with(ProviderType.LLAMACPP, () => "http://host.docker.internal:8080") + .with(ProviderType.VLLM, () => "http://host.docker.internal:8000") + .exhaustive(); +} + +export function getProviderAuthByType(provider_type: ProviderType) { + return match(provider_type) + .with(ProviderType.OPENAI, () => ProviderAuthType.API_KEY) + .with(ProviderType.ANTHROPIC, () => ProviderAuthType.API_KEY) + .with(ProviderType.OPENROUTER, () => ProviderAuthType.API_KEY) + .otherwise(() => ProviderAuthType.NONE); +} diff --git a/src/features/workspace/components/workspace-preferred-model.tsx b/src/features/workspace/components/workspace-preferred-model.tsx index 47747732..60ba4331 100644 --- a/src/features/workspace/components/workspace-preferred-model.tsx +++ b/src/features/workspace/components/workspace-preferred-model.tsx @@ -1,9 +1,12 @@ import { + Alert, Button, Card, CardBody, CardFooter, Form, + Link, + LinkButton, Text, } from "@stacklok/ui-kit"; import { twMerge } from "tailwind-merge"; @@ -14,6 +17,19 @@ import { usePreferredModelWorkspace } from "../hooks/use-preferred-preferred-mod import { Select, SelectButton } from "@stacklok/ui-kit"; import { useModelsData } from "@/hooks/use-models-data"; +function MissingProviderBanner() { + return ( + + + Add Provider + + + ); +} + export function WorkspacePreferredModel({ className, workspaceName, @@ -23,11 +39,12 @@ export function WorkspacePreferredModel({ workspaceName: string; isArchived: boolean | undefined; }) { - const { preferredModel, setPreferredModel } = + const { preferredModel, setPreferredModel, isPending } = usePreferredModelWorkspace(workspaceName); const { mutateAsync } = useMutationPreferredModelWorkspace(); const { data: providerModels = [] } = useModelsData(); const { model, provider_id } = preferredModel; + const isModelsEmpty = !isPending && providerModels.length === 0; const handleSubmit = (event: FormEvent) => { event.preventDefault(); @@ -50,16 +67,22 @@ export function WorkspacePreferredModel({
Preferred Model - - Select the model you would like to use in this workspace. + + Select the model you would like to use in this workspace. This + section applies only if you are using the{" "} + + MUX endpoint. +
+ {isModelsEmpty && }