diff --git a/src/Page.tsx b/src/Page.tsx index e2d4c30d..6854a284 100644 --- a/src/Page.tsx +++ b/src/Page.tsx @@ -29,7 +29,7 @@ export default function Page() { } /> }> - } /> + } /> } /> diff --git a/src/api/generated/@tanstack/react-query.gen.ts b/src/api/generated/@tanstack/react-query.gen.ts index 125f7497..e00cc4b7 100644 --- a/src/api/generated/@tanstack/react-query.gen.ts +++ b/src/api/generated/@tanstack/react-query.gen.ts @@ -24,6 +24,7 @@ import { v1ActivateWorkspace, v1UpdateWorkspace, v1DeleteWorkspace, + v1GetWorkspaceByName, v1ListArchivedWorkspaces, v1RecoverWorkspace, v1HardDeleteWorkspace, @@ -36,7 +37,6 @@ import { v1DeleteWorkspaceCustomInstructions, v1GetWorkspaceMuxes, v1SetWorkspaceMuxes, - v1ListWorkspacesByProvider, v1StreamSse, v1VersionCheck, v1GetWorkspaceTokenUsage, @@ -62,6 +62,7 @@ import type { V1ConfigureAuthMaterialData, V1ConfigureAuthMaterialError, V1ConfigureAuthMaterialResponse, + V1ListWorkspacesData, V1CreateWorkspaceData, V1CreateWorkspaceError, V1CreateWorkspaceResponse, @@ -74,6 +75,7 @@ import type { V1DeleteWorkspaceData, V1DeleteWorkspaceError, V1DeleteWorkspaceResponse, + V1GetWorkspaceByNameData, V1RecoverWorkspaceData, V1RecoverWorkspaceError, V1RecoverWorkspaceResponse, @@ -97,7 +99,6 @@ import type { V1SetWorkspaceMuxesData, V1SetWorkspaceMuxesError, V1SetWorkspaceMuxesResponse, - V1ListWorkspacesByProviderData, V1GetWorkspaceTokenUsageData, V1CreatePersonaData, V1CreatePersonaError, @@ -349,11 +350,13 @@ export const v1ConfigureAuthMaterialMutation = ( return mutationOptions } -export const v1ListWorkspacesQueryKey = (options?: OptionsLegacyParser) => [ - createQueryKey('v1ListWorkspaces', options), -] +export const v1ListWorkspacesQueryKey = ( + options?: OptionsLegacyParser +) => [createQueryKey('v1ListWorkspaces', options)] -export const v1ListWorkspacesOptions = (options?: OptionsLegacyParser) => { +export const v1ListWorkspacesOptions = ( + options?: OptionsLegacyParser +) => { return queryOptions({ queryFn: async ({ queryKey, signal }) => { const { data } = await v1ListWorkspaces({ @@ -511,6 +514,27 @@ export const v1DeleteWorkspaceMutation = ( return mutationOptions } +export const v1GetWorkspaceByNameQueryKey = ( + options: OptionsLegacyParser +) => [createQueryKey('v1GetWorkspaceByName', options)] + +export const v1GetWorkspaceByNameOptions = ( + options: OptionsLegacyParser +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await v1GetWorkspaceByName({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }) + return data + }, + queryKey: v1GetWorkspaceByNameQueryKey(options), + }) +} + export const v1ListArchivedWorkspacesQueryKey = ( options?: OptionsLegacyParser ) => [createQueryKey('v1ListArchivedWorkspaces', options)] @@ -867,27 +891,6 @@ export const v1SetWorkspaceMuxesMutation = ( return mutationOptions } -export const v1ListWorkspacesByProviderQueryKey = ( - options: OptionsLegacyParser -) => [createQueryKey('v1ListWorkspacesByProvider', options)] - -export const v1ListWorkspacesByProviderOptions = ( - options: OptionsLegacyParser -) => { - return queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await v1ListWorkspacesByProvider({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }) - return data - }, - queryKey: v1ListWorkspacesByProviderQueryKey(options), - }) -} - export const v1StreamSseQueryKey = (options?: OptionsLegacyParser) => [ createQueryKey('v1StreamSse', options), ] diff --git a/src/api/generated/sdk.gen.ts b/src/api/generated/sdk.gen.ts index 4f4f0553..9780b4f4 100644 --- a/src/api/generated/sdk.gen.ts +++ b/src/api/generated/sdk.gen.ts @@ -31,6 +31,7 @@ import type { V1ConfigureAuthMaterialData, V1ConfigureAuthMaterialError, V1ConfigureAuthMaterialResponse, + V1ListWorkspacesData, V1ListWorkspacesError, V1ListWorkspacesResponse, V1CreateWorkspaceData, @@ -47,6 +48,9 @@ import type { V1DeleteWorkspaceData, V1DeleteWorkspaceError, V1DeleteWorkspaceResponse, + V1GetWorkspaceByNameData, + V1GetWorkspaceByNameError, + V1GetWorkspaceByNameResponse, V1ListArchivedWorkspacesError, V1ListArchivedWorkspacesResponse, V1RecoverWorkspaceData, @@ -82,9 +86,6 @@ import type { V1SetWorkspaceMuxesData, V1SetWorkspaceMuxesError, V1SetWorkspaceMuxesResponse, - V1ListWorkspacesByProviderData, - V1ListWorkspacesByProviderError, - V1ListWorkspacesByProviderResponse, V1StreamSseError, V1StreamSseResponse, V1VersionCheckError, @@ -192,13 +193,13 @@ export const v1ListModelsByProvider = ( ThrowOnError >({ ...options, - url: '/api/v1/provider-endpoints/{provider_id}/models', + url: '/api/v1/provider-endpoints/{provider_name}/models', }) } /** * Get Provider Endpoint - * Get a provider endpoint by ID. + * Get a provider endpoint by name. */ export const v1GetProviderEndpoint = ( options: OptionsLegacyParser @@ -209,13 +210,13 @@ export const v1GetProviderEndpoint = ( ThrowOnError >({ ...options, - url: '/api/v1/provider-endpoints/{provider_id}', + url: '/api/v1/provider-endpoints/{provider_name}', }) } /** * Update Provider Endpoint - * Update a provider endpoint by ID. + * Update a provider endpoint by name. */ export const v1UpdateProviderEndpoint = ( options: OptionsLegacyParser @@ -226,13 +227,13 @@ export const v1UpdateProviderEndpoint = ( ThrowOnError >({ ...options, - url: '/api/v1/provider-endpoints/{provider_id}', + url: '/api/v1/provider-endpoints/{provider_name}', }) } /** * Delete Provider Endpoint - * Delete a provider endpoint by id. + * Delete a provider endpoint by name. */ export const v1DeleteProviderEndpoint = ( options: OptionsLegacyParser @@ -243,7 +244,7 @@ export const v1DeleteProviderEndpoint = ( ThrowOnError >({ ...options, - url: '/api/v1/provider-endpoints/{provider_id}', + url: '/api/v1/provider-endpoints/{provider_name}', }) } @@ -260,16 +261,24 @@ export const v1ConfigureAuthMaterial = ( ThrowOnError >({ ...options, - url: '/api/v1/provider-endpoints/{provider_id}/auth-material', + url: '/api/v1/provider-endpoints/{provider_name}/auth-material', }) } /** * List Workspaces * List all workspaces. + * + * Args: + * provider_name (Optional[str]): Filter workspaces by provider name. If provided, + * will return workspaces where models from the specified provider (e.g., OpenAI, + * Anthropic) have been used in workspace muxing rules. + * + * Returns: + * ListWorkspacesResponse: A response object containing the list of workspaces. */ export const v1ListWorkspaces = ( - options?: OptionsLegacyParser + options?: OptionsLegacyParser ) => { return (options?.client ?? client).get< V1ListWorkspacesResponse, @@ -369,6 +378,23 @@ export const v1DeleteWorkspace = ( }) } +/** + * Get Workspace By Name + * List workspaces by provider ID. + */ +export const v1GetWorkspaceByName = ( + options: OptionsLegacyParser +) => { + return (options?.client ?? client).get< + V1GetWorkspaceByNameResponse, + V1GetWorkspaceByNameError, + ThrowOnError + >({ + ...options, + url: '/api/v1/workspaces/{workspace_name}', + }) +} + /** * List Archived Workspaces * List all archived workspaces. @@ -591,25 +617,6 @@ export const v1SetWorkspaceMuxes = ( }) } -/** - * List Workspaces By Provider - * List workspaces by provider ID. - */ -export const v1ListWorkspacesByProvider = < - ThrowOnError extends boolean = false, ->( - options: OptionsLegacyParser -) => { - return (options?.client ?? client).get< - V1ListWorkspacesByProviderResponse, - V1ListWorkspacesByProviderError, - ThrowOnError - >({ - ...options, - url: '/api/v1/workspaces/{provider_id}', - }) -} - /** * Stream Sse * Send alerts event diff --git a/src/api/generated/types.gen.ts b/src/api/generated/types.gen.ts index 8f09345f..8dbeeb3c 100644 --- a/src/api/generated/types.gen.ts +++ b/src/api/generated/types.gen.ts @@ -171,7 +171,7 @@ export type ListWorkspacesResponse = { */ export type ModelByProvider = { name: string - provider_id: string + provider_type: ProviderType provider_name: string } @@ -200,8 +200,8 @@ export enum MuxMatcherType { * Represents a mux rule for a provider. */ export type MuxRule = { - provider_name?: string | null - provider_id: string + provider_name: string + provider_type: ProviderType model: string matcher_type: MuxMatcherType matcher?: string | null @@ -340,15 +340,6 @@ export type WorkspaceConfig_Output = { muxing_rules: Array } -/** - * Returns a workspace ID with model name - */ -export type WorkspaceWithModel = { - id: string - name: string - provider_model_name: string -} - export type HealthCheckHealthGetResponse = unknown export type HealthCheckHealthGetError = unknown @@ -377,7 +368,7 @@ export type V1ListAllModelsForAllProvidersError = unknown export type V1ListModelsByProviderData = { path: { - provider_id: string + provider_name: string } } @@ -387,7 +378,7 @@ export type V1ListModelsByProviderError = HTTPValidationError export type V1GetProviderEndpointData = { path: { - provider_id: string + provider_name: string } } @@ -398,7 +389,7 @@ export type V1GetProviderEndpointError = HTTPValidationError export type V1UpdateProviderEndpointData = { body: ProviderEndpoint path: { - provider_id: string + provider_name: string } } @@ -408,7 +399,7 @@ export type V1UpdateProviderEndpointError = HTTPValidationError export type V1DeleteProviderEndpointData = { path: { - provider_id: string + provider_name: string } } @@ -419,7 +410,7 @@ export type V1DeleteProviderEndpointError = HTTPValidationError export type V1ConfigureAuthMaterialData = { body: ConfigureAuthMaterial path: { - provider_id: string + provider_name: string } } @@ -427,9 +418,15 @@ export type V1ConfigureAuthMaterialResponse = void export type V1ConfigureAuthMaterialError = HTTPValidationError +export type V1ListWorkspacesData = { + query?: { + provider_name?: string | null + } +} + export type V1ListWorkspacesResponse = ListWorkspacesResponse -export type V1ListWorkspacesError = unknown +export type V1ListWorkspacesError = HTTPValidationError export type V1CreateWorkspaceData = { body: FullWorkspace_Input @@ -475,6 +472,16 @@ export type V1DeleteWorkspaceResponse = unknown export type V1DeleteWorkspaceError = HTTPValidationError +export type V1GetWorkspaceByNameData = { + path: { + workspace_name: string + } +} + +export type V1GetWorkspaceByNameResponse = FullWorkspace_Output + +export type V1GetWorkspaceByNameError = HTTPValidationError + export type V1ListArchivedWorkspacesResponse = ListWorkspacesResponse export type V1ListArchivedWorkspacesError = unknown @@ -598,16 +605,6 @@ export type V1SetWorkspaceMuxesResponse = void export type V1SetWorkspaceMuxesError = HTTPValidationError -export type V1ListWorkspacesByProviderData = { - path: { - provider_id: string - } -} - -export type V1ListWorkspacesByProviderResponse = Array - -export type V1ListWorkspacesByProviderError = HTTPValidationError - export type V1StreamSseResponse = unknown export type V1StreamSseError = unknown diff --git a/src/api/openapi.json b/src/api/openapi.json index 9cd9c62a..ae50d9a7 100644 --- a/src/api/openapi.json +++ b/src/api/openapi.json @@ -137,7 +137,7 @@ } } }, - "/api/v1/provider-endpoints/{provider_id}/models": { + "/api/v1/provider-endpoints/{provider_name}/models": { "get": { "tags": ["CodeGate API", "Providers"], "summary": "List Models By Provider", @@ -145,13 +145,12 @@ "operationId": "v1_list_models_by_provider", "parameters": [ { - "name": "provider_id", + "name": "provider_name", "in": "path", "required": true, "schema": { "type": "string", - "format": "uuid", - "title": "Provider Id" + "title": "Provider Name" } } ], @@ -183,21 +182,20 @@ } } }, - "/api/v1/provider-endpoints/{provider_id}": { + "/api/v1/provider-endpoints/{provider_name}": { "get": { "tags": ["CodeGate API", "Providers"], "summary": "Get Provider Endpoint", - "description": "Get a provider endpoint by ID.", + "description": "Get a provider endpoint by name.", "operationId": "v1_get_provider_endpoint", "parameters": [ { - "name": "provider_id", + "name": "provider_name", "in": "path", "required": true, "schema": { "type": "string", - "format": "uuid", - "title": "Provider Id" + "title": "Provider Name" } } ], @@ -227,17 +225,16 @@ "put": { "tags": ["CodeGate API", "Providers"], "summary": "Update Provider Endpoint", - "description": "Update a provider endpoint by ID.", + "description": "Update a provider endpoint by name.", "operationId": "v1_update_provider_endpoint", "parameters": [ { - "name": "provider_id", + "name": "provider_name", "in": "path", "required": true, "schema": { "type": "string", - "format": "uuid", - "title": "Provider Id" + "title": "Provider Name" } } ], @@ -277,17 +274,16 @@ "delete": { "tags": ["CodeGate API", "Providers"], "summary": "Delete Provider Endpoint", - "description": "Delete a provider endpoint by id.", + "description": "Delete a provider endpoint by name.", "operationId": "v1_delete_provider_endpoint", "parameters": [ { - "name": "provider_id", + "name": "provider_name", "in": "path", "required": true, "schema": { "type": "string", - "format": "uuid", - "title": "Provider Id" + "title": "Provider Name" } } ], @@ -313,7 +309,7 @@ } } }, - "/api/v1/provider-endpoints/{provider_id}/auth-material": { + "/api/v1/provider-endpoints/{provider_name}/auth-material": { "put": { "tags": ["CodeGate API", "Providers"], "summary": "Configure Auth Material", @@ -321,13 +317,12 @@ "operationId": "v1_configure_auth_material", "parameters": [ { - "name": "provider_id", + "name": "provider_name", "in": "path", "required": true, "schema": { "type": "string", - "format": "uuid", - "title": "Provider Id" + "title": "Provider Name" } } ], @@ -362,8 +357,26 @@ "get": { "tags": ["CodeGate API", "Workspaces"], "summary": "List Workspaces", - "description": "List all workspaces.", + "description": "List all workspaces.\n\nArgs:\n provider_name (Optional[str]): Filter workspaces by provider name. If provided,\n will return workspaces where models from the specified provider (e.g., OpenAI,\n Anthropic) have been used in workspace muxing rules.\n\nReturns:\n ListWorkspacesResponse: A response object containing the list of workspaces.", "operationId": "v1_list_workspaces", + "parameters": [ + { + "name": "provider_name", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Provider Name" + } + } + ], "responses": { "200": { "description": "Successful Response", @@ -374,6 +387,16 @@ } } } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } } } }, @@ -383,14 +406,14 @@ "description": "Create a new workspace.", "operationId": "v1_create_workspace", "requestBody": { + "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FullWorkspace-Input" } } - }, - "required": true + } }, "responses": { "201": { @@ -511,7 +534,7 @@ } }, "responses": { - "201": { + "200": { "description": "Successful Response", "content": { "application/json": { @@ -569,6 +592,45 @@ } } } + }, + "get": { + "tags": ["CodeGate API", "Workspaces"], + "summary": "Get Workspace By Name", + "description": "List workspaces by provider ID.", + "operationId": "v1_get_workspace_by_name", + "parameters": [ + { + "name": "workspace_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Workspace Name" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FullWorkspace-Output" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } } }, "/api/v1/workspaces/archive": { @@ -1113,52 +1175,6 @@ } } }, - "/api/v1/workspaces/{provider_id}": { - "get": { - "tags": ["CodeGate API", "Workspaces"], - "summary": "List Workspaces By Provider", - "description": "List workspaces by provider ID.", - "operationId": "v1_list_workspaces_by_provider", - "parameters": [ - { - "name": "provider_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "title": "Provider Id" - } - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/WorkspaceWithModel" - }, - "title": "Response V1 List Workspaces By Provider" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, "/api/v1/alerts_notification": { "get": { "tags": ["CodeGate API", "Dashboard"], @@ -1989,9 +2005,8 @@ "type": "string", "title": "Name" }, - "provider_id": { - "type": "string", - "title": "Provider Id" + "provider_type": { + "$ref": "#/components/schemas/ProviderType" }, "provider_name": { "type": "string", @@ -1999,7 +2014,7 @@ } }, "type": "object", - "required": ["name", "provider_id", "provider_name"], + "required": ["name", "provider_type", "provider_name"], "title": "ModelByProvider", "description": "Represents a model supported by a provider.\n\nNote that these are auto-discovered by the provider." }, @@ -2017,19 +2032,11 @@ "MuxRule": { "properties": { "provider_name": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], + "type": "string", "title": "Provider Name" }, - "provider_id": { - "type": "string", - "title": "Provider Id" + "provider_type": { + "$ref": "#/components/schemas/ProviderType" }, "model": { "type": "string", @@ -2051,7 +2058,7 @@ } }, "type": "object", - "required": ["provider_id", "model", "matcher_type"], + "required": ["provider_name", "provider_type", "model", "matcher_type"], "title": "MuxRule", "description": "Represents a mux rule for a provider." }, @@ -2362,27 +2369,6 @@ "type": "object", "required": ["custom_instructions", "muxing_rules"], "title": "WorkspaceConfig" - }, - "WorkspaceWithModel": { - "properties": { - "id": { - "type": "string", - "title": "Id" - }, - "name": { - "type": "string", - "pattern": "^[a-zA-Z0-9_-]+$", - "title": "Name" - }, - "provider_model_name": { - "type": "string", - "title": "Provider Model Name" - } - }, - "type": "object", - "required": ["id", "name", "provider_model_name"], - "title": "WorkspaceWithModel", - "description": "Returns a workspace ID with model name" } } } diff --git a/src/context/confirm-context.tsx b/src/context/confirm-context.tsx index 8509ce62..34c30e25 100644 --- a/src/context/confirm-context.tsx +++ b/src/context/confirm-context.tsx @@ -57,8 +57,12 @@ export function ConfirmProvider({ children }: { children: ReactNode }) { {children} - - + + {activeQuestion?.config.title} @@ -70,6 +74,7 @@ export function ConfirmProvider({ children }: { children: ReactNode }) { {activeQuestion?.config.buttons.no ?? ' '} + ) +} diff --git a/src/features/workspace/components/workspace-models-dropdown.tsx b/src/features/workspace/components/workspace-models-dropdown.tsx index 23c8c71e..b9c74db3 100644 --- a/src/features/workspace/components/workspace-models-dropdown.tsx +++ b/src/features/workspace/components/workspace-models-dropdown.tsx @@ -1,6 +1,5 @@ import { ModelByProvider, - MuxRule, V1ListAllModelsForAllProvidersResponse, } from '@/api/generated' import { @@ -16,17 +15,21 @@ import { import { ChevronDown, SearchMd } from '@untitled-ui/icons-react' import { map, groupBy } from 'lodash' import { useState } from 'react' +import { deserializeMuxModel, serializeMuxModel } from '../lib/mux-model-serde' +import { PreferredMuxRule } from '../hooks/use-muxing-rules-form-workspace' type Props = { - rule: MuxRule & { id: string } + rule: PreferredMuxRule isArchived: boolean models: V1ListAllModelsForAllProvidersResponse onChange: ({ model, - provider_id, + provider_name, + provider_type, }: { model: string - provider_id: string + provider_name: string + provider_type: string }) => void } @@ -37,7 +40,7 @@ function groupModelsByProviderName( id: providerName, textValue: providerName, items: items.map((item) => ({ - id: `${item.provider_id}@${item.name}`, + id: serializeMuxModel(item), textValue: item.name, })), })) @@ -77,12 +80,20 @@ export function WorkspaceModelsDropdown({ const [isOpen, setIsOpen] = useState(false) const [searchItem, setSearchItem] = useState('') const groupedModels = groupModelsByProviderName(models) - const currentProvider = models.find((p) => p.provider_id === rule.provider_id) + const currentProvider = models.find( + (p) => p.provider_name === rule.provider_name + ) const currentModel = currentProvider && rule.model ? `${currentProvider?.provider_name}/${rule.model}` : '' - const selectedKey = `${rule.provider_id}/${rule.model}` + const selectedKey = rule.provider_type + ? serializeMuxModel({ + name: rule.model, + provider_name: rule.provider_name, + provider_type: rule.provider_type, + }) + : undefined return (
@@ -110,19 +121,14 @@ export function WorkspaceModelsDropdown({ selectionBehavior="replace" selectedKeys={selectedKey ? [selectedKey] : []} onSelectionChange={(v) => { - if (v === 'all') { - return - } - const selectedValue = v.values().next().value - if (!selectedValue && typeof selectedValue !== 'string') return - if (typeof selectedValue === 'string') { - const atIndex = selectedValue.indexOf('@') - const provider_id = selectedValue.slice(0, atIndex) - const modelName = selectedValue.slice(atIndex + 1) - if (atIndex === -1 && (!provider_id || !modelName)) return + if (v === 'all') return + const id = v.values().next().value?.toString() + if (typeof id === 'string') { + const model = deserializeMuxModel(id) onChange({ - model: modelName, - provider_id, + model: model.name, + provider_name: model.provider_name, + provider_type: model.provider_type, }) setIsOpen(false) } diff --git a/src/features/workspace/components/workspace-muxing-model.tsx b/src/features/workspace/components/workspace-muxing-model.tsx index feb800cf..d6dcfcf5 100644 --- a/src/features/workspace/components/workspace-muxing-model.tsx +++ b/src/features/workspace/components/workspace-muxing-model.tsx @@ -19,7 +19,10 @@ import { } from '@stacklok/ui-kit' import { twMerge } from 'tailwind-merge' import { useMutationPreferredModelWorkspace } from '../hooks/use-mutation-preferred-model-workspace' -import { V1ListAllModelsForAllProvidersResponse } from '@/api/generated' +import { + ProviderType, + V1ListAllModelsForAllProvidersResponse, +} from '@/api/generated' import { FormEvent } from 'react' import { LayersThree01, @@ -37,6 +40,7 @@ import { } from '../hooks/use-muxing-rules-form-workspace' import { FormButtons } from '@/components/FormButtons' import { getRuleData, isRequestType } from '../lib/utils' +import { z } from 'zod' function MissingProviderBanner() { return ( @@ -120,9 +124,15 @@ function SortableItem({ rule={rule} isArchived={isArchived} models={models} - onChange={({ model, provider_id }) => - setRuleItem({ ...rule, provider_id, model }) - } + onChange={({ model, provider_name, provider_type }) => { + if (provider_type === undefined) return + setRuleItem({ + ...rule, + provider_name, + provider_type: z.nativeEnum(ProviderType).parse(provider_type), + model, + }) + }} /> {showRemoveButton && !isDefaultRule ? ( + ) +} diff --git a/src/features/workspace/hooks/use-invalidate-workspace-queries.ts b/src/features/workspace/hooks/use-invalidate-workspace-queries.ts index 58957dc6..dd131f42 100644 --- a/src/features/workspace/hooks/use-invalidate-workspace-queries.ts +++ b/src/features/workspace/hooks/use-invalidate-workspace-queries.ts @@ -1,7 +1,7 @@ import { + v1GetWorkspaceByNameQueryKey, v1GetWorkspaceMuxesQueryKey, v1ListArchivedWorkspacesQueryKey, - v1ListWorkspacesByProviderQueryKey, v1ListWorkspacesQueryKey, } from '@/api/generated/@tanstack/react-query.gen' import { invalidateQueries } from '@/lib/react-query-utils' @@ -16,7 +16,7 @@ export function useInvalidateWorkspaceQueries() { v1ListWorkspacesQueryKey, v1ListArchivedWorkspacesQueryKey, v1GetWorkspaceMuxesQueryKey, - v1ListWorkspacesByProviderQueryKey, + v1GetWorkspaceByNameQueryKey, ]) }, [queryClient]) diff --git a/src/features/workspace/hooks/use-mutation-set-workspace-custom-instructions.tsx b/src/features/workspace/hooks/use-mutation-set-workspace-custom-instructions.tsx index af5f86e9..7c9bce0d 100644 --- a/src/features/workspace/hooks/use-mutation-set-workspace-custom-instructions.tsx +++ b/src/features/workspace/hooks/use-mutation-set-workspace-custom-instructions.tsx @@ -1,5 +1,7 @@ import { + v1GetWorkspaceByNameQueryKey, v1GetWorkspaceCustomInstructionsQueryKey, + v1GetWorkspaceMuxesQueryKey, v1SetWorkspaceCustomInstructionsMutation, } from '@/api/generated/@tanstack/react-query.gen' import { V1GetWorkspaceCustomInstructionsData } from '@/api/generated' @@ -16,7 +18,9 @@ export function useMutationSetWorkspaceCustomInstructions( ...v1SetWorkspaceCustomInstructionsMutation(options), onSuccess: () => invalidateQueries(queryClient, [ + v1GetWorkspaceMuxesQueryKey, v1GetWorkspaceCustomInstructionsQueryKey, + v1GetWorkspaceByNameQueryKey, ]), successMsg: 'Successfully updated custom instructions', }) diff --git a/src/features/workspace/hooks/use-mutation-update-workspace.ts b/src/features/workspace/hooks/use-mutation-update-workspace.ts index 5ae08b88..d168f7c2 100644 --- a/src/features/workspace/hooks/use-mutation-update-workspace.ts +++ b/src/features/workspace/hooks/use-mutation-update-workspace.ts @@ -1,4 +1,5 @@ import { + v1GetWorkspaceByNameQueryKey, v1GetWorkspaceCustomInstructionsQueryKey, v1GetWorkspaceMuxesQueryKey, v1UpdateWorkspaceMutation, @@ -20,6 +21,7 @@ export function useMutationUpdateWorkspace() { queryKeyFns: [ v1GetWorkspaceMuxesQueryKey, v1GetWorkspaceCustomInstructionsQueryKey, + v1GetWorkspaceByNameQueryKey, ], }) await invalidate() diff --git a/src/features/workspace/hooks/use-muxing-rules-form-workspace.ts b/src/features/workspace/hooks/use-muxing-rules-form-workspace.ts index 2949c31a..5ee0e0c8 100644 --- a/src/features/workspace/hooks/use-muxing-rules-form-workspace.ts +++ b/src/features/workspace/hooks/use-muxing-rules-form-workspace.ts @@ -1,10 +1,12 @@ -import { MuxMatcherType, MuxRule } from '@/api/generated' +import { MuxMatcherType, MuxRule, ProviderType } from '@/api/generated' import { useFormState } from '@/hooks/useFormState' import { isEqual } from 'lodash' import { useCallback, useEffect, useRef } from 'react' import { v4 as uuidv4 } from 'uuid' -export type PreferredMuxRule = MuxRule & { id: string } +export type PreferredMuxRule = Omit & { + provider_type: ProviderType | undefined +} & { id: string } type MuxingRulesFormState = { rules: PreferredMuxRule[] @@ -12,7 +14,8 @@ type MuxingRulesFormState = { const DEFAULT_STATE: PreferredMuxRule = { id: uuidv4(), - provider_id: '', + provider_name: '', + provider_type: undefined, model: '', matcher: '', matcher_type: MuxMatcherType.CATCH_ALL, diff --git a/src/features/workspace/hooks/use-query-get-workspace-by-name.ts b/src/features/workspace/hooks/use-query-get-workspace-by-name.ts new file mode 100644 index 00000000..7aeb0a24 --- /dev/null +++ b/src/features/workspace/hooks/use-query-get-workspace-by-name.ts @@ -0,0 +1,12 @@ +import { v1GetWorkspaceByNameOptions } from '@/api/generated/@tanstack/react-query.gen' +import { useQuery } from '@tanstack/react-query' + +export function useQueryGetWorkspaceByName(options: { + path: { + workspace_name: string + } +}) { + return useQuery({ + ...v1GetWorkspaceByNameOptions(options), + }) +} diff --git a/src/features/workspace/hooks/use-query-muxing-rules-workspace.ts b/src/features/workspace/hooks/use-query-muxing-rules-workspace.ts index b42d0fba..c7c8551c 100644 --- a/src/features/workspace/hooks/use-query-muxing-rules-workspace.ts +++ b/src/features/workspace/hooks/use-query-muxing-rules-workspace.ts @@ -1,20 +1,24 @@ -import { V1GetWorkspaceMuxesData } from "@/api/generated"; -import { v1GetWorkspaceMuxesOptions } from "@/api/generated/@tanstack/react-query.gen"; -import { useQuery } from "@tanstack/react-query"; -import { useMemo } from "react"; +import { V1GetWorkspaceMuxesData } from '@/api/generated' +import { v1GetWorkspaceMuxesOptions } from '@/api/generated/@tanstack/react-query.gen' +import { getQueryCacheConfig } from '@/lib/react-query-utils' +import { useQuery } from '@tanstack/react-query' +import { useMemo } from 'react' export const useQueryMuxingRulesWorkspace = (workspace_name: string) => { const options: V1GetWorkspaceMuxesData & - Omit = useMemo( + Omit = useMemo( () => ({ path: { workspace_name }, }), - [workspace_name], - ); + [workspace_name] + ) const { data = [], ...rest } = useQuery({ ...v1GetWorkspaceMuxesOptions(options), - }); + ...getQueryCacheConfig('no-cache'), + // eslint-disable-next-line no-restricted-syntax + refetchOnMount: true, + }) - return { data: data, ...rest }; -}; + return { data: data, ...rest } +} diff --git a/src/features/workspace/lib/__tests__/mux-model-serde.test.ts b/src/features/workspace/lib/__tests__/mux-model-serde.test.ts new file mode 100644 index 00000000..49fdbe90 --- /dev/null +++ b/src/features/workspace/lib/__tests__/mux-model-serde.test.ts @@ -0,0 +1,35 @@ +import { describe, it, expect } from 'vitest' +import { serializeMuxModel, deserializeMuxModel } from '../mux-model-serde' +import { ModelByProvider } from '@/api/generated' + +describe('mux-model serialization/deserialization', () => { + it.each([ + { + name: 'deepseek-r1:1.5b', + provider_type: 'ollama', + provider_name: 'ollama', + }, + { + name: 'mistral-nemo:latest', + provider_type: 'ollama', + provider_name: 'ollama_muxing', + }, + { + name: '01-ai/yi-large', + provider_type: 'openrouter', + provider_name: 'openrouter_muxing', + }, + { + name: 'anthropic/claude-3-opus:beta', + provider_type: 'openrouter', + provider_name: 'openrouter_muxing', + }, + ] as ModelByProvider[])( + 'should correctly serialize and deserialize model: $name', + (model) => { + const serialized = serializeMuxModel(model) + const deserialized = deserializeMuxModel(serialized) + expect(deserialized).toEqual(model) + } + ) +}) diff --git a/src/features/workspace/lib/mux-model-serde.ts b/src/features/workspace/lib/mux-model-serde.ts new file mode 100644 index 00000000..fc0b7f63 --- /dev/null +++ b/src/features/workspace/lib/mux-model-serde.ts @@ -0,0 +1,17 @@ +import { ModelByProvider, ProviderType } from '@/api/generated' +import { z } from 'zod' + +export function serializeMuxModel(model: ModelByProvider): string { + return `${model.provider_name}___${model.provider_type}___${model.name}` +} + +export function deserializeMuxModel(str: string): ModelByProvider { + const [provider_name, provider_type, name] = str.split('___') + if (!provider_name || !provider_type || !name) + throw new Error('Invalid model') + return { + provider_name, + provider_type: z.nativeEnum(ProviderType).parse(provider_type), + name, + } +} diff --git a/src/hooks/use-query-list-all-models-for-all-providers.ts b/src/hooks/use-query-list-all-models-for-all-providers.ts index dd3d5995..b6f70a5f 100644 --- a/src/hooks/use-query-list-all-models-for-all-providers.ts +++ b/src/hooks/use-query-list-all-models-for-all-providers.ts @@ -1,8 +1,12 @@ import { useQuery } from '@tanstack/react-query' import { v1ListAllModelsForAllProvidersOptions } from '@/api/generated/@tanstack/react-query.gen' +import { getQueryCacheConfig } from '@/lib/react-query-utils' export const useQueryListAllModelsForAllProviders = () => { return useQuery({ ...v1ListAllModelsForAllProvidersOptions(), + ...getQueryCacheConfig('no-cache'), + // eslint-disable-next-line no-restricted-syntax + refetchOnMount: true, }) } diff --git a/src/mocks/msw/fixtures/GET_PROVIDERS_MODELS.json b/src/mocks/msw/fixtures/GET_PROVIDERS_MODELS.json index 7be5f209..2a273c8a 100644 --- a/src/mocks/msw/fixtures/GET_PROVIDERS_MODELS.json +++ b/src/mocks/msw/fixtures/GET_PROVIDERS_MODELS.json @@ -1,27 +1,27 @@ [ { "name": "claude-3.5", - "provider_id": "id_1", + "provider_type": "openrouter", "provider_name": "anthropic" }, { "name": "claude-3.6", - "provider_id": "id_2", + "provider_type": "openrouter", "provider_name": "anthropic" }, { "name": "claude-3.7", - "provider_id": "id_3", + "provider_type": "openrouter", "provider_name": "anthropic" }, { "name": "chatgpt-4o", - "provider_id": "id_4", + "provider_type": "openrouter", "provider_name": "openai" }, { "name": "chatgpt-4p", - "provider_id": "id_5", + "provider_type": "openrouter", "provider_name": "openai" } ] diff --git a/src/mocks/msw/handlers.ts b/src/mocks/msw/handlers.ts index 408ba2b1..cde7ca82 100644 --- a/src/mocks/msw/handlers.ts +++ b/src/mocks/msw/handlers.ts @@ -81,6 +81,15 @@ export const handlers = [ { status: 201 } ) ), + http.get(mswEndpoint('/api/v1/workspaces/:workspace_name'), () => + HttpResponse.json({ + name: 'foo', + config: { + custom_instructions: '', + muxing_rules: [], + }, + }) + ), http.post( mswEndpoint('/api/v1/workspaces/archive/:workspace_name/recover'), () => new HttpResponse(null, { status: 204 }) @@ -135,13 +144,14 @@ export const handlers = [ mswEndpoint('/api/v1/workspaces/:workspace_name/muxes'), () => new HttpResponse(null, { status: 204 }) ), - http.get(mswEndpoint('/api/v1/provider-endpoints/:provider_id/models'), () => - HttpResponse.json(mockedProvidersModels) + http.get( + mswEndpoint('/api/v1/provider-endpoints/:provider_name/models'), + () => HttpResponse.json(mockedProvidersModels) ), http.get(mswEndpoint('/api/v1/provider-endpoints/models'), () => HttpResponse.json(mockedProvidersModels) ), - http.get(mswEndpoint('/api/v1/provider-endpoints/:provider_id'), () => + http.get(mswEndpoint('/api/v1/provider-endpoints/:provider_name'), () => HttpResponse.json(mockedProviders[0]) ), http.get(mswEndpoint('/api/v1/provider-endpoints'), () => diff --git a/src/routes/route-provider-update.tsx b/src/routes/route-provider-update.tsx index c286e0f5..2f756c4f 100644 --- a/src/routes/route-provider-update.tsx +++ b/src/routes/route-provider-update.tsx @@ -7,16 +7,16 @@ import { DialogContent, Form } from '@stacklok/ui-kit' import { useParams } from 'react-router-dom' export function RouteProviderUpdate() { - const { id } = useParams() - if (id === undefined) { - throw new Error('Provider id is required') + const { name } = useParams() + if (name === undefined) { + throw new Error('Provider name is required') } - const { setProvider, provider } = useProvider(id) + const { setProvider, provider } = useProvider(name) const { mutateAsync } = useMutationUpdateProvider() const handleSubmit = (event: React.FormEvent) => { event.preventDefault() - mutateAsync(provider) + mutateAsync({ ...provider, oldName: name }) } // TODO add empty state and loading in a next step diff --git a/src/routes/route-workspace.tsx b/src/routes/route-workspace.tsx index d5df90ed..6453768f 100644 --- a/src/routes/route-workspace.tsx +++ b/src/routes/route-workspace.tsx @@ -10,6 +10,7 @@ import { WorkspaceCustomInstructions } from '@/features/workspace/components/wor import { WorkspaceMuxingModel } from '@/features/workspace/components/workspace-muxing-model' import { PageContainer } from '@/components/page-container' import { WorkspaceActivateButton } from '@/features/workspace/components/workspace-activate-button' +import { WorkspaceDownloadButton } from '@/features/workspace/components/workspace-download-button' function WorkspaceArchivedBanner({ name }: { name: string }) { const restoreButtonProps = useRestoreWorkspaceButton({ workspaceName: name }) @@ -46,7 +47,13 @@ export function RouteWorkspace() { - +
+ + +
{isArchived ? : null} diff --git a/src/routes/route-workspaces.tsx b/src/routes/route-workspaces.tsx index b4f21b7d..a5001364 100644 --- a/src/routes/route-workspaces.tsx +++ b/src/routes/route-workspaces.tsx @@ -14,6 +14,7 @@ import { useNavigate } from 'react-router-dom' import { hrefs } from '@/lib/hrefs' import { PlusSquare } from '@untitled-ui/icons-react' import { PageContainer } from '@/components/page-container' +import { WorkspaceUploadButton } from '@/features/workspace/components/workspace-upload-button' export function RouteWorkspaces() { const navigate = useNavigate() @@ -28,15 +29,20 @@ export function RouteWorkspaces() { - - - Create - - - Create a new workspace - C - - +
+ + + + Create + + + + Create a new workspace + + C + + +