diff --git a/package-lock.json b/package-lock.json index 8b580694..0d96321b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,8 @@ "name": "vite-project", "version": "0.9.0", "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", "@hey-api/client-fetch": "^0.7.1", "@monaco-editor/react": "^4.6.0", "@radix-ui/react-dialog": "^1.1.4", @@ -32,6 +34,7 @@ "tailwind-merge": "^2.5.5", "tailwind-variants": "^0.3.0", "tailwindcss-animate": "^1.0.7", + "uuid": "^11.0.5", "zustand": "^5.0.3" }, "devDependencies": { @@ -397,6 +400,55 @@ "tough-cookie": "^4.1.4" } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.24.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", @@ -11855,6 +11907,18 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uuid": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz", + "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", diff --git a/package.json b/package.json index b4cf0642..148d4218 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,8 @@ "generate-icons": "npx @svgr/cli --typescript --no-dimensions --replace-attr-values '#2E323A=currentColor' --jsx-runtime automatic --out-dir ./src/components/icons/ -- icons" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", "@hey-api/client-fetch": "^0.7.1", "@monaco-editor/react": "^4.6.0", "@radix-ui/react-dialog": "^1.1.4", @@ -43,6 +45,7 @@ "tailwind-merge": "^2.5.5", "tailwind-variants": "^0.3.0", "tailwindcss-animate": "^1.0.7", + "uuid": "^11.0.5", "zustand": "^5.0.3" }, "devDependencies": { diff --git a/src/api/generated/@tanstack/react-query.gen.ts b/src/api/generated/@tanstack/react-query.gen.ts index 2ca69ce6..8fe97ff7 100644 --- a/src/api/generated/@tanstack/react-query.gen.ts +++ b/src/api/generated/@tanstack/react-query.gen.ts @@ -7,11 +7,11 @@ import { healthCheckHealthGet, v1ListProviderEndpoints, v1AddProviderEndpoint, + v1ListAllModelsForAllProviders, + v1ListModelsByProvider, v1GetProviderEndpoint, v1UpdateProviderEndpoint, v1DeleteProviderEndpoint, - v1ListModelsByProvider, - v1ListAllModelsForAllProviders, v1ListWorkspaces, v1CreateWorkspace, v1ListActiveWorkspaces, @@ -36,6 +36,7 @@ import type { V1AddProviderEndpointData, V1AddProviderEndpointError, V1AddProviderEndpointResponse, + V1ListModelsByProviderData, V1GetProviderEndpointData, V1UpdateProviderEndpointData, V1UpdateProviderEndpointError, @@ -43,7 +44,6 @@ import type { V1DeleteProviderEndpointData, V1DeleteProviderEndpointError, V1DeleteProviderEndpointResponse, - V1ListModelsByProviderData, V1CreateWorkspaceData, V1CreateWorkspaceError, V1CreateWorkspaceResponse, @@ -190,6 +190,48 @@ export const v1AddProviderEndpointMutation = ( return mutationOptions; }; +export const v1ListAllModelsForAllProvidersQueryKey = ( + options?: OptionsLegacyParser, +) => [createQueryKey("v1ListAllModelsForAllProviders", options)]; + +export const v1ListAllModelsForAllProvidersOptions = ( + options?: OptionsLegacyParser, +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await v1ListAllModelsForAllProviders({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: v1ListAllModelsForAllProvidersQueryKey(options), + }); +}; + +export const v1ListModelsByProviderQueryKey = ( + options: OptionsLegacyParser, +) => [createQueryKey("v1ListModelsByProvider", options)]; + +export const v1ListModelsByProviderOptions = ( + options: OptionsLegacyParser, +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await v1ListModelsByProvider({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: v1ListModelsByProviderQueryKey(options), + }); +}; + export const v1GetProviderEndpointQueryKey = ( options: OptionsLegacyParser, ) => [createQueryKey("v1GetProviderEndpoint", options)]; @@ -251,48 +293,6 @@ export const v1DeleteProviderEndpointMutation = ( return mutationOptions; }; -export const v1ListModelsByProviderQueryKey = ( - options: OptionsLegacyParser, -) => [createQueryKey("v1ListModelsByProvider", options)]; - -export const v1ListModelsByProviderOptions = ( - options: OptionsLegacyParser, -) => { - return queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await v1ListModelsByProvider({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: v1ListModelsByProviderQueryKey(options), - }); -}; - -export const v1ListAllModelsForAllProvidersQueryKey = ( - options?: OptionsLegacyParser, -) => [createQueryKey("v1ListAllModelsForAllProviders", options)]; - -export const v1ListAllModelsForAllProvidersOptions = ( - options?: OptionsLegacyParser, -) => { - return queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await v1ListAllModelsForAllProviders({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: v1ListAllModelsForAllProvidersQueryKey(options), - }); -}; - export const v1ListWorkspacesQueryKey = (options?: OptionsLegacyParser) => [ createQueryKey("v1ListWorkspaces", options), ]; diff --git a/src/api/generated/sdk.gen.ts b/src/api/generated/sdk.gen.ts index 37fe6c10..b94d71b6 100644 --- a/src/api/generated/sdk.gen.ts +++ b/src/api/generated/sdk.gen.ts @@ -14,6 +14,11 @@ import type { V1AddProviderEndpointData, V1AddProviderEndpointError, V1AddProviderEndpointResponse, + V1ListAllModelsForAllProvidersError, + V1ListAllModelsForAllProvidersResponse, + V1ListModelsByProviderData, + V1ListModelsByProviderError, + V1ListModelsByProviderResponse, V1GetProviderEndpointData, V1GetProviderEndpointError, V1GetProviderEndpointResponse, @@ -23,11 +28,6 @@ import type { V1DeleteProviderEndpointData, V1DeleteProviderEndpointError, V1DeleteProviderEndpointResponse, - V1ListModelsByProviderData, - V1ListModelsByProviderError, - V1ListModelsByProviderResponse, - V1ListAllModelsForAllProvidersError, - V1ListAllModelsForAllProvidersResponse, V1ListWorkspacesError, V1ListWorkspacesResponse, V1CreateWorkspaceData, @@ -131,6 +131,42 @@ export const v1AddProviderEndpoint = ( }); }; +/** + * List All Models For All Providers + * List all models for all providers. + */ +export const v1ListAllModelsForAllProviders = < + ThrowOnError extends boolean = false, +>( + options?: OptionsLegacyParser, +) => { + return (options?.client ?? client).get< + V1ListAllModelsForAllProvidersResponse, + V1ListAllModelsForAllProvidersError, + ThrowOnError + >({ + ...options, + url: "/api/v1/provider-endpoints/models", + }); +}; + +/** + * List Models By Provider + * List models by provider. + */ +export const v1ListModelsByProvider = ( + options: OptionsLegacyParser, +) => { + return (options?.client ?? client).get< + V1ListModelsByProviderResponse, + V1ListModelsByProviderError, + ThrowOnError + >({ + ...options, + url: "/api/v1/provider-endpoints/{provider_id}/models", + }); +}; + /** * Get Provider Endpoint * Get a provider endpoint by ID. @@ -182,42 +218,6 @@ export const v1DeleteProviderEndpoint = ( }); }; -/** - * List Models By Provider - * List models by provider. - */ -export const v1ListModelsByProvider = ( - options: OptionsLegacyParser, -) => { - return (options?.client ?? client).get< - V1ListModelsByProviderResponse, - V1ListModelsByProviderError, - ThrowOnError - >({ - ...options, - url: "/api/v1/provider-endpoints/{provider_name}/models", - }); -}; - -/** - * List All Models For All Providers - * List all models for all providers. - */ -export const v1ListAllModelsForAllProviders = < - ThrowOnError extends boolean = false, ->( - options?: OptionsLegacyParser, -) => { - return (options?.client ?? client).get< - V1ListAllModelsForAllProvidersResponse, - V1ListAllModelsForAllProvidersError, - ThrowOnError - >({ - ...options, - url: "/api/v1/provider-endpoints/models", - }); -}; - /** * List Workspaces * List all workspaces. diff --git a/src/api/generated/types.gen.ts b/src/api/generated/types.gen.ts index c45aecb6..24105bd0 100644 --- a/src/api/generated/types.gen.ts +++ b/src/api/generated/types.gen.ts @@ -84,7 +84,8 @@ export type ListWorkspacesResponse = { */ export type ModelByProvider = { name: string; - provider: string; + provider_id: string; + provider_name: string; }; /** @@ -120,7 +121,7 @@ export enum ProviderAuthType { * so we can use this for muxing messages. */ export type ProviderEndpoint = { - id: number; + id?: string | null; name: string; description?: string; provider_type: ProviderType; @@ -135,8 +136,9 @@ export enum ProviderType { OPENAI = "openai", ANTHROPIC = "anthropic", VLLM = "vllm", - LLAMACPP = "llamacpp", OLLAMA = "ollama", + LM_STUDIO = "lm_studio", + LLAMACPP = "llamacpp", } /** @@ -216,9 +218,23 @@ export type V1AddProviderEndpointResponse = ProviderEndpoint; export type V1AddProviderEndpointError = HTTPValidationError; +export type V1ListAllModelsForAllProvidersResponse = Array; + +export type V1ListAllModelsForAllProvidersError = unknown; + +export type V1ListModelsByProviderData = { + path: { + provider_id: string; + }; +}; + +export type V1ListModelsByProviderResponse = Array; + +export type V1ListModelsByProviderError = HTTPValidationError; + export type V1GetProviderEndpointData = { path: { - provider_id: number; + provider_id: string; }; }; @@ -229,7 +245,7 @@ export type V1GetProviderEndpointError = HTTPValidationError; export type V1UpdateProviderEndpointData = { body: ProviderEndpoint; path: { - provider_id: number; + provider_id: string; }; }; @@ -239,7 +255,7 @@ export type V1UpdateProviderEndpointError = HTTPValidationError; export type V1DeleteProviderEndpointData = { path: { - provider_id: number; + provider_id: string; }; }; @@ -247,20 +263,6 @@ export type V1DeleteProviderEndpointResponse = unknown; export type V1DeleteProviderEndpointError = HTTPValidationError; -export type V1ListModelsByProviderData = { - path: { - provider_name: string; - }; -}; - -export type V1ListModelsByProviderResponse = Array; - -export type V1ListModelsByProviderError = HTTPValidationError; - -export type V1ListAllModelsForAllProvidersResponse = Array; - -export type V1ListAllModelsForAllProvidersError = unknown; - export type V1ListWorkspacesResponse = ListWorkspacesResponse; export type V1ListWorkspacesError = unknown; diff --git a/src/api/openapi.json b/src/api/openapi.json index 7c11f8c8..829cd2d0 100644 --- a/src/api/openapi.json +++ b/src/api/openapi.json @@ -8,7 +8,9 @@ "paths": { "/health": { "get": { - "tags": ["System"], + "tags": [ + "System" + ], "summary": "Health Check", "operationId": "health_check_health_get", "responses": { @@ -25,7 +27,10 @@ }, "/api/v1/provider-endpoints": { "get": { - "tags": ["CodeGate API", "Providers"], + "tags": [ + "CodeGate API", + "Providers" + ], "summary": "List Provider Endpoints", "description": "List all provider endpoints.", "operationId": "v1_list_provider_endpoints", @@ -75,7 +80,10 @@ } }, "post": { - "tags": ["CodeGate API", "Providers"], + "tags": [ + "CodeGate API", + "Providers" + ], "summary": "Add Provider Endpoint", "description": "Add a provider endpoint.", "operationId": "v1_add_provider_endpoint", @@ -113,19 +121,50 @@ } } }, - "/api/v1/provider-endpoints/{provider_id}": { + "/api/v1/provider-endpoints/models": { "get": { - "tags": ["CodeGate API", "Providers"], - "summary": "Get Provider Endpoint", - "description": "Get a provider endpoint by ID.", - "operationId": "v1_get_provider_endpoint", + "tags": [ + "CodeGate API", + "Providers" + ], + "summary": "List All Models For All Providers", + "description": "List all models for all providers.", + "operationId": "v1_list_all_models_for_all_providers", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/ModelByProvider" + }, + "type": "array", + "title": "Response V1 List All Models For All Providers" + } + } + } + } + } + } + }, + "/api/v1/provider-endpoints/{provider_id}/models": { + "get": { + "tags": [ + "CodeGate API", + "Providers" + ], + "summary": "List Models By Provider", + "description": "List models by provider.", + "operationId": "v1_list_models_by_provider", "parameters": [ { "name": "provider_id", "in": "path", "required": true, "schema": { - "type": "integer", + "type": "string", + "format": "uuid", "title": "Provider Id" } } @@ -136,7 +175,11 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ProviderEndpoint" + "type": "array", + "items": { + "$ref": "#/components/schemas/ModelByProvider" + }, + "title": "Response V1 List Models By Provider" } } } @@ -152,33 +195,29 @@ } } } - }, - "put": { - "tags": ["CodeGate API", "Providers"], - "summary": "Update Provider Endpoint", - "description": "Update a provider endpoint by ID.", - "operationId": "v1_update_provider_endpoint", + } + }, + "/api/v1/provider-endpoints/{provider_id}": { + "get": { + "tags": [ + "CodeGate API", + "Providers" + ], + "summary": "Get Provider Endpoint", + "description": "Get a provider endpoint by ID.", + "operationId": "v1_get_provider_endpoint", "parameters": [ { "name": "provider_id", "in": "path", "required": true, "schema": { - "type": "integer", + "type": "string", + "format": "uuid", "title": "Provider Id" } } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProviderEndpoint" - } - } - } - }, "responses": { "200": { "description": "Successful Response", @@ -202,28 +241,44 @@ } } }, - "delete": { - "tags": ["CodeGate API", "Providers"], - "summary": "Delete Provider Endpoint", - "description": "Delete a provider endpoint by id.", - "operationId": "v1_delete_provider_endpoint", + "put": { + "tags": [ + "CodeGate API", + "Providers" + ], + "summary": "Update Provider Endpoint", + "description": "Update a provider endpoint by ID.", + "operationId": "v1_update_provider_endpoint", "parameters": [ { "name": "provider_id", "in": "path", "required": true, "schema": { - "type": "integer", + "type": "string", + "format": "uuid", "title": "Provider Id" } } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProviderEndpoint" + } + } + } + }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { - "schema": {} + "schema": { + "$ref": "#/components/schemas/ProviderEndpoint" + } } } }, @@ -238,22 +293,24 @@ } } } - } - }, - "/api/v1/provider-endpoints/{provider_name}/models": { - "get": { - "tags": ["CodeGate API", "Providers"], - "summary": "List Models By Provider", - "description": "List models by provider.", - "operationId": "v1_list_models_by_provider", + }, + "delete": { + "tags": [ + "CodeGate API", + "Providers" + ], + "summary": "Delete Provider Endpoint", + "description": "Delete a provider endpoint by id.", + "operationId": "v1_delete_provider_endpoint", "parameters": [ { - "name": "provider_name", + "name": "provider_id", "in": "path", "required": true, "schema": { "type": "string", - "title": "Provider Name" + "format": "uuid", + "title": "Provider Id" } } ], @@ -262,13 +319,7 @@ "description": "Successful Response", "content": { "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ModelByProvider" - }, - "title": "Response V1 List Models By Provider" - } + "schema": {} } } }, @@ -285,33 +336,12 @@ } } }, - "/api/v1/provider-endpoints/models": { - "get": { - "tags": ["CodeGate API", "Providers"], - "summary": "List All Models For All Providers", - "description": "List all models for all providers.", - "operationId": "v1_list_all_models_for_all_providers", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/ModelByProvider" - }, - "type": "array", - "title": "Response V1 List All Models For All Providers" - } - } - } - } - } - } - }, "/api/v1/workspaces": { "get": { - "tags": ["CodeGate API", "Workspaces"], + "tags": [ + "CodeGate API", + "Workspaces" + ], "summary": "List Workspaces", "description": "List all workspaces.", "operationId": "v1_list_workspaces", @@ -329,7 +359,10 @@ } }, "post": { - "tags": ["CodeGate API", "Workspaces"], + "tags": [ + "CodeGate API", + "Workspaces" + ], "summary": "Create Workspace", "description": "Create a new workspace.", "operationId": "v1_create_workspace", @@ -369,7 +402,10 @@ }, "/api/v1/workspaces/active": { "get": { - "tags": ["CodeGate API", "Workspaces"], + "tags": [ + "CodeGate API", + "Workspaces" + ], "summary": "List Active Workspaces", "description": "List all active workspaces.\n\nIn it's current form, this function will only return one workspace. That is,\nthe globally active workspace.", "operationId": "v1_list_active_workspaces", @@ -387,7 +423,10 @@ } }, "post": { - "tags": ["CodeGate API", "Workspaces"], + "tags": [ + "CodeGate API", + "Workspaces" + ], "summary": "Activate Workspace", "description": "Activate a workspace by name.", "operationId": "v1_activate_workspace", @@ -436,7 +475,10 @@ }, "/api/v1/workspaces/{workspace_name}": { "delete": { - "tags": ["CodeGate API", "Workspaces"], + "tags": [ + "CodeGate API", + "Workspaces" + ], "summary": "Delete Workspace", "description": "Delete a workspace by name.", "operationId": "v1_delete_workspace", @@ -475,7 +517,10 @@ }, "/api/v1/workspaces/archive": { "get": { - "tags": ["CodeGate API", "Workspaces"], + "tags": [ + "CodeGate API", + "Workspaces" + ], "summary": "List Archived Workspaces", "description": "List all archived workspaces.", "operationId": "v1_list_archived_workspaces", @@ -495,7 +540,10 @@ }, "/api/v1/workspaces/archive/{workspace_name}/recover": { "post": { - "tags": ["CodeGate API", "Workspaces"], + "tags": [ + "CodeGate API", + "Workspaces" + ], "summary": "Recover Workspace", "description": "Recover an archived workspace by name.", "operationId": "v1_recover_workspace", @@ -529,7 +577,10 @@ }, "/api/v1/workspaces/archive/{workspace_name}": { "delete": { - "tags": ["CodeGate API", "Workspaces"], + "tags": [ + "CodeGate API", + "Workspaces" + ], "summary": "Hard Delete Workspace", "description": "Hard delete an archived workspace by name.", "operationId": "v1_hard_delete_workspace", @@ -568,7 +619,10 @@ }, "/api/v1/workspaces/{workspace_name}/alerts": { "get": { - "tags": ["CodeGate API", "Workspaces"], + "tags": [ + "CodeGate API", + "Workspaces" + ], "summary": "Get Workspace Alerts", "description": "Get alerts for a workspace.", "operationId": "v1_get_workspace_alerts", @@ -620,7 +674,10 @@ }, "/api/v1/workspaces/{workspace_name}/messages": { "get": { - "tags": ["CodeGate API", "Workspaces"], + "tags": [ + "CodeGate API", + "Workspaces" + ], "summary": "Get Workspace Messages", "description": "Get messages for a workspace.", "operationId": "v1_get_workspace_messages", @@ -665,7 +722,10 @@ }, "/api/v1/workspaces/{workspace_name}/custom-instructions": { "get": { - "tags": ["CodeGate API", "Workspaces"], + "tags": [ + "CodeGate API", + "Workspaces" + ], "summary": "Get Workspace Custom Instructions", "description": "Get the custom instructions of a workspace.", "operationId": "v1_get_workspace_custom_instructions", @@ -704,7 +764,10 @@ } }, "put": { - "tags": ["CodeGate API", "Workspaces"], + "tags": [ + "CodeGate API", + "Workspaces" + ], "summary": "Set Workspace Custom Instructions", "operationId": "v1_set_workspace_custom_instructions", "parameters": [ @@ -745,7 +808,10 @@ } }, "delete": { - "tags": ["CodeGate API", "Workspaces"], + "tags": [ + "CodeGate API", + "Workspaces" + ], "summary": "Delete Workspace Custom Instructions", "operationId": "v1_delete_workspace_custom_instructions", "parameters": [ @@ -778,7 +844,11 @@ }, "/api/v1/workspaces/{workspace_name}/muxes": { "get": { - "tags": ["CodeGate API", "Workspaces", "Muxes"], + "tags": [ + "CodeGate API", + "Workspaces", + "Muxes" + ], "summary": "Get Workspace Muxes", "description": "Get the mux rules of a workspace.\n\nThe list is ordered in order of priority. That is, the first rule in the list\nhas the highest priority.", "operationId": "v1_get_workspace_muxes", @@ -821,7 +891,11 @@ } }, "put": { - "tags": ["CodeGate API", "Workspaces", "Muxes"], + "tags": [ + "CodeGate API", + "Workspaces", + "Muxes" + ], "summary": "Set Workspace Muxes", "description": "Set the mux rules of a workspace.", "operationId": "v1_set_workspace_muxes", @@ -869,7 +943,10 @@ }, "/api/v1/alerts_notification": { "get": { - "tags": ["CodeGate API", "Dashboard"], + "tags": [ + "CodeGate API", + "Dashboard" + ], "summary": "Stream Sse", "description": "Send alerts event", "operationId": "v1_stream_sse", @@ -887,7 +964,10 @@ }, "/api/v1/version": { "get": { - "tags": ["CodeGate API", "Dashboard"], + "tags": [ + "CodeGate API", + "Dashboard" + ], "summary": "Version Check", "operationId": "v1_version_check", "responses": { @@ -904,7 +984,11 @@ }, "/api/v1/workspaces/{workspace_name}/token-usage": { "get": { - "tags": ["CodeGate API", "Workspaces", "Token Usage"], + "tags": [ + "CodeGate API", + "Workspaces", + "Token Usage" + ], "summary": "Get Workspace Token Usage", "description": "Get the token usage of a workspace.", "operationId": "v1_get_workspace_token_usage", @@ -954,7 +1038,9 @@ } }, "type": "object", - "required": ["name"], + "required": [ + "name" + ], "title": "ActivateWorkspaceRequest" }, "ActiveWorkspace": { @@ -972,7 +1058,11 @@ } }, "type": "object", - "required": ["name", "is_active", "last_updated"], + "required": [ + "name", + "is_active", + "last_updated" + ], "title": "ActiveWorkspace" }, "AlertConversation": { @@ -1059,7 +1149,11 @@ } }, "type": "object", - "required": ["message", "timestamp", "message_id"], + "required": [ + "message", + "timestamp", + "message_id" + ], "title": "ChatMessage", "description": "Represents a chat message." }, @@ -1100,7 +1194,11 @@ } }, "type": "object", - "required": ["code", "language", "filepath"], + "required": [ + "code", + "language", + "filepath" + ], "title": "CodeSnippet" }, "Conversation": { @@ -1177,7 +1275,9 @@ } }, "type": "object", - "required": ["name"], + "required": [ + "name" + ], "title": "CreateOrRenameWorkspaceRequest" }, "CustomInstructions": { @@ -1188,7 +1288,9 @@ } }, "type": "object", - "required": ["prompt"], + "required": [ + "prompt" + ], "title": "CustomInstructions" }, "HTTPValidationError": { @@ -1215,7 +1317,9 @@ } }, "type": "object", - "required": ["workspaces"], + "required": [ + "workspaces" + ], "title": "ListActiveWorkspacesResponse" }, "ListWorkspacesResponse": { @@ -1229,7 +1333,9 @@ } }, "type": "object", - "required": ["workspaces"], + "required": [ + "workspaces" + ], "title": "ListWorkspacesResponse" }, "ModelByProvider": { @@ -1238,19 +1344,30 @@ "type": "string", "title": "Name" }, - "provider": { + "provider_id": { "type": "string", - "title": "Provider" + "title": "Provider Id" + }, + "provider_name": { + "type": "string", + "title": "Provider Name" } }, "type": "object", - "required": ["name", "provider"], + "required": [ + "name", + "provider_id", + "provider_name" + ], "title": "ModelByProvider", "description": "Represents a model supported by a provider.\n\nNote that these are auto-discovered by the provider." }, "MuxMatcherType": { "type": "string", - "enum": ["file_regex", "catch_all"], + "enum": [ + "file_regex", + "catch_all" + ], "title": "MuxMatcherType", "description": "Represents the different types of matchers we support." }, @@ -1280,21 +1397,38 @@ } }, "type": "object", - "required": ["provider", "model", "matcher_type", "matcher"], + "required": [ + "provider", + "model", + "matcher_type", + "matcher" + ], "title": "MuxRule", "description": "Represents a mux rule for a provider." }, "ProviderAuthType": { "type": "string", - "enum": ["none", "passthrough", "api_key"], + "enum": [ + "none", + "passthrough", + "api_key" + ], "title": "ProviderAuthType", "description": "Represents the different types of auth we support for providers." }, "ProviderEndpoint": { "properties": { "id": { - "type": "integer", - "title": "Id" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Id", + "default": "" }, "name": { "type": "string", @@ -1317,13 +1451,25 @@ } }, "type": "object", - "required": ["id", "name", "provider_type", "endpoint", "auth_type"], + "required": [ + "name", + "provider_type", + "endpoint", + "auth_type" + ], "title": "ProviderEndpoint", "description": "Represents a provider's endpoint configuration. This\nallows us to persist the configuration for each provider,\nso we can use this for muxing messages." }, "ProviderType": { "type": "string", - "enum": ["openai", "anthropic", "vllm", "llamacpp", "ollama"], + "enum": [ + "openai", + "anthropic", + "vllm", + "ollama", + "lm_studio", + "llamacpp" + ], "title": "ProviderType", "description": "Represents the different types of providers we support." }, @@ -1344,13 +1490,19 @@ } }, "type": "object", - "required": ["question", "answer"], + "required": [ + "question", + "answer" + ], "title": "QuestionAnswer", "description": "Represents a question and answer pair." }, "QuestionType": { "type": "string", - "enum": ["chat", "fim"], + "enum": [ + "chat", + "fim" + ], "title": "QuestionType" }, "TokenUsage": { @@ -1394,7 +1546,10 @@ } }, "type": "object", - "required": ["tokens_by_model", "token_usage"], + "required": [ + "tokens_by_model", + "token_usage" + ], "title": "TokenUsageAggregate", "description": "Represents the tokens used. Includes the information of the tokens used by model.\n`used_tokens` are the total tokens used in the `tokens_by_model` list." }, @@ -1412,7 +1567,11 @@ } }, "type": "object", - "required": ["provider_type", "model", "token_usage"], + "required": [ + "provider_type", + "model", + "token_usage" + ], "title": "TokenUsageByModel", "description": "Represents the tokens used by a model." }, @@ -1442,7 +1601,11 @@ } }, "type": "object", - "required": ["loc", "msg", "type"], + "required": [ + "loc", + "msg", + "type" + ], "title": "ValidationError" }, "Workspace": { @@ -1457,7 +1620,10 @@ } }, "type": "object", - "required": ["name", "is_active"], + "required": [ + "name", + "is_active" + ], "title": "Workspace" } } diff --git a/src/components/SortableArea.tsx b/src/components/SortableArea.tsx new file mode 100644 index 00000000..b9623668 --- /dev/null +++ b/src/components/SortableArea.tsx @@ -0,0 +1,93 @@ +import { + closestCenter, + DndContext, + DragEndEvent, + KeyboardSensor, + PointerSensor, + UniqueIdentifier, + useSensor, + useSensors, +} from "@dnd-kit/core"; +import { + arrayMove, + SortableContext, + sortableKeyboardCoordinates, + verticalListSortingStrategy, +} from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { useSortable } from "@dnd-kit/sortable"; +import { GripVertical } from "lucide-react"; + +type Props = { + children: (item: T, index: number) => React.ReactNode; + + setItems: (items: T[]) => void; + items: T[]; +}; + +function ItemWrapper({ + children, + id, +}: { + children: React.ReactNode; + id: UniqueIdentifier; +}) { + const { attributes, listeners, setNodeRef, transform, transition } = + useSortable({ id }); + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; + return ( +
+
+ +
+
{children}
+
+ ); +} + +export function SortableArea({ + children, + setItems, + items, +}: Props) { + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }), + ); + + function handleDragEnd(event: DragEndEvent) { + const { active, over } = event; + + if (over == null) { + return; + } + + if (active.id !== over.id) { + const oldIndex = items.findIndex(({ id }) => id === active.id); + const newIndex = items.findIndex(({ id }) => id === over.id); + + setItems(arrayMove(items, oldIndex, newIndex)); + } + } + + return ( + + + {items.map((item, index) => ( + + {children(item, index)} + + ))} + + + ); +} diff --git a/src/features/workspace/components/__tests__/workspace-custom-instructions.tsx b/src/features/workspace/components/__tests__/workspace-custom-instructions.test.tsx similarity index 100% rename from src/features/workspace/components/__tests__/workspace-custom-instructions.tsx rename to src/features/workspace/components/__tests__/workspace-custom-instructions.test.tsx diff --git a/src/features/workspace/components/__tests__/workspace-model-overrides.test.tsx b/src/features/workspace/components/__tests__/workspace-model-overrides.test.tsx new file mode 100644 index 00000000..a236264b --- /dev/null +++ b/src/features/workspace/components/__tests__/workspace-model-overrides.test.tsx @@ -0,0 +1,142 @@ +import { render } from "@/lib/test-utils"; +import { screen, waitFor } from "@testing-library/react"; +import { WorkspaceModelOverrides } from "../workspace-model-overrides"; +import userEvent from "@testing-library/user-event"; + +test("render model overrides", () => { + render( + , + ); + expect(screen.getByText(/model overrides/i)).toBeVisible(); + expect( + screen.getByText( + /route to different large language models based on file type, individual files, or repository./i, + ), + ).toBeVisible(); + expect( + screen.getAllByRole("textbox", { name: /filter by/i }).length, + ).toBeGreaterThanOrEqual(1); + expect( + screen.getAllByRole("button", { name: /preferred model/i }).length, + ).toBeGreaterThanOrEqual(1); + + expect( + screen.getAllByPlaceholderText(/eg file type, file name, or repository/i) + .length, + ).toBeGreaterThanOrEqual(1); +}); + +test("submit model overrides", async () => { + render( + , + ); + + const filterByEl = screen.getAllByRole("textbox", { name: /filter by/i })[0]; + await userEvent.type(filterByEl as HTMLFormElement, "*.tsx"); + + const modelEl = screen.getAllByRole("button", { + name: /preferred model/i, + })[0]; + await userEvent.click(modelEl as HTMLFormElement); + + await userEvent.click( + screen.getByRole("option", { + name: "claude-3.5", + }), + ); + + await userEvent.click(screen.getByRole("button", { name: /save/i })); + + await waitFor(() => { + expect( + screen.getByText( + /model overrides on fake-workspace successfully submitted!/i, + ), + ); + }); +}); + +test("submit additional model overrides", async () => { + render( + , + ); + + expect(screen.getAllByRole("textbox", { name: /filter by/i }).length).toEqual( + 2, + ); + + await userEvent.click( + screen.getByRole("button", { name: /additional filter/i }), + ); + + const textFields = await screen.findAllByRole("textbox", { + name: /filter by/i, + }); + expect(textFields.length).toEqual(3); + expect(textFields[2]).toBeDefined(); + await userEvent.type(textFields[2] as HTMLFormElement, "*.ts"); + + await userEvent.click( + screen.getAllByRole("button", { + name: /preferred model/i, + })[2] as HTMLFormElement, + ); + + await userEvent.click( + screen.getByRole("option", { + name: "claude-3.5", + }), + ); + + await userEvent.click(screen.getByRole("button", { name: /save/i })); + + await waitFor(() => { + expect( + screen.getByText( + /model overrides on fake-workspace successfully submitted!/i, + ), + ); + }); +}); + +test("remove the first model override and submit", async () => { + render( + , + ); + + const textFields = screen.getAllByRole("textbox", { name: /filter by/i }); + expect(textFields.length).toEqual(3); + await userEvent.click( + screen.getAllByRole("button", { + name: /remove override/i, + })[0] as HTMLFormElement, + ); + + await waitFor(() => + expect( + screen.getAllByRole("textbox", { name: /filter by/i }).length, + ).toEqual(2), + ); + + await userEvent.click(screen.getByRole("button", { name: /save/i })); + + await waitFor(() => { + expect( + screen.getByText( + /model overrides on fake-workspace successfully submitted!/i, + ), + ); + }); +}); diff --git a/src/features/workspace/components/workspace-custom-instructions.tsx b/src/features/workspace/components/workspace-custom-instructions.tsx index d2387a8b..bb7d4cdd 100644 --- a/src/features/workspace/components/workspace-custom-instructions.tsx +++ b/src/features/workspace/components/workspace-custom-instructions.tsx @@ -213,7 +213,6 @@ export function WorkspaceCustomInstructions({ isPending={isMutationPending} isDisabled={Boolean(isArchived ?? isCustomInstructionsPending)} onPress={() => handleSubmit(value)} - variant="secondary" > Save diff --git a/src/features/workspace/components/workspace-model-overrides.tsx b/src/features/workspace/components/workspace-model-overrides.tsx new file mode 100644 index 00000000..626a2fc1 --- /dev/null +++ b/src/features/workspace/components/workspace-model-overrides.tsx @@ -0,0 +1,167 @@ +import { + Button, + Card, + CardBody, + CardFooter, + Form, + Label, + Text, +} from "@stacklok/ui-kit"; +import { twMerge } from "tailwind-merge"; +import { useMutationModelOverridesWorkspace } from "../hooks/use-mutation-model-overrides-workspace"; +import { MuxMatcherType } from "@/api/generated"; +import { FormEvent } from "react"; +import { Plus } from "@untitled-ui/icons-react"; +import { useModelOverridesWorkspace } from "../hooks/use-model-overrides-workspace"; +import { Input, Select, SelectButton, TextField } from "@stacklok/ui-kit"; +import { Trash01 } from "@untitled-ui/icons-react"; +import { OverrideRule } from "@/features/workspace/hooks/use-model-overrides-workspace"; +import { useModelsData } from "@/hooks/useModelsData"; + +import { SortableArea } from "@/components/SortableArea"; + +type SortableItemProps = { + index: number; + override: OverrideRule; +}; + +export function SortableItem({ override, index }: SortableItemProps) { + const { removeOverride, setOverrideItem } = useModelOverridesWorkspace(); + + const { data: models = [] } = useModelsData(); + + return ( +
+
+ event.preventDefault()} + value={override?.matcher ?? ""} + name="matcher" + onChange={(matcher) => { + setOverrideItem(override.id, { matcher }); + }} + > + + +
+
+ + {index !== 0 && ( + + )} +
+
+ ); +} + +export function WorkspaceModelOverrides({ + className, + workspaceName, + isArchived, +}: { + className?: string; + workspaceName: string; + isArchived: boolean | undefined; +}) { + const { overrides, addOverride, setOverrides, resetOverrides } = + useModelOverridesWorkspace(); + const { mutateAsync } = useMutationModelOverridesWorkspace(); + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + mutateAsync({ + path: { workspace_name: workspaceName }, + body: overrides.map(({ matcher, model, provider }) => ({ + matcher, + provider, + model, + matcher_type: MuxMatcherType.FILE_REGEX, + })), + }); + }; + + return ( +
+ + +
+ Model Overrides + + Route to different large language models based on file type, + individual files, or repository. + +
+
+
+
 
+
+ +
+
+ +
+
+ +
+ + {(override, index) => ( + + )} + +
+
+
+ + +
+ + +
+
+
+
+ ); +} diff --git a/src/features/workspace/components/workspace-name.tsx b/src/features/workspace/components/workspace-name.tsx index 68cf8c7c..513ac47b 100644 --- a/src/features/workspace/components/workspace-name.tsx +++ b/src/features/workspace/components/workspace-name.tsx @@ -71,7 +71,6 @@ export function WorkspaceName({ isDisabled={isArchived || name === ""} isPending={isPending} type="submit" - variant="secondary" > Save diff --git a/src/features/workspace/hooks/use-model-overrides-workspace.ts b/src/features/workspace/hooks/use-model-overrides-workspace.ts new file mode 100644 index 00000000..84e814a8 --- /dev/null +++ b/src/features/workspace/hooks/use-model-overrides-workspace.ts @@ -0,0 +1,80 @@ +import { MuxRule } from "@/api/generated"; +import { create } from "zustand"; +import { v4 as uuidv4 } from "uuid"; + +export type OverrideRule = Omit & { + id: string; +}; + +type State = { + removeOverride: (index: number) => void; + resetOverrides: () => void; + addOverride: () => void; + setOverrides: (overrides: OverrideRule[]) => void; + setOverrideItem: ( + idToChange: string, + { + model, + matcher, + }: { + matcher?: string; + model?: string; + }, + ) => void; + overrides: OverrideRule[]; +}; + +export const useModelOverridesWorkspace = create((set, get) => ({ + overrides: [ + { + id: uuidv4(), + provider: "anthropic", + model: "claude-3.5", + matcher: "", + }, + { + id: uuidv4(), + provider: "anthropic", + model: "claude-3.7", + matcher: "hello", + }, + ], + addOverride: () => { + const { overrides } = get(); + set({ + overrides: [ + ...overrides, + { id: uuidv4(), matcher: "", model: "", provider: "" }, + ], + }); + }, + resetOverrides: () => { + set({ + overrides: [{ id: uuidv4(), matcher: "", model: "", provider: "" }], + }); + }, + removeOverride: (overrideIndex: number) => { + const { overrides } = get(); + set({ + overrides: overrides.filter((_, index) => index !== overrideIndex), + }); + }, + setOverrides: (overrides: OverrideRule[]) => { + set({ overrides }); + }, + setOverrideItem: (idToChange, { model, matcher }) => { + const { overrides } = get(); + + set({ + overrides: overrides.map((item) => + item.id === idToChange + ? { + ...item, + model: model ?? item.model, + matcher: matcher ?? item.matcher, + } + : item, + ), + }); + }, +})); diff --git a/src/features/workspace/hooks/use-mutation-model-overrides-workspace.ts b/src/features/workspace/hooks/use-mutation-model-overrides-workspace.ts new file mode 100644 index 00000000..c6f0a738 --- /dev/null +++ b/src/features/workspace/hooks/use-mutation-model-overrides-workspace.ts @@ -0,0 +1,15 @@ +import { useToastMutation } from "@/hooks/use-toast-mutation"; +import { useInvalidateWorkspaceQueries } from "./use-invalidate-workspace-queries"; +import { v1SetWorkspaceMuxesMutation } from "@/api/generated/@tanstack/react-query.gen"; + +export function useMutationModelOverridesWorkspace() { + const invalidate = useInvalidateWorkspaceQueries(); + return useToastMutation({ + ...v1SetWorkspaceMuxesMutation(), + onSuccess: async () => { + await invalidate(); + }, + successMsg: (variables) => + `Model overrides on ${variables.path.workspace_name} successfully submitted!`, + }); +} diff --git a/src/hooks/useModelsData.ts b/src/hooks/useModelsData.ts new file mode 100644 index 00000000..b58fda4e --- /dev/null +++ b/src/hooks/useModelsData.ts @@ -0,0 +1,32 @@ +import { useQuery } from "@tanstack/react-query"; +import { v1ListAllModelsForAllProvidersOptions } from "@/api/generated/@tanstack/react-query.gen"; +import { V1ListAllModelsForAllProvidersResponse } from "@/api/generated"; + +export const useModelsData = () => { + return useQuery({ + ...v1ListAllModelsForAllProvidersOptions(), + queryFn: async () => { + const response: V1ListAllModelsForAllProvidersResponse = [ + { + name: "claude-3.5", + provider_name: "anthropic", + provider_id: "anthropic", + }, + { + name: "claude-3.6", + provider_name: "anthropic", + provider_id: "anthropic", + }, + { + name: "claude-3.7", + provider_name: "anthropic", + provider_id: "anthropic", + }, + { name: "chatgpt-4o", provider_name: "openai", provider_id: "openai" }, + { name: "chatgpt-4p", provider_name: "openai", provider_id: "openai" }, + ]; + + return response; + }, + }); +}; diff --git a/src/mocks/msw/handlers.ts b/src/mocks/msw/handlers.ts index 51e09f40..2b36a026 100644 --- a/src/mocks/msw/handlers.ts +++ b/src/mocks/msw/handlers.ts @@ -108,9 +108,21 @@ export const handlers = [ () => new HttpResponse(null, { status: 204 }), ), http.get("*/api/v1/provider-endpoints/:provider_name/models", () => - HttpResponse.json({ name: "dummy", provider: "dummy" }), + HttpResponse.json([ + { name: "claude-3.5", provider: "anthropic" }, + { name: "claude-3.6", provider: "anthropic" }, + { name: "claude-3.7", provider: "anthropic" }, + { name: "chatgpt-4o", provider: "openai" }, + { name: "chatgpt-4p", provider: "openai" }, + ]), ), http.get("*/api/v1/provider-endpoints/models", () => - HttpResponse.json({ name: "dummy", provider: "dummy" }), + HttpResponse.json([ + { name: "claude-3.5", provider: "anthropic" }, + { name: "claude-3.6", provider: "anthropic" }, + { name: "claude-3.7", provider: "anthropic" }, + { name: "chatgpt-4o", provider: "openai" }, + { name: "chatgpt-4p", provider: "openai" }, + ]), ), ]; diff --git a/src/routes/route-workspace.tsx b/src/routes/route-workspace.tsx index 968a3609..76ab8ca3 100644 --- a/src/routes/route-workspace.tsx +++ b/src/routes/route-workspace.tsx @@ -8,6 +8,7 @@ import { useParams } from "react-router-dom"; import { useArchivedWorkspaces } from "@/features/workspace/hooks/use-archived-workspaces"; import { useRestoreWorkspaceButton } from "@/features/workspace/hooks/use-restore-workspace-button"; import { WorkspaceCustomInstructions } from "@/features/workspace/components/workspace-custom-instructions"; +import { WorkspaceModelOverrides } from "@/features/workspace/components/workspace-model-overrides"; function WorkspaceArchivedBanner({ name }: { name: string }) { const restoreButtonProps = useRestoreWorkspaceButton({ workspaceName: name }); @@ -52,6 +53,11 @@ export function RouteWorkspace() { className="mb-4" workspaceName={name} /> +