diff --git a/docs/cli-agents/claude-code.mdx b/docs/cli-agents/claude-code.mdx index 4c2ba7463e..8f9d6e5078 100644 --- a/docs/cli-agents/claude-code.mdx +++ b/docs/cli-agents/claude-code.mdx @@ -31,6 +31,42 @@ Claude Code supports multiple authentication methods. Choose the one that matche claude ``` +## Setup with the VS Code extension + +If you prefer using Claude Code inside VS Code, install the [Claude Code extension](https://marketplace.visualstudio.com/items?itemName=anthropic.claude-code) and point it to Bifrost using the same environment variables you use for the CLI. + +### 1. Install the extension + +1. Open **Extensions** in VS Code (`Cmd+Shift+X` on macOS) +2. Search for **Claude Code** +3. Install **Claude Code** by Anthropic + +### 2. Configure Bifrost endpoint + +Add Bifrost variables in your editor settings: + +```json +{ + "claudeCode.environmentVariables": [ + { + "name": "ANTHROPIC_BASE_URL", + "value": "http://localhost:8080/anthropic" + }, + { + "name": "ANTHROPIC_DEFAULT_SONNET_MODEL", + "value": "vertex/claude-sonnet-4-6" + }, + { + "name": "ANTHROPIC_DEFAULT_HAIKU_MODEL", + "value": "vertex/claude-haiku-4-5" + } + ] +} +``` + +If virtual keys are enabled, also add `ANTHROPIC_API_KEY` in the same list (use your Bifrost virtual key value). If not, set it to `dummy`. + + ## Amazon Bedrock via Bifrost ### Setup diff --git a/docs/enterprise/setting-up-okta.mdx b/docs/enterprise/setting-up-okta.mdx index 26ac5e8225..32ba21c289 100644 --- a/docs/enterprise/setting-up-okta.mdx +++ b/docs/enterprise/setting-up-okta.mdx @@ -308,12 +308,6 @@ You can also map any custom attributes to any entity (role, team or business uni - **Team and business unit mappings**: All matching rules apply — users can be placed on multiple teams and business units simultaneously. - **Claim values**: Can be strings, arrays, or nested objects. Bifrost resolves dotted paths (e.g., `realm_access.roles`). - - If a user matches multiple role mapping rules, the highest privilege role is assigned. If no - mapping matches, the first user to sign in receives the **Admin** role, and subsequent users receive the **Viewer** - role. - - 5. Click **Save Configuration** After saving, you'll need to restart your Bifrost server for the changes to take effect. diff --git a/transports/bifrost-http/integrations/anthropic.go b/transports/bifrost-http/integrations/anthropic.go index 6dd80b527a..da9fff1da0 100644 --- a/transports/bifrost-http/integrations/anthropic.go +++ b/transports/bifrost-http/integrations/anthropic.go @@ -115,7 +115,11 @@ func createAnthropicMessagesRouteConfig(pathPrefix string, logger schemas.Logger StreamConfig: &StreamConfig{ ResponsesStreamResponseConverter: func(ctx *schemas.BifrostContext, resp *schemas.BifrostResponsesStreamResponse) (string, interface{}, error) { if shouldUsePassthrough(ctx, resp.ExtraFields.Provider, resp.ExtraFields.OriginalModelRequested, resp.ExtraFields.ResolvedModelUsed) { - if resp.ExtraFields.RawResponse != nil { + // Skip passthrough for ContentPartAdded: it's a synthetic bifrost event whose + // RawResponse carries the parent content_block_start already emitted by OutputItemAdded. + // Passing through here would produce a duplicate content_block_start that causes + // the Anthropic SDK to error and drop all subsequent content_block_delta events. + if resp.ExtraFields.RawResponse != nil && resp.Type != schemas.ResponsesStreamResponseTypeContentPartAdded { raw, ok := resp.ExtraFields.RawResponse.(string) if !ok { return "", nil, fmt.Errorf("expected RawResponse string, got %T", resp.ExtraFields.RawResponse) diff --git a/ui/app/workspace/governance/teams/page.tsx b/ui/app/workspace/governance/teams/page.tsx index 334ea97f63..f8f5320e4f 100644 --- a/ui/app/workspace/governance/teams/page.tsx +++ b/ui/app/workspace/governance/teams/page.tsx @@ -1,9 +1,105 @@ -import { TeamsView } from "@enterprise/components/user-groups/teamsView"; +import FullPageLoader from "@/components/fullPageLoader"; +import { useDebouncedValue } from "@/hooks/useDebounce"; +import { getErrorMessage, useGetCustomersQuery, useGetTeamsQuery, useGetVirtualKeysQuery } from "@/lib/store"; +import { RbacOperation, RbacResource, useRbac } from "@enterprise/lib"; +import { useEffect, useRef, useState } from "react"; +import { toast } from "sonner"; +import TeamsTable from "@/app/workspace/governance/views/teamsTable"; + +const POLLING_INTERVAL = 5000; +const PAGE_SIZE = 25; export default function GovernanceTeamsPage() { + const hasVirtualKeysAccess = useRbac(RbacResource.VirtualKeys, RbacOperation.View); + const hasCustomersAccess = useRbac(RbacResource.Customers, RbacOperation.View); + const hasTeamsAccess = useRbac(RbacResource.Teams, RbacOperation.View); + const shownErrorsRef = useRef(new Set()); + + const [search, setSearch] = useState(""); + const [offset, setOffset] = useState(0); + const debouncedSearch = useDebouncedValue(search, 300); + + useEffect(() => { + setOffset(0); + }, [debouncedSearch]); + + const { + data: virtualKeysData, + error: vkError, + isLoading: vkLoading, + } = useGetVirtualKeysQuery(undefined, { + skip: !hasVirtualKeysAccess, + pollingInterval: POLLING_INTERVAL, + }); + const { + data: customersData, + error: customersError, + isLoading: customersLoading, + } = useGetCustomersQuery(undefined, { + skip: !hasCustomersAccess, + pollingInterval: POLLING_INTERVAL, + }); + const { + data: teamsData, + error: teamsError, + isLoading: teamsLoading, + } = useGetTeamsQuery( + { + limit: PAGE_SIZE, + offset, + search: debouncedSearch || undefined, + }, + { + skip: !hasTeamsAccess, + pollingInterval: POLLING_INTERVAL, + }, + ); + + const teamsTotal = teamsData?.total_count ?? 0; + + // Snap offset back when total shrinks past current page (e.g. delete last item on last page) + useEffect(() => { + if (!teamsData || offset < teamsTotal) return; + setOffset(teamsTotal === 0 ? 0 : Math.floor((teamsTotal - 1) / PAGE_SIZE) * PAGE_SIZE); + }, [teamsTotal, offset]); + + const isLoading = vkLoading || customersLoading || teamsLoading; + + useEffect(() => { + if (!vkError && !customersError && !teamsError) { + shownErrorsRef.current.clear(); + return; + } + const errorKey = `${!!vkError}-${!!customersError}-${!!teamsError}`; + if (shownErrorsRef.current.has(errorKey)) return; + shownErrorsRef.current.add(errorKey); + if (vkError && customersError && teamsError) { + toast.error("Failed to load governance data."); + } else { + if (vkError) toast.error(`Failed to load virtual keys: ${getErrorMessage(vkError)}`); + if (customersError) toast.error(`Failed to load customers: ${getErrorMessage(customersError)}`); + if (teamsError) toast.error(`Failed to load teams: ${getErrorMessage(teamsError)}`); + } + }, [vkError, customersError, teamsError]); + + if (isLoading) { + return ; + } + return (
- +
); -} \ No newline at end of file +} diff --git a/ui/app/workspace/virtual-keys/hooks/useVirtualKeyUsage.ts b/ui/app/workspace/virtual-keys/hooks/useVirtualKeyUsage.ts index 12a88de0ae..1554424791 100644 --- a/ui/app/workspace/virtual-keys/hooks/useVirtualKeyUsage.ts +++ b/ui/app/workspace/virtual-keys/hooks/useVirtualKeyUsage.ts @@ -41,7 +41,7 @@ export function useVirtualKeyUsage(vk: VirtualKey | null | undefined): { const isManagedByProfile = managingProfile !== undefined; const displayBudgets: Budget[] | undefined = managingProfile - ? (managingProfile.budgets ?? []).map((line) => ({ + ? (managingProfile.budget_lines ?? []).map((line) => ({ id: line.id, max_limit: line.max_limit, reset_duration: line.reset_duration, @@ -50,7 +50,7 @@ export function useVirtualKeyUsage(vk: VirtualKey | null | undefined): { })) : vk?.budgets; - const apRL = managingProfile?.rate_limit; + const apRL = managingProfile?.rate_limits; const hasApRateLimit = !!(apRL && (apRL.token_max_limit != null || apRL.request_max_limit != null)); // When profile-managed, never fall back to raw VK rate limits (that would contradict the // locked edit/delete UX). If the profile has no rate limit, displayRateLimit is undefined. diff --git a/ui/app/workspace/virtual-keys/views/virtualKeyDetailsSheet.tsx b/ui/app/workspace/virtual-keys/views/virtualKeyDetailsSheet.tsx index 499a7ab84f..89586b5bc6 100644 --- a/ui/app/workspace/virtual-keys/views/virtualKeyDetailsSheet.tsx +++ b/ui/app/workspace/virtual-keys/views/virtualKeyDetailsSheet.tsx @@ -342,7 +342,7 @@ export default function VirtualKeyDetailSheet({ virtualKey, onClose }: VirtualKe

Budget Information - {isManagedByProfile && managingProfile?.budgets?.length ? ( + {isManagedByProfile && managingProfile?.budget_lines?.length ? ( (from {managingProfile.name}) ) : null}

diff --git a/ui/app/workspace/virtual-keys/views/virtualKeySheet.tsx b/ui/app/workspace/virtual-keys/views/virtualKeySheet.tsx index 703f9684dc..aa29c2ffd8 100644 --- a/ui/app/workspace/virtual-keys/views/virtualKeySheet.tsx +++ b/ui/app/workspace/virtual-keys/views/virtualKeySheet.tsx @@ -619,7 +619,7 @@ export default function VirtualKeySheet({ virtualKey, teams, customers, defaultT
diff --git a/ui/components/ui/multiBudgetLines.tsx b/ui/components/ui/MultiBudgetLines.tsx similarity index 100% rename from ui/components/ui/multiBudgetLines.tsx rename to ui/components/ui/MultiBudgetLines.tsx