refactor: remove dead code and enforce code quality gates (#8395)

* refactor: remove dead code and enforce code quality gates

* fix(ci): bind script coverage to the executed runner
This commit is contained in:
Waleed
2026-09-28 18:17:03 -07:00
committed by GitHub
parent 90f3eed349
commit 07c3ffe7db
102 changed files with 1885 additions and 7191 deletions
+1 -1
View File
@@ -377,7 +377,7 @@ jobs:
- name: Install ripgrep
run: command -v rg || (sudo apt-get update && sudo apt-get install -y ripgrep)
# Runs the setup CLI's Bun tests plus each workspace's Vitest suite,
# Runs the root scripts and each workspace's Vitest suite,
# without `--coverage`.
- name: Run tests
env:
@@ -1,11 +0,0 @@
import type { MentionFolderId } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/constants'
/**
* Shared folder navigation state for the mention menu.
*/
export interface MentionFolderNav {
currentFolder: MentionFolderId | null
isInFolder: boolean
openFolder: (folderId: MentionFolderId, title: string) => void
closeFolder: () => void
}
@@ -1,290 +0,0 @@
'use client'
import { useState } from 'react'
import { Chip, toast } from '@sim/emcn'
import { Workflow } from '@sim/emcn/icons'
import { getErrorMessage } from '@sim/utils/errors'
import type { CredentialGroupAccessResponse } from '@/lib/api/contracts/credential-groups'
import { CREDENTIAL_GROUP_WORKFLOW_ACCESS_LIMIT } from '@/lib/credential-groups/limits'
import { RowActionsMenu } from '@/app/workspace/[workspaceId]/settings/components/row-actions-menu'
import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
import {
RESOURCE_LIST_STACK,
SettingsResourceRow,
} from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row'
import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
import { CredentialGroupAddResourceModal } from '@/ee/credential-groups/components/credential-group-add-resource-modal'
import {
useCredentialGroupAccess,
useUpdateCredentialGroupAccess,
} from '@/hooks/queries/credential-groups'
import { useSettingsDirtyStore } from '@/stores/settings/dirty/store'
interface AccessDraft {
allowedWorkflowIds: string[]
baseline: string
expectedRevision: number
groupId: string
}
interface UseCredentialGroupAccessEditorProps {
workspaceId: string
groupId: string
enabled: boolean
}
function normalizeAllowedWorkflowIds(workflowIds: readonly string[]): string[] {
for (const workflowId of workflowIds) {
if (!workflowId || workflowId !== workflowId.trim()) {
throw new Error('Credential Group workflow access requires canonical non-empty workflow IDs')
}
}
if (new Set(workflowIds).size !== workflowIds.length) {
throw new Error('Credential Group workflow access contains duplicate workflows')
}
if (workflowIds.length > CREDENTIAL_GROUP_WORKFLOW_ACCESS_LIMIT) {
throw new Error(
`Credential Group workflow access cannot exceed ${CREDENTIAL_GROUP_WORKFLOW_ACCESS_LIMIT} workflows`
)
}
return [...workflowIds].sort()
}
function serializeAllowedWorkflowIds(workflowIds: readonly string[]): string {
return JSON.stringify(normalizeAllowedWorkflowIds(workflowIds))
}
export function useCredentialGroupAccessEditor({
workspaceId,
groupId,
enabled,
}: UseCredentialGroupAccessEditorProps) {
const access = useCredentialGroupAccess(workspaceId, groupId, { enabled })
const updateAccess = useUpdateCredentialGroupAccess()
const setSettingsNavigationBlocked = useSettingsDirtyStore((state) => state.setNavigationBlocked)
const [draft, setDraft] = useState<AccessDraft | null>(null)
if (draft && draft.groupId !== groupId) {
throw new Error('Credential Group access draft cannot move between resources')
}
const persistedAllowedWorkflowIds = access.data
? normalizeAllowedWorkflowIds(access.data.allowedWorkflowIds)
: null
const persistedValue = persistedAllowedWorkflowIds
? serializeAllowedWorkflowIds(persistedAllowedWorkflowIds)
: ''
const allowedWorkflowIds = draft?.allowedWorkflowIds ?? persistedAllowedWorkflowIds
const revision = draft?.expectedRevision ?? access.data?.revision ?? null
const dirty = draft !== null
const workflowIds = new Set(access.data?.workflows.map((workflow) => workflow.id) ?? [])
const selectionsAvailable = Boolean(
allowedWorkflowIds?.every((workflowId) => workflowIds.has(workflowId))
)
const setAllowedWorkflowIds = (nextWorkflowIds: readonly string[], expectedRevision: number) => {
if (!access.data) throw new Error('Credential Group workflow access is unavailable')
const currentRevision = draft?.expectedRevision ?? access.data.revision
if (expectedRevision !== currentRevision) {
throw new Error('Credential Group workflow access changed while it was being edited')
}
const normalizedWorkflowIds = normalizeAllowedWorkflowIds(nextWorkflowIds)
const nextValue = serializeAllowedWorkflowIds(normalizedWorkflowIds)
updateAccess.reset()
setDraft((current) => {
const baseline = current?.baseline ?? persistedValue
if (nextValue === baseline) return null
return {
allowedWorkflowIds: normalizedWorkflowIds,
baseline,
expectedRevision: current?.expectedRevision ?? access.data.revision,
groupId,
}
})
}
const discard = () => {
setDraft(null)
updateAccess.reset()
}
const save = async () => {
if (!draft) return
setSettingsNavigationBlocked(true)
try {
const availableWorkflowIds = new Set(access.data?.workflows.map((workflow) => workflow.id))
if (draft.allowedWorkflowIds.some((workflowId) => !availableWorkflowIds.has(workflowId))) {
throw new Error('Remove unavailable workflows before saving access')
}
await updateAccess.mutateAsync({
workspaceId,
groupId,
body: {
expectedRevision: draft.expectedRevision,
allowedWorkflowIds: draft.allowedWorkflowIds,
},
})
setDraft(null)
toast.success('Workflow access saved')
} catch (error) {
toast.error(getErrorMessage(error, 'Could not update workflow access'))
} finally {
setSettingsNavigationBlocked(false)
}
}
return {
allowedWorkflowIds,
revision,
workflows: access.data?.workflows ?? null,
setAllowedWorkflowIds,
discard,
save,
dirty,
error: updateAccess.error
? getErrorMessage(updateAccess.error, 'Could not update workflow access')
: null,
isPending: access.isPending && !access.data,
loadError: access.data ? null : access.error,
isReady: Boolean(access.data && selectionsAvailable),
saving: updateAccess.isPending,
}
}
interface CredentialGroupAccessProps {
allowedWorkflowIds: readonly string[] | null
revision: number | null
workflows: CredentialGroupAccessResponse['workflows'] | null
onAllowedWorkflowIdsChange: (workflowIds: readonly string[], expectedRevision: number) => void
error: string | null
isPending: boolean
loadError: unknown
saving: boolean
}
export function CredentialGroupAccess({
allowedWorkflowIds,
revision,
workflows,
onAllowedWorkflowIdsChange,
error,
isPending,
loadError,
saving,
}: CredentialGroupAccessProps) {
const [showAddWorkflow, setShowAddWorkflow] = useState(false)
if (loadError) {
return (
<SettingsEmptyState tone='error'>
{getErrorMessage(loadError, "Couldn't load workflow access")}
</SettingsEmptyState>
)
}
if (isPending) return null
if (!workflows) throw new Error('Credential Group workflow catalog is unavailable')
if (!allowedWorkflowIds) throw new Error('Credential Group workflow access is unavailable')
if (revision === null) throw new Error('Credential Group access revision is unavailable')
const allowedWorkflowIdSet = new Set(allowedWorkflowIds)
if (allowedWorkflowIdSet.size !== allowedWorkflowIds.length) {
throw new Error('Credential Group workflow access contains duplicate workflows')
}
const workflowsById = new Map(workflows.map((workflow) => [workflow.id, workflow]))
for (const workflowId of allowedWorkflowIds) {
if (!workflowsById.has(workflowId)) {
throw new Error(
`Credential Group workflow access references unavailable workflow ${workflowId}`
)
}
}
const allowedWorkflows = workflows.filter((workflow) => allowedWorkflowIdSet.has(workflow.id))
const availableWorkflows = workflows.filter((workflow) => !allowedWorkflowIdSet.has(workflow.id))
const addWorkflow = (workflowId: string) => {
if (!workflowsById.has(workflowId)) throw new Error(`Workflow ${workflowId} is unavailable`)
if (allowedWorkflowIdSet.has(workflowId)) {
throw new Error(`Workflow ${workflowId} already has Credential Group access`)
}
onAllowedWorkflowIdsChange([...allowedWorkflowIds, workflowId], revision)
}
const removeWorkflow = (workflowId: string) => {
if (!allowedWorkflowIdSet.has(workflowId)) {
throw new Error(`Workflow ${workflowId} does not have Credential Group access`)
}
onAllowedWorkflowIdsChange(
allowedWorkflowIds.filter((allowedWorkflowId) => allowedWorkflowId !== workflowId),
revision
)
}
const sectionAction = (
<Chip
onClick={() => setShowAddWorkflow(true)}
disabled={
saving ||
availableWorkflows.length === 0 ||
allowedWorkflowIds.length >= CREDENTIAL_GROUP_WORKFLOW_ACCESS_LIMIT
}
>
Add workflow
</Chip>
)
return (
<>
<SettingsSection label='Workflow access' action={sectionAction}>
{error && (
<p role='alert' className='mb-3 px-0.5 text-[var(--text-error)] text-caption'>
{error}
</p>
)}
{allowedWorkflows.length === 0 ? (
<SettingsEmptyState variant='inline'>No workflows have access</SettingsEmptyState>
) : (
<div className={RESOURCE_LIST_STACK}>
{allowedWorkflows.map((workflow) => (
<SettingsResourceRow
key={workflow.id}
icon={<Workflow className='text-[var(--text-icon)]' aria-hidden />}
iconFilled
title={workflow.name}
description='Deployed runs can use every credential in this group'
disabled={saving}
trailing={
saving ? undefined : (
<RowActionsMenu
label={`${workflow.name} actions`}
actions={[
{
label: 'Remove',
destructive: true,
onSelect: () => removeWorkflow(workflow.id),
},
]}
/>
)
}
/>
))}
</div>
)}
</SettingsSection>
{showAddWorkflow && (
<CredentialGroupAddResourceModal
resourceType='workflow'
resources={availableWorkflows}
disabled={saving}
onAdd={(id) => {
addWorkflow(id)
setShowAddWorkflow(false)
}}
onClose={() => setShowAddWorkflow(false)}
/>
)}
</>
)
}
@@ -1,110 +0,0 @@
'use client'
import { useState } from 'react'
import {
ChipDropdown,
ChipModal,
ChipModalBody,
ChipModalError,
ChipModalField,
ChipModalFooter,
ChipModalHeader,
ChipSelect,
} from '@sim/emcn'
interface CredentialGroupAddResourceModalBaseProps {
resources: readonly { id: string; name: string }[]
disabled: boolean
error?: string
onClose: () => void
}
type CredentialGroupAddResourceModalProps = CredentialGroupAddResourceModalBaseProps &
(
| { resourceType: 'workflow'; onAdd: (resourceId: string) => void }
| { resourceType: 'workspace'; onAdd: (resourceIds: string[]) => void }
)
export function CredentialGroupAddResourceModal(props: CredentialGroupAddResourceModalProps) {
const { resources, resourceType, disabled, error, onClose } = props
const [selectedResourceIds, setSelectedResourceIds] = useState<string[]>([])
const label = resourceType === 'workspace' ? 'Workspaces' : 'Workflow'
const title = resourceType === 'workspace' ? 'Add workspaces' : 'Add workflow'
const options = resources.map((resource) => ({ value: resource.id, label: resource.name }))
const handleAdd = () => {
if (!selectedResourceIds.length)
throw new Error(`Select a ${resourceType} before granting access`)
if (new Set(selectedResourceIds).size !== selectedResourceIds.length) {
throw new Error('Access selection contains duplicate resources')
}
const available = new Set(resources.map((resource) => resource.id))
for (const id of selectedResourceIds) {
if (!available.has(id)) throw new Error(`Selected ${resourceType} ${id} is unavailable`)
}
if (props.resourceType === 'workspace') props.onAdd(selectedResourceIds)
else props.onAdd(selectedResourceIds[0])
}
return (
<ChipModal
open
onOpenChange={(open) => !open && !disabled && onClose()}
srTitle={title}
size='sm'
dismissDisabled={disabled}
>
<ChipModalHeader onClose={onClose} closeDisabled={disabled}>
{title}
</ChipModalHeader>
<ChipModalBody>
<ChipModalField type='custom' title={label} required submitOnEnter={false}>
{(aria) =>
resourceType === 'workspace' ? (
<ChipDropdown
multiple
options={options}
value={selectedResourceIds}
onChange={setSelectedResourceIds}
allLabel='Select workspaces'
showAllOption={false}
searchPlaceholder='Search workspaces'
searchable
aria-label={label}
disabled={disabled}
fullWidth
matchTriggerWidth
align='start'
{...aria}
/>
) : (
<ChipSelect
options={options}
value={selectedResourceIds[0] ?? ''}
onChange={(id) => setSelectedResourceIds([id])}
placeholder={`Select ${resourceType}`}
searchPlaceholder={`Search ${resourceType}s`}
searchable
aria-label={label}
disabled={disabled}
fullWidth
dropdownWidth='trigger'
align='start'
{...aria}
/>
)
}
</ChipModalField>
<ChipModalError>{error}</ChipModalError>
</ChipModalBody>
<ChipModalFooter
onCancel={onClose}
primaryAction={{
label: title,
onClick: handleAdd,
disabled: disabled || selectedResourceIds.length === 0,
}}
/>
</ChipModal>
)
}
@@ -1,371 +0,0 @@
'use client'
import { useState } from 'react'
import { Avatar, Chip, ChipConfirmModal, ChipModalTabs, toast } from '@sim/emcn'
import { Plus } from '@sim/emcn/icons'
import { getErrorMessage } from '@sim/utils/errors'
import { useQueryState } from 'nuqs'
import { saveDiscardActions } from '@/components/settings/save-discard-actions'
import type { CredentialGroupEnrollment } from '@/lib/api/contracts/credential-groups'
import { SLACK_CUSTOM_BOT_PROVIDER_ID } from '@/lib/oauth/types'
import { UnsavedChangesModal } from '@/app/workspace/[workspaceId]/components/credential-detail'
import {
credentialGroupPeopleSearchParam,
credentialGroupPeopleSearchUrlKeys,
credentialGroupProviderSearchParam,
credentialGroupProviderSearchUrlKeys,
credentialGroupTabParam,
credentialGroupTabUrlKeys,
} from '@/app/workspace/[workspaceId]/settings/[section]/search-params'
import { RowActionsMenu } from '@/app/workspace/[workspaceId]/settings/components/row-actions-menu'
import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
import type { SettingsAction } from '@/app/workspace/[workspaceId]/settings/components/settings-header/settings-header'
import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
import {
RESOURCE_LIST_STACK,
SettingsResourceRow,
} from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row'
import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
import { useSettingsUnsavedGuard } from '@/app/workspace/[workspaceId]/settings/hooks/use-settings-unsaved-guard'
import {
CredentialGroupAccess,
useCredentialGroupAccessEditor,
} from '@/ee/credential-groups/components/credential-group-access'
import { CredentialGroupDetails } from '@/ee/credential-groups/components/credential-group-details'
import { EnrollmentConnections } from '@/ee/credential-groups/components/credential-group-enrollment-connections'
import { CredentialGroupInviteModal } from '@/ee/credential-groups/components/credential-group-invite-modal'
import {
useCredentialGroupDetail,
useDeleteCredentialGroupEnrollment,
useResendCredentialGroupEnrollment,
useUpdateCredentialGroup,
} from '@/hooks/queries/credential-groups'
import { useWorkspaceCredentials } from '@/hooks/queries/credentials'
import { useDebouncedSearchSetter } from '@/hooks/use-debounced-search-setter'
interface CredentialGroupDetailProps {
workspaceId: string
groupId: string
}
type CredentialGroupTab = 'details' | 'people' | 'access'
const CREDENTIAL_GROUP_TABS = [
{ value: 'details', label: 'Accounts' },
{ value: 'people', label: 'People' },
{ value: 'access', label: 'Workflow access' },
] as const
export function CredentialGroupDetail({ workspaceId, groupId }: CredentialGroupDetailProps) {
const detail = useCredentialGroupDetail(workspaceId, groupId)
const slackBots = useWorkspaceCredentials({
workspaceId,
type: 'service_account',
providerId: SLACK_CUSTOM_BOT_PROVIDER_ID,
})
const resend = useResendCredentialGroupEnrollment()
const deleteEnrollment = useDeleteCredentialGroupEnrollment()
const updateGroup = useUpdateCredentialGroup()
const [activeTab, setActiveTab] = useQueryState(credentialGroupTabParam.key, {
...credentialGroupTabParam.parser,
...credentialGroupTabUrlKeys,
})
const accessEditor = useCredentialGroupAccessEditor({
workspaceId,
groupId,
enabled: activeTab === 'access',
})
const [providerSearch, setProviderSearchParam] = useQueryState(
credentialGroupProviderSearchParam.key,
{ ...credentialGroupProviderSearchParam.parser, ...credentialGroupProviderSearchUrlKeys }
)
const setProviderSearch = useDebouncedSearchSetter(setProviderSearchParam)
const [peopleSearch, setPeopleSearchParam] = useQueryState(credentialGroupPeopleSearchParam.key, {
...credentialGroupPeopleSearchParam.parser,
...credentialGroupPeopleSearchUrlKeys,
})
const setPeopleSearch = useDebouncedSearchSetter(setPeopleSearchParam)
const [showInvite, setShowInvite] = useState(false)
const [deletingEnrollmentId, setDeletingEnrollmentId] = useState<string | null>(null)
const credentialGroup = detail.data?.pages[0]?.credentialGroup
const enrollments = detail.data?.pages.flatMap((page) => page.enrollments) ?? []
const peopleFilter = peopleSearch.trim().toLowerCase()
/**
* Only the pages already loaded: the enrollment list is cursor-paginated with no
* server-side term, so a match on a later page appears only once it is fetched.
*/
const visibleEnrollments = peopleFilter
? enrollments.filter((enrollment) => enrollment.email.toLowerCase().includes(peopleFilter))
: enrollments
/**
* `+` means more people exist than are loaded, so it stays on the total. While
* filtering, the match count is reported against that total rather than replacing
* it — otherwise `People (2+)` reads as a two-person group with more to come.
*/
const loadedTotal = `${enrollments.length}${detail.hasNextPage ? '+' : ''}`
const peopleLabel = peopleFilter
? `People (${visibleEnrollments.length} of ${loadedTotal})`
: `People (${loadedTotal})`
const deletingEnrollment = deletingEnrollmentId
? (enrollments.find((enrollment) => enrollment.id === deletingEnrollmentId) ?? null)
: null
const configurationReady =
Boolean(
credentialGroup &&
(credentialGroup.options.length ||
credentialGroup.mcpServers.some(
(server) => server.enabled && server.authType === 'oauth'
))
) &&
credentialGroup?.options.every(
(option) =>
option.provider !== 'slack' ||
(option.configurationStatus === 'ready' &&
slackBots.data?.some((bot) => bot.id === option.slackBotCredentialId))
)
const guard = useSettingsUnsavedGuard({
isDirty: accessEditor.dirty,
navigationBlocked: updateGroup.isPending || accessEditor.saving,
})
const credentialGroupMutationPending =
updateGroup.isPending || accessEditor.saving || resend.isPending || deleteEnrollment.isPending
const handleTabChange = (value: string) => {
const nextTab = value as CredentialGroupTab
if (nextTab === activeTab) return
guard.guardBack(() => {
accessEditor.discard()
void setActiveTab(nextTab)
})
}
const handleEnable = async () => {
if (!credentialGroup) return
try {
await updateGroup.mutateAsync({
workspaceId,
groupId: credentialGroup.id,
body: { status: 'active' },
})
toast.success('Connected accounts enabled')
} catch (error) {
toast.error(getErrorMessage(error, 'Could not enable connected accounts'))
}
}
const actions: SettingsAction[] = credentialGroup
? [
...(credentialGroup.status === 'disabled'
? [
{
text: updateGroup.isPending ? 'Enabling...' : 'Enable accounts',
variant: 'primary' as const,
onSelect: () => void handleEnable(),
disabled: credentialGroupMutationPending,
},
]
: []),
...(activeTab === 'people'
? [
{
text: 'Request connections',
icon: Plus,
variant: 'primary' as const,
onSelect: () => setShowInvite(true),
disabled: credentialGroup.status !== 'active' || !configurationReady,
},
]
: activeTab === 'access'
? saveDiscardActions({
dirty: accessEditor.dirty,
saving: accessEditor.saving,
onSave: () => void accessEditor.save(),
onDiscard: accessEditor.discard,
saveDisabled: !accessEditor.isReady,
saveTooltip: !accessEditor.isReady ? 'Workflow access is unavailable' : undefined,
})
: []),
]
: []
const handleResend = async (enrollment: CredentialGroupEnrollment) => {
try {
await resend.mutateAsync({ workspaceId, groupId, enrollmentId: enrollment.id })
toast.success(`Invitation resent to ${enrollment.email}`)
} catch (error) {
toast.error(getErrorMessage(error, 'Failed to resend invitation'))
}
}
const handleDeleteEnrollment = async () => {
if (!deletingEnrollment) return
try {
await deleteEnrollment.mutateAsync({
workspaceId,
groupId,
enrollmentId: deletingEnrollment.id,
})
toast.success(`${deletingEnrollment.email} deleted`)
setDeletingEnrollmentId(null)
} catch (error) {
toast.error(getErrorMessage(error, 'Failed to delete person'))
}
}
return (
<>
<SettingsPanel
actions={actions}
search={
activeTab === 'details'
? {
value: providerSearch,
onChange: setProviderSearch,
placeholder: 'Search accounts and MCP servers...',
disabled: detail.isPending,
}
: activeTab === 'people'
? {
value: peopleSearch,
onChange: setPeopleSearch,
placeholder: 'Search people...',
disabled: detail.isPending,
}
: undefined
}
>
{detail.error ? (
<SettingsEmptyState tone='error'>
{getErrorMessage(detail.error, "Couldn't load connected accounts")}
</SettingsEmptyState>
) : detail.isPending || !credentialGroup ? null : (
<>
<ChipModalTabs
tabs={CREDENTIAL_GROUP_TABS}
value={activeTab}
onChange={handleTabChange}
aria-label='Connected accounts sections'
/>
{activeTab === 'details' && (
<>
<CredentialGroupDetails
workspaceId={workspaceId}
credentialGroup={credentialGroup}
providerSearch={providerSearch}
/>
</>
)}
{activeTab === 'people' && (
<SettingsSection
label={peopleLabel}
action={
detail.hasNextPage ? (
<Chip
onClick={() => void detail.fetchNextPage()}
disabled={detail.isFetchingNextPage}
>
{detail.isFetchingNextPage ? 'Loading...' : 'Load more'}
</Chip>
) : undefined
}
>
{visibleEnrollments.length === 0 ? (
<SettingsEmptyState variant='inline'>
{peopleFilter ? 'No people match your search' : 'No people invited yet'}
</SettingsEmptyState>
) : (
<div className={RESOURCE_LIST_STACK}>
{visibleEnrollments.map((enrollment) => {
return (
<SettingsResourceRow
key={enrollment.id}
icon={<Avatar size='xs' name={enrollment.email} aria-hidden />}
iconVariant='custom'
title={enrollment.email}
description={
<EnrollmentConnections
connections={enrollment.connections}
mcpConnections={enrollment.mcpConnections}
/>
}
trailing={
<RowActionsMenu
label={`${enrollment.email} actions`}
actions={[
{
label: 'Resend',
onSelect: () => void handleResend(enrollment),
disabled: resend.isPending,
},
{
label: 'Delete',
destructive: true,
onSelect: () => setDeletingEnrollmentId(enrollment.id),
},
]}
/>
}
/>
)
})}
</div>
)}
</SettingsSection>
)}
{activeTab === 'access' && (
<CredentialGroupAccess
key={groupId}
allowedWorkflowIds={accessEditor.allowedWorkflowIds}
revision={accessEditor.revision}
workflows={accessEditor.workflows}
onAllowedWorkflowIdsChange={accessEditor.setAllowedWorkflowIds}
error={accessEditor.error}
isPending={accessEditor.isPending}
loadError={accessEditor.loadError}
saving={accessEditor.saving}
/>
)}
</>
)}
</SettingsPanel>
{credentialGroup && (
<CredentialGroupInviteModal
open={showInvite}
onOpenChange={setShowInvite}
workspaceId={workspaceId}
groupId={groupId}
/>
)}
<ChipConfirmModal
open={Boolean(deletingEnrollment)}
onOpenChange={(open) =>
!open && !deleteEnrollment.isPending && setDeletingEnrollmentId(null)
}
srTitle='Delete person'
title='Delete person'
text={[
`Delete ${deletingEnrollment?.email ?? 'this person'}?`,
{
text: ' Their invitation link will stop working and the accounts they connected here will be removed.',
error: true,
},
]}
dismissLabel='Cancel'
confirm={{
label: deleteEnrollment.isPending ? 'Deleting...' : 'Delete',
onClick: handleDeleteEnrollment,
disabled: deleteEnrollment.isPending,
}}
/>
<UnsavedChangesModal
open={guard.showUnsavedModal}
onOpenChange={guard.setShowUnsavedModal}
onDiscard={guard.confirmDiscard}
/>
</>
)
}
@@ -1,414 +0,0 @@
'use client'
import { useState } from 'react'
import { Chip, ChipConfirmModal, ChipTag, toast } from '@sim/emcn'
import { getErrorMessage } from '@sim/utils/errors'
import type { WorkspaceCredential } from '@/lib/api/contracts'
import type {
CredentialGroup,
CredentialGroupOption,
UpdateCredentialGroupBody,
} from '@/lib/api/contracts/credential-groups'
import { getManagedMcpConnectorIcon } from '@/lib/credential-groups/managed-mcp-connector-icons'
import {
MANAGED_MCP_CONNECTOR_IDS,
MANAGED_MCP_CONNECTORS,
type ManagedMcpConnectorId,
} from '@/lib/credential-groups/managed-mcp-connectors'
import {
CREDENTIAL_GROUP_PROVIDER_IDS,
type CredentialGroupProvider,
type CredentialGroupStandardOAuthProvider,
getCredentialGroupProviderService,
getCredentialGroupProviderSupport,
isCredentialGroupStandardOAuthProvider,
} from '@/lib/credential-groups/providers'
import { SLACK_MANAGED_USER_SCOPES } from '@/lib/credential-groups/slack-managed-user-scopes'
import { SLACK_CUSTOM_BOT_PROVIDER_ID } from '@/lib/oauth/types'
import { RowActionsMenu } from '@/app/workspace/[workspaceId]/settings/components/row-actions-menu'
import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
import {
RESOURCE_LIST_STACK,
SettingsResourceRow,
} from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row'
import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
import { CredentialGroupProviderTile } from '@/ee/credential-groups/components/credential-group-provider-tile'
import { SlackManagedUsersModal } from '@/ee/credential-groups/components/slack-managed-users-modal'
import {
useCreateCredentialGroupMcpConnector,
useDeleteCredentialGroupMcpConnector,
useUpdateCredentialGroup,
useWorkspaceAccounts,
} from '@/hooks/queries/credential-groups'
import { useWorkspaceCredentials } from '@/hooks/queries/credentials'
/** Stable identity so a pending/errored credentials query cannot churn the modal's `bots` prop. */
const EMPTY_SLACK_BOTS: WorkspaceCredential[] = []
interface CredentialGroupDetailsProps {
credentialGroup: CredentialGroup
workspaceId: string
/** Filters the account types offered below; owned by the panel header's search field. */
providerSearch: string
}
function toOptionUpdateInput(
option: CredentialGroupOption
): NonNullable<UpdateCredentialGroupBody['options']>[number] {
const common = {
id: option.id,
label: getCredentialGroupProviderService(option.provider).name,
required: false,
}
if (option.provider !== 'slack') return { ...common, provider: option.provider }
return {
...common,
provider: 'slack',
slackBotCredentialId: option.slackBotCredentialId,
}
}
export function CredentialGroupDetails({
credentialGroup,
workspaceId,
providerSearch,
}: CredentialGroupDetailsProps) {
const updateGroup = useUpdateCredentialGroup()
const createMcpConnector = useCreateCredentialGroupMcpConnector()
const deleteMcpConnector = useDeleteCredentialGroupMcpConnector()
/**
* Reads the same cache entry the workspace settings already populated, so the deployment's configured
* providers arrive without a second request.
*/
const accounts = useWorkspaceAccounts(workspaceId)
const availableProviders = accounts.data?.availableProviders
const slackBots = useWorkspaceCredentials({
workspaceId,
type: 'service_account',
providerId: SLACK_CUSTOM_BOT_PROVIDER_ID,
})
const [slackSetup, setSlackSetup] = useState<{ credentialId?: string } | null>(null)
const [removingProvider, setRemovingProvider] = useState<CredentialGroupProvider | null>(null)
const [removingMcpConnector, setRemovingMcpConnector] = useState<ManagedMcpConnectorId | null>(
null
)
const isUpdating =
updateGroup.isPending || createMcpConnector.isPending || deleteMcpConnector.isPending
const updateOptions = async (
options: NonNullable<UpdateCredentialGroupBody['options']>,
successMessage: string
) => {
try {
await updateGroup.mutateAsync({
workspaceId,
groupId: credentialGroup.id,
body: { options },
})
toast.success(successMessage)
return true
} catch (error) {
toast.error(getErrorMessage(error, 'Could not update connected accounts'))
return false
}
}
const addProvider = async (provider: CredentialGroupStandardOAuthProvider) => {
const service = getCredentialGroupProviderService(provider)
const existing = credentialGroup.options.map(toOptionUpdateInput)
const nextOption: NonNullable<UpdateCredentialGroupBody['options']>[number] = {
provider,
label: service.name,
required: false,
}
return updateOptions([...existing, nextOption], `${service.name} added`)
}
const openSlackSetup = (credentialId?: string) => {
setSlackSetup({ credentialId })
}
const handleProviderAction = (provider: CredentialGroupProvider) => {
const support = getCredentialGroupProviderSupport(provider)
if (isCredentialGroupStandardOAuthProvider(provider)) {
void addProvider(provider)
return
}
if (support.configuration === 'slack_custom_bot') {
openSlackSetup()
return
}
throw new Error(`Unsupported Credential Group configuration: ${support.configuration}`)
}
const handleRemoveProvider = async () => {
if (!removingProvider) return
const service = getCredentialGroupProviderService(removingProvider)
const options = credentialGroup.options
.filter((option) => option.provider !== removingProvider)
.map(toOptionUpdateInput)
if (await updateOptions(options, `${service.name} removed`)) setRemovingProvider(null)
}
const addMcpConnector = async (connectorId: Exclude<ManagedMcpConnectorId, 'databricks'>) => {
try {
await createMcpConnector.mutateAsync({
workspaceId,
groupId: credentialGroup.id,
body: { connectorId },
})
toast.success(`${MANAGED_MCP_CONNECTORS[connectorId].name} added`)
} catch (error) {
toast.error(getErrorMessage(error, 'Could not add MCP app'))
}
}
const handleRemoveMcpConnector = async () => {
if (!removingMcpConnector) return
const connector = MANAGED_MCP_CONNECTORS[removingMcpConnector]
try {
await deleteMcpConnector.mutateAsync({
workspaceId,
groupId: credentialGroup.id,
connectorId: removingMcpConnector,
})
toast.success(`${connector.name} removed`)
setRemovingMcpConnector(null)
} catch (error) {
toast.error(getErrorMessage(error, 'Could not remove MCP app'))
}
}
/**
* A provider whose OAuth client this deployment has not configured can never finish an
* enrollment, so it is not offered — but one already on the group stays listed regardless, or
* the row that removes it would disappear along with it.
*/
const configuredProviders = new Set(credentialGroup.options.map((option) => option.provider))
const offerableProviders = availableProviders ? new Set(availableProviders) : null
const providerQuery = providerSearch.trim().toLowerCase()
const shownProviders = CREDENTIAL_GROUP_PROVIDER_IDS.filter((provider) => {
if (
!configuredProviders.has(provider) &&
offerableProviders &&
!offerableProviders.has(provider)
) {
return false
}
if (!providerQuery) return true
return getCredentialGroupProviderService(provider).name.toLowerCase().includes(providerQuery)
})
const shownMcpConnectors = MANAGED_MCP_CONNECTOR_IDS.filter((connectorId) => {
if (
!credentialGroup.mcpServers.some((server) => server.managedConnectorId === connectorId) &&
!accounts.data?.availableMcpConnectors.includes(connectorId)
)
return false
if (!providerQuery) return true
const connector = MANAGED_MCP_CONNECTORS[connectorId]
return (
connector.name.toLowerCase().includes(providerQuery) ||
connector.description.toLowerCase().includes(providerQuery)
)
})
return (
<>
<SettingsSection label='Accounts people can connect'>
{shownProviders.length === 0 ? (
<SettingsEmptyState variant='inline'>
{providerSearch.trim()
? `No account types found matching "${providerSearch}"`
: 'No account types are available. Configure an OAuth client to offer one.'}
</SettingsEmptyState>
) : null}
<div className={RESOURCE_LIST_STACK}>
{shownProviders.map((provider) => {
const service = getCredentialGroupProviderService(provider)
const support = getCredentialGroupProviderSupport(provider)
const option = credentialGroup.options.find(
(candidate) => candidate.provider === provider
)
const ProviderIcon = service.icon
const slackBot =
provider === 'slack' && option?.provider === 'slack'
? slackBots.data?.find((bot) => bot.id === option.slackBotCredentialId)
: undefined
const slackNeedsSetup =
provider === 'slack' &&
option?.provider === 'slack' &&
(!slackBot || option.configurationStatus !== 'ready')
const descriptionText =
provider === 'slack' && option
? slackBot
? `${slackBot.displayName}${slackNeedsSetup ? ' · Finish member sign-in setup' : ''}`
: slackBots.isPending
? 'Loading custom Slack app...'
: 'Custom Slack app unavailable'
: support.description
return (
<SettingsResourceRow
key={provider}
iconVariant='custom'
icon={<CredentialGroupProviderTile provider={provider} icon={ProviderIcon} />}
title={service.name}
description={descriptionText}
badge={
option && !slackNeedsSetup ? <ChipTag variant='gray'>Added</ChipTag> : undefined
}
trailing={
option ? (
<div className='flex items-center gap-1'>
{slackNeedsSetup && option.provider === 'slack' && slackBot ? (
<Chip onClick={() => openSlackSetup(slackBot.id)} disabled={isUpdating}>
Continue setup
</Chip>
) : null}
<RowActionsMenu
label={`${service.name} actions`}
actions={[
...(provider === 'slack'
? [
{
label: 'Change Slack app',
onSelect: () =>
openSlackSetup(
option?.provider === 'slack'
? option.slackBotCredentialId
: undefined
),
disabled: isUpdating,
},
]
: []),
{
label: 'Remove',
destructive: true,
onSelect: () => setRemovingProvider(provider),
disabled: isUpdating,
},
]}
/>
</div>
) : (
<Chip
onClick={() => handleProviderAction(provider)}
disabled={isUpdating || (provider === 'slack' && slackBots.isPending)}
>
{support.configuration === 'oauth' ? 'Add' : 'Set up'}
</Chip>
)
}
/>
)
})}
</div>
</SettingsSection>
<SettingsSection label='MCP apps people can connect'>
{shownMcpConnectors.length === 0 ? (
<SettingsEmptyState variant='inline'>
{providerSearch.trim()
? `No MCP apps found matching "${providerSearch}"`
: 'No MCP apps are available.'}
</SettingsEmptyState>
) : null}
<div className={RESOURCE_LIST_STACK}>
{shownMcpConnectors.map((connectorId) => {
const connector = MANAGED_MCP_CONNECTORS[connectorId]
const server = credentialGroup.mcpServers.find(
(candidate) => candidate.managedConnectorId === connectorId
)
const ConnectorIcon = getManagedMcpConnectorIcon(connectorId)
return (
<SettingsResourceRow
key={connectorId}
iconVariant='custom'
icon={<CredentialGroupProviderTile provider={connectorId} icon={ConnectorIcon} />}
title={server?.name ?? connector.name}
description={
connectorId === 'databricks'
? 'Configure Databricks in organization Connected accounts'
: connector.description
}
badge={server ? <ChipTag variant='gray'>Added</ChipTag> : undefined}
trailing={
server ? (
<RowActionsMenu
label={`${connector.name} actions`}
actions={[
{
label: 'Remove',
destructive: true,
onSelect: () => setRemovingMcpConnector(connectorId),
disabled: isUpdating,
},
]}
/>
) : connectorId !== 'databricks' ? (
<Chip disabled={isUpdating} onClick={() => void addMcpConnector(connectorId)}>
Add
</Chip>
) : undefined
}
/>
)
})}
</div>
</SettingsSection>
<SlackManagedUsersModal
open={slackSetup !== null}
workspaceId={workspaceId}
credentialGroupId={credentialGroup.id}
onOpenChange={(nextOpen) => {
if (!nextOpen) setSlackSetup(null)
}}
bots={slackBots.data ?? EMPTY_SLACK_BOTS}
isLoading={slackBots.isPending}
error={slackBots.error}
initialCredentialId={slackSetup?.credentialId}
initialRequiredScopes={
credentialGroup.options.find((option) => option.provider === 'slack')?.requiredScopes ??
(credentialGroup.options.some((option) => option.provider === 'slack')
? SLACK_MANAGED_USER_SCOPES
: undefined)
}
/>
<ChipConfirmModal
open={Boolean(removingProvider)}
onOpenChange={(open) => !open && !isUpdating && setRemovingProvider(null)}
srTitle='Remove account type'
title={`Remove ${
removingProvider ? getCredentialGroupProviderService(removingProvider).name : 'account'
}`}
text='People will no longer be able to connect this account. Previously connected accounts will no longer be available to Search or workflows.'
dismissLabel='Cancel'
confirm={{
label: isUpdating ? 'Removing...' : 'Remove',
onClick: handleRemoveProvider,
disabled: isUpdating,
}}
/>
<ChipConfirmModal
open={Boolean(removingMcpConnector)}
onOpenChange={(open) => !open && !isUpdating && setRemovingMcpConnector(null)}
srTitle='Remove MCP app'
title={`Remove ${
removingMcpConnector ? MANAGED_MCP_CONNECTORS[removingMcpConnector].name : 'MCP app'
}`}
text='People will no longer be able to connect this app. Existing OAuth grants and saved tool metadata will be revoked.'
dismissLabel='Cancel'
confirm={{
label: isUpdating ? 'Removing...' : 'Remove',
onClick: handleRemoveMcpConnector,
disabled: isUpdating,
}}
/>
</>
)
}
@@ -1,50 +0,0 @@
import { McpIcon } from '@/components/icons'
import type {
CredentialGroupEnrollmentConnection,
CredentialGroupEnrollmentMcpConnection,
} from '@/lib/api/contracts/credential-groups'
import { getCredentialGroupProviderService } from '@/lib/credential-groups/providers'
import { resolveCredentialDisplay } from '@/lib/integrations/credential-display'
interface EnrollmentConnectionsProps {
connections: CredentialGroupEnrollmentConnection[]
mcpConnections: CredentialGroupEnrollmentMcpConnection[]
}
interface CredentialProviderIconProps {
provider: CredentialGroupEnrollmentConnection['provider']
}
function CredentialProviderIcon({ provider }: CredentialProviderIconProps) {
if (provider === 'gitlab') {
const display = resolveCredentialDisplay({
type: 'personal_token',
providerId: provider,
displayName: provider,
})
const Icon = display.icon
return Icon ? <Icon className='size-[14px]' aria-label={display.detailTitle} /> : null
}
const ProviderIcon = getCredentialGroupProviderService(provider).icon
return <ProviderIcon className='size-[14px]' aria-hidden />
}
export function EnrollmentConnections({ connections, mcpConnections }: EnrollmentConnectionsProps) {
const connected = connections.filter((connection) => connection.status === 'active')
const connectedMcp = mcpConnections.filter((connection) => connection.status === 'active')
const count =
connected.reduce((total, connection) => total + connection.count, 0) + connectedMcp.length
const providers = [...new Set(connected.map((connection) => connection.provider))]
return (
<span className='flex items-center gap-1.5'>
{providers.map((provider) => {
return <CredentialProviderIcon key={provider} provider={provider} />
})}
{connectedMcp.length > 0 ? <McpIcon className='size-[14px]' aria-hidden /> : null}
<span>
{count} {count === 1 ? 'account' : 'accounts'} connected
</span>
</span>
)
}
@@ -1,122 +0,0 @@
'use client'
import { useCallback, useState } from 'react'
import {
ChipModal,
ChipModalBody,
ChipModalError,
ChipModalField,
ChipModalFooter,
ChipModalHeader,
toast,
} from '@sim/emcn'
import { getErrorMessage } from '@sim/utils/errors'
import { quickValidateEmail } from '@/lib/messaging/email/validation'
import { useInviteCredentialGroupEnrollments } from '@/hooks/queries/credential-groups'
interface CredentialGroupInviteModalProps {
open: boolean
onOpenChange: (open: boolean) => void
workspaceId: string
groupId: string
}
function validateEmail(email: string): string | null {
const result = quickValidateEmail(email)
return result.isValid ? null : (result.reason ?? 'Invalid email')
}
export function CredentialGroupInviteModal({
open,
onOpenChange,
workspaceId,
groupId,
}: CredentialGroupInviteModalProps) {
const invite = useInviteCredentialGroupEnrollments()
const [emails, setEmails] = useState<string[]>([])
const [deliveryError, setDeliveryError] = useState<string | null>(null)
const canSubmit = emails.length > 0 && !invite.isPending
const handleEmailsChange = useCallback((next: string[]) => {
setEmails(next)
setDeliveryError(null)
}, [])
const handleOpenChange = (nextOpen: boolean) => {
if (invite.isPending) return
onOpenChange(nextOpen)
if (!nextOpen) {
setEmails([])
setDeliveryError(null)
invite.reset()
}
}
const handleSubmit = async () => {
if (!canSubmit) return
setDeliveryError(null)
try {
const result = await invite.mutateAsync({
workspaceId,
groupId,
body: { emails },
})
const failures = result.results.filter((item) => !item.success)
if (failures.length === 0) {
toast.success(
result.sentCount === 1
? 'Connection request sent'
: `${result.sentCount} connection requests sent`
)
handleOpenChange(false)
return
}
setEmails(failures.map((item) => item.email))
setDeliveryError(
result.sentCount > 0
? `${result.sentCount} sent. ${failures.length} failed: ${failures.map((item) => item.email).join(', ')}`
: `No invitations were sent: ${failures.map((item) => `${item.email} (${item.error})`).join(', ')}`
)
} catch {
return
}
}
return (
<ChipModal
open={open}
onOpenChange={handleOpenChange}
srTitle='Request account connections'
dismissDisabled={invite.isPending}
>
<ChipModalHeader onClose={() => handleOpenChange(false)}>
Request account connections
</ChipModalHeader>
<ChipModalBody>
<ChipModalField
type='emails'
title='Emails'
value={emails}
onChange={handleEmailsChange}
validate={validateEmail}
placeholder='Enter emails'
disabled={invite.isPending}
/>
<ChipModalError>
{deliveryError ??
(invite.error ? getErrorMessage(invite.error, 'Failed to send invitations') : null)}
</ChipModalError>
</ChipModalBody>
<ChipModalFooter
onCancel={() => handleOpenChange(false)}
cancelDisabled={invite.isPending}
primaryAction={{
label: invite.isPending ? 'Sending...' : 'Send requests',
onClick: handleSubmit,
disabled: !canSubmit,
}}
/>
</ChipModal>
)
}
@@ -17,10 +17,7 @@ import {
vi.mock('@/lib/api/client/request', () => apiClientRequestMock)
import {
useUpdateCredentialGroupAccess,
useWorkspaceAccounts,
} from '@/hooks/queries/credential-groups'
import { useWorkspaceAccounts } from '@/hooks/queries/credential-groups'
import { credentialGroupKeys } from '@/hooks/queries/utils/credential-group-queries'
const mockRequestJson = apiClientRequestMockFns.mockRequestJson
@@ -30,34 +27,8 @@ const GROUP_ID = 'group-1'
const mountedRoots: Root[] = []
function renderMutation(queryClient: QueryClient) {
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
const container = document.createElement('div')
const root = createRoot(container)
mountedRoots.push(root)
let result: ReturnType<typeof useUpdateCredentialGroupAccess> | undefined
function Probe() {
result = useUpdateCredentialGroupAccess()
return null
}
act(() =>
root.render(
<QueryClientProvider client={queryClient}>
<Probe />
</QueryClientProvider>
)
)
return () => {
if (!result) throw new Error('Credential Group access mutation did not render')
return result
}
}
beforeEach(() => {
mockRequestJson.mockResolvedValue({ revision: 4, allowedWorkflowIds: ['workflow-2'] })
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
})
afterEach(() => {
@@ -66,24 +37,6 @@ afterEach(() => {
})
})
describe('useUpdateCredentialGroupAccess', () => {
it('fails before the request when the access cache has not been loaded', async () => {
const queryClient = new QueryClient({ defaultOptions: { mutations: { retry: false } } })
const getMutation = renderMutation(queryClient)
await expect(
act(async () =>
getMutation().mutateAsync({
workspaceId: WORKSPACE_ID,
groupId: GROUP_ID,
body: { expectedRevision: 3, allowedWorkflowIds: ['workflow-2'] },
})
)
).rejects.toThrow('Credential Group access must be loaded before it can be updated')
expect(mockRequestJson).not.toHaveBeenCalled()
})
})
describe('useWorkspaceAccounts', () => {
it('keeps account records scoped to their workspace while the next workspace loads', async () => {
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
+1 -328
View File
@@ -1,37 +1,18 @@
'use client'
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useMutation, useQuery } from '@tanstack/react-query'
import { requestJson } from '@/lib/api/client/request'
import type { ContractBodyInput } from '@/lib/api/contracts'
import {
type CredentialGroupAccessResponse,
createCredentialGroupMcpConnectorContract,
deleteCredentialGroupEnrollmentContract,
deleteCredentialGroupMcpConnectorContract,
ensureWorkspaceAccountsContract,
getCredentialGroupAccessContract,
getCredentialGroupContract,
inviteCredentialGroupEnrollmentsContract,
resendCredentialGroupEnrollmentContract,
type StartSlackCredentialGroupConfigurationBody,
startSlackCredentialGroupConfigurationContract,
updateCredentialGroupAccessContract,
updateCredentialGroupContract,
updateCredentialGroupMcpConnectorContract,
} from '@/lib/api/contracts/credential-groups'
import { startOrganizationSlackConfigurationContract } from '@/lib/api/contracts/organization-accounts'
import type { ContractJsonResponse } from '@/lib/api/contracts/types'
import { resourceScopeFromOwner } from '@/lib/core/resource-scope'
import type { ManagedMcpConnectorId } from '@/lib/credential-groups/managed-mcp-connectors'
import {
CREDENTIAL_GROUP_ACCESS_STALE_TIME,
CREDENTIAL_GROUP_DETAIL_STALE_TIME,
credentialGroupKeys,
fetchWorkspaceAccounts,
WORKSPACE_ACCOUNTS_STALE_TIME,
} from '@/hooks/queries/utils/credential-group-queries'
import { mcpKeys } from '@/hooks/queries/utils/mcp-keys'
import { invalidateSelectorQueries } from '@/hooks/queries/utils/selector-keys'
export function useWorkspaceAccounts(workspaceId?: string) {
return useQuery({
@@ -45,229 +26,6 @@ export function useWorkspaceAccounts(workspaceId?: string) {
})
}
export function useCredentialGroupDetail(workspaceId?: string, groupId?: string) {
return useInfiniteQuery({
queryKey: credentialGroupKeys.detail(workspaceId, groupId),
queryFn: ({ signal, pageParam }) => {
if (!workspaceId || !groupId)
throw new Error('Credential group detail identifiers are required')
return requestJson(getCredentialGroupContract, {
params: { id: workspaceId, groupId },
query: { limit: 50, ...(pageParam ? { cursor: pageParam } : {}) },
signal,
})
},
initialPageParam: null as string | null,
getNextPageParam: (lastPage: ContractJsonResponse<typeof getCredentialGroupContract>) =>
lastPage.nextCursor ?? undefined,
enabled: Boolean(workspaceId && groupId),
staleTime: CREDENTIAL_GROUP_DETAIL_STALE_TIME,
// An infinite staleTime never goes stale, so the app-wide `retryOnMount: false`
// would cache one transient failure for the life of the QueryClient.
retryOnMount: true,
})
}
interface UseCredentialGroupAccessOptions {
enabled?: boolean
}
export function useCredentialGroupAccess(
workspaceId?: string,
groupId?: string,
{ enabled = true }: UseCredentialGroupAccessOptions = {}
) {
return useQuery({
queryKey: credentialGroupKeys.access(workspaceId, groupId),
queryFn: ({ signal }) => {
if (!workspaceId || !groupId) {
throw new Error('Credential Group access identifiers are required')
}
return requestJson(getCredentialGroupAccessContract, {
params: { id: workspaceId, groupId },
signal,
})
},
enabled: Boolean(workspaceId && groupId && enabled),
staleTime: CREDENTIAL_GROUP_ACCESS_STALE_TIME,
retryOnMount: true,
})
}
export function useUpdateCredentialGroupAccess() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({
workspaceId,
groupId,
body,
}: {
workspaceId: string
groupId: string
body: ContractBodyInput<typeof updateCredentialGroupAccessContract>
}) =>
requestJson(updateCredentialGroupAccessContract, {
params: { id: workspaceId, groupId },
body,
}),
onMutate: async (variables) => {
const queryKey = credentialGroupKeys.access(variables.workspaceId, variables.groupId)
await queryClient.cancelQueries({ queryKey, exact: true })
const cachedAccess = queryClient.getQueryData<CredentialGroupAccessResponse>(queryKey)
if (!cachedAccess) {
throw new Error('Credential Group access must be loaded before it can be updated')
}
return { queryKey, workflows: cachedAccess.workflows }
},
onSuccess: (access, _variables, context) => {
if (!context) throw new Error('Credential Group access mutation context is unavailable')
queryClient.setQueryData<CredentialGroupAccessResponse>(context.queryKey, {
...access,
workflows: context.workflows,
})
},
onSettled: (_data, _error, variables) =>
Promise.all([
queryClient.invalidateQueries({
queryKey: credentialGroupKeys.access(variables.workspaceId, variables.groupId),
exact: true,
}),
invalidateSelectorQueries(queryClient),
]),
})
}
export function useEnsureWorkspaceAccounts() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ workspaceId }: { workspaceId: string }) =>
requestJson(ensureWorkspaceAccountsContract, { params: { id: workspaceId } }),
onSuccess: (_data, variables) =>
Promise.all([
queryClient.invalidateQueries({
queryKey: credentialGroupKeys.workspace(variables.workspaceId),
}),
queryClient.invalidateQueries({
queryKey: mcpKeys.managedCatalogList(variables.workspaceId),
}),
invalidateSelectorQueries(queryClient),
]),
})
}
export function useUpdateCredentialGroup() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({
workspaceId,
groupId,
body,
}: {
workspaceId: string
groupId: string
body: ContractBodyInput<typeof updateCredentialGroupContract>
}) =>
requestJson(updateCredentialGroupContract, {
params: { id: workspaceId, groupId },
body,
}),
// Returned so `mutateAsync` resolves only once the refetch has landed. Callers
// clear their edit buffer on success, which would otherwise fall back onto the
// pre-save cache and flash the old values.
onSettled: (_data, _error, variables) =>
Promise.all([
queryClient.invalidateQueries({
queryKey: credentialGroupKeys.workspace(variables.workspaceId),
}),
queryClient.invalidateQueries({
queryKey: credentialGroupKeys.detail(variables.workspaceId, variables.groupId),
}),
queryClient.invalidateQueries({
queryKey: mcpKeys.managedCatalogList(variables.workspaceId),
}),
invalidateSelectorQueries(queryClient),
]),
})
}
function invalidateManagedMcpConnectorQueries(
queryClient: ReturnType<typeof useQueryClient>,
workspaceId: string,
groupId: string
) {
return Promise.all([
queryClient.invalidateQueries({ queryKey: credentialGroupKeys.workspace(workspaceId) }),
queryClient.invalidateQueries({ queryKey: credentialGroupKeys.detail(workspaceId, groupId) }),
queryClient.invalidateQueries({ queryKey: mcpKeys.serversList(workspaceId) }),
queryClient.invalidateQueries({ queryKey: mcpKeys.managedCatalogList(workspaceId) }),
invalidateSelectorQueries(queryClient),
])
}
export function useCreateCredentialGroupMcpConnector() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({
workspaceId,
groupId,
body,
}: {
workspaceId: string
groupId: string
body: ContractBodyInput<typeof createCredentialGroupMcpConnectorContract>
}) =>
requestJson(createCredentialGroupMcpConnectorContract, {
params: { id: workspaceId, groupId },
body,
}),
onSettled: (_data, _error, variables) =>
invalidateManagedMcpConnectorQueries(queryClient, variables.workspaceId, variables.groupId),
})
}
export function useUpdateCredentialGroupMcpConnector() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({
workspaceId,
groupId,
connectorId,
body,
}: {
workspaceId: string
groupId: string
connectorId: ManagedMcpConnectorId
body: ContractBodyInput<typeof updateCredentialGroupMcpConnectorContract>
}) =>
requestJson(updateCredentialGroupMcpConnectorContract, {
params: { id: workspaceId, groupId, connectorId },
body,
}),
onSettled: (_data, _error, variables) =>
invalidateManagedMcpConnectorQueries(queryClient, variables.workspaceId, variables.groupId),
})
}
export function useDeleteCredentialGroupMcpConnector() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({
workspaceId,
groupId,
connectorId,
}: {
workspaceId: string
groupId: string
connectorId: ManagedMcpConnectorId
}) =>
requestJson(deleteCredentialGroupMcpConnectorContract, {
params: { id: workspaceId, groupId, connectorId },
}),
onSettled: (_data, _error, variables) =>
invalidateManagedMcpConnectorQueries(queryClient, variables.workspaceId, variables.groupId),
})
}
export function useStartSlackCredentialGroupConfiguration() {
return useMutation({
mutationFn: async ({
@@ -302,88 +60,3 @@ export function useStartSlackCredentialGroupConfiguration() {
},
})
}
export function useInviteCredentialGroupEnrollments() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({
workspaceId,
groupId,
body,
}: {
workspaceId: string
groupId: string
body: ContractBodyInput<typeof inviteCredentialGroupEnrollmentsContract>
}) =>
requestJson(inviteCredentialGroupEnrollmentsContract, {
params: { id: workspaceId, groupId },
body,
}),
onSettled: (_data, _error, variables) =>
Promise.all([
queryClient.invalidateQueries({
queryKey: credentialGroupKeys.detail(variables.workspaceId, variables.groupId),
}),
queryClient.invalidateQueries({
queryKey: mcpKeys.managedCatalogList(variables.workspaceId),
}),
invalidateSelectorQueries(queryClient),
]),
})
}
export function useResendCredentialGroupEnrollment() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({
workspaceId,
groupId,
enrollmentId,
}: {
workspaceId: string
groupId: string
enrollmentId: string
}) =>
requestJson(resendCredentialGroupEnrollmentContract, {
params: { id: workspaceId, groupId, enrollmentId },
}),
onSettled: (_data, _error, variables) =>
Promise.all([
queryClient.invalidateQueries({
queryKey: credentialGroupKeys.detail(variables.workspaceId, variables.groupId),
}),
queryClient.invalidateQueries({
queryKey: mcpKeys.managedCatalogList(variables.workspaceId),
}),
invalidateSelectorQueries(queryClient),
]),
})
}
export function useDeleteCredentialGroupEnrollment() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({
workspaceId,
groupId,
enrollmentId,
}: {
workspaceId: string
groupId: string
enrollmentId: string
}) =>
requestJson(deleteCredentialGroupEnrollmentContract, {
params: { id: workspaceId, groupId, enrollmentId },
}),
onSettled: (_data, _error, variables) =>
Promise.all([
queryClient.invalidateQueries({
queryKey: credentialGroupKeys.detail(variables.workspaceId, variables.groupId),
}),
queryClient.invalidateQueries({
queryKey: mcpKeys.managedCatalogList(variables.workspaceId),
}),
invalidateSelectorQueries(queryClient),
]),
})
}
@@ -2,10 +2,7 @@ import { requestJson } from '@/lib/api/client/request'
import type { WorkspaceAccountsSettings } from '@/lib/api/contracts/credential-groups'
import { getWorkspaceAccountsContract } from '@/lib/api/contracts/credential-groups'
export const CREDENTIAL_GROUP_DETAIL_STALE_TIME = Number.POSITIVE_INFINITY
export const WORKSPACE_ACCOUNTS_STALE_TIME = 30 * 1000
export const CREDENTIAL_GROUP_ACCESS_STALE_TIME = 30 * 1000
const CREDENTIAL_GROUP_ACCESS_QUERY_VERSION = 4
export const credentialGroupKeys = {
all: ['workspace-accounts'] as const,
@@ -15,12 +12,6 @@ export const credentialGroupKeys = {
details: () => [...credentialGroupKeys.all, 'detail'] as const,
detail: (workspaceId?: string, groupId?: string) =>
[...credentialGroupKeys.details(), workspaceId ?? '', groupId ?? ''] as const,
access: (workspaceId?: string, groupId?: string) =>
[
...credentialGroupKeys.detail(workspaceId, groupId),
'access',
CREDENTIAL_GROUP_ACCESS_QUERY_VERSION,
] as const,
}
/** The workspace account configuration and provider availability share one cache entry. */
-16
View File
@@ -1,16 +0,0 @@
'use client'
import { useOptionalWorkspaceHostContext } from '@/app/workspace/[workspaceId]/providers/workspace-host-provider'
/**
* Whether per-member knowledge access is on for the routed workspace, as
* `isKnowledgeMemberAccessAvailable` judged it, resolved once into the
* workspace host context.
*
* The single client-side reading of that judgement, so no surface can drift
* into offering a feature the server refuses. Outside a workspace route there
* is no workspace to judge, so it reads false.
*/
export function useMemberAccessAvailable(): boolean {
return useOptionalWorkspaceHostContext()?.features?.knowledgeMemberAccess === true
}
@@ -9,5 +9,4 @@ export * from './google'
export * from './media'
export * from './microsoft'
export * from './onepassword'
export * from './shared'
export * from './workday'
@@ -1,4 +0,0 @@
import { z } from 'zod'
export const imageProxyQuerySchema = z.object({
url: z.string({ error: 'Missing URL parameter' }).min(1, 'Missing URL parameter'),
})
@@ -1,4 +1,3 @@
export * from '@/lib/api/contracts/tools/media/document-parse'
export * from '@/lib/api/contracts/tools/media/image'
export * from '@/lib/api/contracts/tools/media/shared'
export * from '@/lib/api/contracts/tools/media/video'
@@ -1,10 +0,0 @@
import { z } from 'zod'
export const genericToolResponseSchema = z
.object({
success: z.boolean().optional(),
output: z.unknown().optional(),
error: z.string().optional(),
details: z.array(z.unknown()).optional(),
})
.passthrough()
-15
View File
@@ -1,15 +0,0 @@
import { z } from 'zod'
import { COPILOT_REQUEST_MODES } from '@/lib/mothership/constants'
export const v1CopilotChatBodySchema = z.object({
message: z.string().min(1, 'message is required'),
workflowId: z.string().optional(),
workflowName: z.string().optional(),
chatId: z.string().optional(),
mode: z.enum(COPILOT_REQUEST_MODES).optional().default('agent'),
model: z.string().optional(),
autoExecuteTools: z.boolean().optional().default(true),
timeout: z.number().int().min(1000).max(3_600_000).optional().default(3_600_000),
})
export type V1CopilotChatBody = z.output<typeof v1CopilotChatBodySchema>
-1
View File
@@ -1,6 +1,5 @@
export * from '@/lib/api/contracts/v1/admin'
export * from '@/lib/api/contracts/v1/audit-logs'
export * from '@/lib/api/contracts/v1/copilot'
export * from '@/lib/api/contracts/v1/files'
export * from '@/lib/api/contracts/v1/knowledge'
export * from '@/lib/api/contracts/v1/logs'
@@ -1,89 +0,0 @@
import { isValidEmailSyntax, normalizeEmail } from '@sim/utils/string'
import { defineAuthorizedWorkspaceUseCase } from '@/lib/core/application'
import { OrchestrationError } from '@/lib/core/orchestration/types'
import {
credentialGroupDelegationPolicy,
requireCredentialGroupWorkflowActor,
} from '@/lib/credential-groups/application/authorization'
import {
requireCredentialGroupsAvailable,
resolveWorkspaceAccountsContext,
} from '@/lib/credential-groups/application/context'
import { credentialGroupOperations } from '@/lib/credential-groups/application/operations'
import {
CredentialGroupEnrollmentError,
type CredentialGroupEnrollmentStatus,
listCredentialGroupEnrollments,
} from '@/lib/credential-groups/enrollments'
export const CREDENTIAL_GROUP_PEOPLE_STATUSES = [
'invited',
'delivery_failed',
'in_progress',
'completed',
'revoked',
] as const satisfies readonly CredentialGroupEnrollmentStatus[]
export interface ListCredentialGroupPeopleInput {
workspaceId: string
limit: number
cursor?: string
email?: string
statuses?: CredentialGroupEnrollmentStatus[]
}
export const listCredentialGroupPeople = defineAuthorizedWorkspaceUseCase({
operation: credentialGroupOperations.listPeople,
resolveContext: ({ input }: { input: ListCredentialGroupPeopleInput }) =>
resolveWorkspaceAccountsContext(input.workspaceId),
authorizationOptions: { delegation: credentialGroupDelegationPolicy },
authorizeResource({ principal }) {
requireCredentialGroupWorkflowActor(principal)
},
execute: async ({ input, context }) => {
if (context.status !== 'active') {
throw new OrchestrationError('conflict', 'Credential group is disabled')
}
if (!Number.isInteger(input.limit) || input.limit < 1 || input.limit > 100) {
throw new OrchestrationError('validation', 'Limit must be an integer between 1 and 100')
}
const email = input.email ? normalizeEmail(input.email) : undefined
if (email && !isValidEmailSyntax(email)) {
throw new OrchestrationError('validation', 'Email must be a valid address')
}
const statuses = [...new Set(input.statuses ?? [])]
const allowedStatuses = new Set<string>(CREDENTIAL_GROUP_PEOPLE_STATUSES)
if (statuses.some((status) => !allowedStatuses.has(status))) {
throw new OrchestrationError('validation', 'People status filter is invalid')
}
await requireCredentialGroupsAvailable(context.workspaceId)
try {
const page = await listCredentialGroupEnrollments(
context.workspaceId,
context.credentialGroupId,
input.limit,
input.cursor,
{ email, statuses: statuses.length > 0 ? statuses : undefined }
)
return {
people: page.enrollments,
count: page.enrollments.length,
hasMore: page.nextCursor !== null,
nextCursor: page.nextCursor,
}
} catch (error) {
if (error instanceof CredentialGroupEnrollmentError) {
throw new OrchestrationError(
error.status === 400 || error.status === 404
? 'validation'
: error.status === 409
? 'conflict'
: 'internal',
error.message
)
}
throw error
}
},
})
@@ -121,15 +121,6 @@ export const credentialGroupOperations = {
principalKinds: ['delegated'],
delegatedServices: ['executor'],
}),
// permission-group-exempt: read by the executor to resolve an enrolled person's credential; the group's enrollment rows are the gate, and no group key names them
listPeople: defineWorkspaceOperation({
id: 'credential_groups.people.list',
minimumRole: 'read',
workspaceApiKey: 'deny',
capability: 'none',
principalKinds: ['delegated'],
delegatedServices: ['executor'],
}),
// permission-group-exempt: enrolls an outside person in a credential group, not a member in a workspace, so invitations.send does not name it
sendInvite: defineWorkspaceOperation({
id: 'credential_groups.invites.send',
@@ -1,54 +0,0 @@
import type { Logger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth, checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { CloudWatchInputError } from '@/lib/internal/cloudwatch/operations'
type CloudWatchRequestParseResult<T> =
| { success: true; data: { body: T } }
| { success: false; response: Response }
interface CloudWatchHttpRouteConfig<T> {
logger: Logger
parse: (request: NextRequest) => Promise<CloudWatchRequestParseResult<T>>
execute: (input: T, signal?: AbortSignal) => Promise<unknown>
errorMessage: string
auth?: 'internal' | 'session-or-internal'
logError?: (error: unknown) => void
}
export function createCloudWatchHttpRoute<T>({
logger,
parse,
execute,
errorMessage,
auth = 'internal',
logError,
}: CloudWatchHttpRouteConfig<T>) {
return withRouteHandler(async (request: NextRequest) => {
try {
const result =
auth === 'session-or-internal'
? await checkSessionOrInternalAuth(request)
: await checkInternalAuth(request)
if (!result.success || !result.userId) {
return NextResponse.json({ error: result.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parse(request)
if (!parsed.success) return parsed.response
return NextResponse.json(await execute(parsed.data.body, request.signal))
} catch (error) {
if (error instanceof CloudWatchInputError) {
return NextResponse.json({ error: error.message }, { status: error.status })
}
if (logError) logError(error)
else logger.error(errorMessage, { error: toError(error).message })
return NextResponse.json(
{ error: `${errorMessage}: ${toError(error).message}` },
{ status: 500 }
)
}
})
}
-31
View File
@@ -1,31 +0,0 @@
import { db } from '@sim/db'
import { workflow, workflowExecutionLogs } from '@sim/db/schema'
import { eq } from 'drizzle-orm'
/** Minimal log record returned by server-side service lookups. */
export interface LogRecord {
id: string
workspaceId: string
startedAt: Date
workflowName: string | null
}
/**
* Fetches a log record by its primary key, joining the workflow name.
* Returns null if no matching record exists.
*/
export async function getLogById(id: string): Promise<LogRecord | null> {
const [record] = await db
.select({
id: workflowExecutionLogs.id,
workspaceId: workflowExecutionLogs.workspaceId,
startedAt: workflowExecutionLogs.startedAt,
workflowName: workflow.name,
})
.from(workflowExecutionLogs)
.leftJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id))
.where(eq(workflowExecutionLogs.id, id))
.limit(1)
return record ?? null
}
-207
View File
@@ -1,207 +0,0 @@
import { dbReplica } from '@sim/db'
import { workflow, workflowExecutionLogs } from '@sim/db/schema'
import { and, eq, sql } from 'drizzle-orm'
import { assertValidTimezone } from '@/lib/core/utils/timezone'
import { buildFilterConditions } from '@/lib/logs/filters'
import { expandFolderIdsWithDescendants } from '@/lib/logs/folder-expansion'
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
/**
* Server-side aggregation over workflow execution logs for the copilot
* `query_logs` stats view: per-workflow, optionally calendar-bucketed counts by
* status, under the same filter set as the list view. Exists so a model never
* has to paginate the list and count client-side. Job (Sim-agent) executions
* are not included — same scope as the Logs dashboard stats.
*/
export interface StatsLogsParams {
workspaceId: string
level?: string
workflowIds?: string
folderIds?: string
triggers?: string
startDate?: string
endDate?: string
search?: string
workflowName?: string
folderName?: string
/** Calendar bucketing for the per-workflow series; omit for totals only. */
bucket?: 'day' | 'hour'
/** IANA timezone the buckets are computed in. Defaults to UTC. */
timezone?: string
}
export interface LogStatsBucket {
/** Bucket start in the requested timezone (naive local timestamp). */
start: string
executions: number
byStatus: Record<string, number>
avgDurationMs: number
}
export interface WorkflowLogStats {
workflowId: string
workflowName: string
executions: number
byStatus: Record<string, number>
avgDurationMs: number
buckets?: LogStatsBucket[]
}
export interface LogStatsResponse {
bucket: 'day' | 'hour' | null
timezone: string
totals: { executions: number; byStatus: Record<string, number>; avgDurationMs: number }
workflows: WorkflowLogStats[]
/** Set when more workflows matched than are returned (ordered by executions). */
workflowsTruncated?: boolean
}
const MAX_WORKFLOWS = 100
interface StatsAccumulator {
executions: number
byStatus: Record<string, number>
durationSumMs: number
durationCount: number
}
function newAccumulator(): StatsAccumulator {
return { executions: 0, byStatus: {}, durationSumMs: 0, durationCount: 0 }
}
function accumulate(acc: StatsAccumulator, status: string, row: RawStatsRow): void {
acc.executions += Number(row.executions)
acc.byStatus[status] = (acc.byStatus[status] ?? 0) + Number(row.executions)
acc.durationSumMs += Number(row.durationSumMs)
acc.durationCount += Number(row.durationCount)
}
function avgOf(acc: StatsAccumulator): number {
return acc.durationCount > 0 ? Math.round(acc.durationSumMs / acc.durationCount) : 0
}
interface RawStatsRow {
workflowId: string
workflowName: string
bucketStart: string | null
status: string | null
executions: number
durationSumMs: number
durationCount: number
}
export async function statsLogs(
params: StatsLogsParams,
userId: string
): Promise<LogStatsResponse> {
const timezone = params.timezone ?? 'UTC'
assertValidTimezone(timezone)
const bucket = params.bucket ?? null
const access = await checkWorkspaceAccess(params.workspaceId, userId)
if (!access.hasAccess) {
return {
bucket,
timezone,
totals: { executions: 0, byStatus: {}, avgDurationMs: 0 },
workflows: [],
}
}
const folderIds = params.folderIds
? await expandFolderIdsWithDescendants(params.workspaceId, params.folderIds)
: params.folderIds
const p = { ...params, folderIds }
const workspaceFilter = eq(workflowExecutionLogs.workspaceId, p.workspaceId)
const commonFilters = buildFilterConditions(p, { useSimpleLevelFilter: true })
const whereCondition = commonFilters ? and(workspaceFilter, commonFilters) : workspaceFilter
const bucketExpr = bucket
? sql<
string | null
>`to_char(date_trunc(${bucket}, ${workflowExecutionLogs.startedAt} AT TIME ZONE ${timezone}), 'YYYY-MM-DD"T"HH24:MI:SS')`
: sql<string | null>`NULL`
const rows = (await dbReplica
.select({
workflowId: sql<string>`COALESCE(${workflowExecutionLogs.workflowId}, 'deleted')`,
workflowName: sql<string>`COALESCE(${workflow.name}, 'Deleted Workflow')`,
bucketStart: bucketExpr.as('bucket_start'),
status: workflowExecutionLogs.status,
executions: sql<number>`COUNT(*)`,
durationSumMs: sql<number>`COALESCE(SUM(${workflowExecutionLogs.totalDurationMs}) FILTER (WHERE ${workflowExecutionLogs.totalDurationMs} > 0), 0)`,
durationCount: sql<number>`COUNT(*) FILTER (WHERE ${workflowExecutionLogs.totalDurationMs} > 0)`,
})
.from(workflowExecutionLogs)
.leftJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id))
.where(whereCondition)
.groupBy(
sql`COALESCE(${workflowExecutionLogs.workflowId}, 'deleted')`,
sql`COALESCE(${workflow.name}, 'Deleted Workflow')`,
sql`bucket_start`,
workflowExecutionLogs.status
)) as RawStatsRow[]
const totals = newAccumulator()
const byWorkflow = new Map<
string,
{ workflowName: string; overall: StatsAccumulator; buckets: Map<string, StatsAccumulator> }
>()
for (const row of rows) {
const status = row.status ?? 'unknown'
accumulate(totals, status, row)
let wf = byWorkflow.get(row.workflowId)
if (!wf) {
wf = { workflowName: row.workflowName, overall: newAccumulator(), buckets: new Map() }
byWorkflow.set(row.workflowId, wf)
}
accumulate(wf.overall, status, row)
if (bucket && row.bucketStart) {
let bucketAcc = wf.buckets.get(row.bucketStart)
if (!bucketAcc) {
bucketAcc = newAccumulator()
wf.buckets.set(row.bucketStart, bucketAcc)
}
accumulate(bucketAcc, status, row)
}
}
const workflows: WorkflowLogStats[] = Array.from(byWorkflow.entries())
.map(([workflowId, wf]) => ({
workflowId,
workflowName: wf.workflowName,
executions: wf.overall.executions,
byStatus: wf.overall.byStatus,
avgDurationMs: avgOf(wf.overall),
...(bucket
? {
buckets: Array.from(wf.buckets.entries())
.sort(([a], [b]) => a.localeCompare(b))
.map(([start, acc]) => ({
start,
executions: acc.executions,
byStatus: acc.byStatus,
avgDurationMs: avgOf(acc),
})),
}
: {}),
}))
.sort((a, b) => b.executions - a.executions)
const truncated = workflows.length > MAX_WORKFLOWS
return {
bucket,
timezone,
totals: {
executions: totals.executions,
byStatus: totals.byStatus,
avgDurationMs: avgOf(totals),
},
workflows: truncated ? workflows.slice(0, MAX_WORKFLOWS) : workflows,
...(truncated ? { workflowsTruncated: true } : {}),
}
}
-22
View File
@@ -1,22 +0,0 @@
import { toError } from '@sim/utils/errors'
export interface MermaidValidationResult {
ok: boolean
error?: string
errorName?: string
}
export async function validateMermaidSource(source: string): Promise<MermaidValidationResult> {
try {
const { default: mermaid } = await import('mermaid')
await mermaid.parse(source)
return { ok: true }
} catch (error) {
const err = toError(error)
return {
ok: false,
error: err.message || 'Invalid Mermaid diagram',
errorName: err.name || 'MermaidParseError',
}
}
}
@@ -1,15 +0,0 @@
import { MANAGED_MCP_DELEGATION_AUDIENCE } from '@/lib/credentials/application/authorization'
import { credentialOperations } from '@/lib/credentials/application/operations'
import { createCopilotApplicationAdapter } from '@/lib/mothership/application/application-adapter'
import { COPILOT_APPLICATION_DELEGATION_TTL_MS } from '@/lib/mothership/auth/application-delegation'
export const executeCopilotManagedMcpUseCase = createCopilotApplicationAdapter({
domain: 'managed MCP',
delegation: {
audience: MANAGED_MCP_DELEGATION_AUDIENCE,
ttlMs: COPILOT_APPLICATION_DELEGATION_TTL_MS,
createDelegationId: (context) => `copilot-tool:${context.toolCallId}`,
},
operations: { use: credentialOperations.useManagedMcp },
projectResourceScope: (input: { credentialId: string }) => ({ credentialId: input.credentialId }),
})
@@ -1,126 +0,0 @@
import type { Principal } from '@sim/auth/principal'
import { createLogger } from '@sim/logger'
import { CodeLanguage } from '@/lib/execution/languages'
import { executeInSandbox } from '@/lib/execution/remote-sandbox'
import { DocCompileUserError } from '@/lib/mothership/tools/server/files/doc-compile-error'
import { compileDoc } from './doc-compile'
const logger = createLogger('CopilotDocRecalc')
const RECALC_TIMEOUT_MS = 150_000
const MAX_REPORTED_ERRORS = 50
export interface XlsxCellError {
sheet: string
cell: string
error: string
}
export interface XlsxRecalcResult {
ok: boolean
errors: XlsxCellError[]
}
/**
* Scans an .xlsx workbook for spilled error values (#REF!/#DIV/0!/etc.) — the
* spreadsheet equivalent of the visual QA loop.
*
* Precondition: the binary must already be recalculated (cached values present).
* compileDoc bakes a LibreOffice recalc into every xlsx artifact, so the input
* here always has cached values — meaning we can read them with openpyxl
* (data_only) directly and skip a second LibreOffice cold-start. Throws on
* sandbox/infra failure.
*/
export async function recalcXlsx(args: {
binary: Buffer
workspaceId: string
}): Promise<XlsxRecalcResult> {
const script = `
import json, openpyxl
ERR = {"#REF!", "#DIV/0!", "#VALUE!", "#NAME?", "#N/A", "#NULL!", "#NUM!"}
# Input is already recalculated by compileDoc, so cached values are present.
wb = openpyxl.load_workbook("/home/user/input.xlsx", data_only=True)
errors = []
for ws in wb.worksheets:
for row in ws.iter_rows():
for c in row:
if isinstance(c.value, str) and c.value.strip() in ERR:
errors.append({"sheet": ws.title, "cell": c.coordinate, "error": c.value.strip()})
print("__SIM_RESULT__=" + json.dumps({"ok": len(errors) == 0, "errors": errors[:${MAX_REPORTED_ERRORS}]}))
`.trim()
const result = await executeInSandbox({
code: script,
language: CodeLanguage.Python,
timeoutMs: RECALC_TIMEOUT_MS,
sandboxKind: 'doc',
sandboxFiles: [
{
path: '/home/user/input.xlsx',
content: args.binary.toString('base64'),
encoding: 'base64',
},
],
})
if (result.error) {
throw new Error(`Spreadsheet recalc failed: ${result.error}`)
}
const payload = result.result as XlsxRecalcResult | null
if (!payload || typeof payload.ok !== 'boolean') {
logger.warn('Recalc returned no structured result', { workspaceId: args.workspaceId })
return { ok: true, errors: [] }
}
return { ok: payload.ok, errors: Array.isArray(payload.errors) ? payload.errors : [] }
}
/** Single-line summary of the first few formula errors, for the compiled-check result. */
export function formatXlsxErrors(errors: XlsxCellError[]): string {
return `${errors.length} formula error(s): ${errors
.slice(0, 5)
.map((e) => `${e.sheet}!${e.cell}=${e.error}`)
.join(', ')}`
}
export interface CompiledCheckResult {
ok: boolean
error?: string
errors?: XlsxCellError[]
}
/**
* Compiles a generated doc (and, for xlsx, recalc-scans its formulas) to verify
* it builds — the shared body behind the /compiled-check route and the VFS
* compiled-check read. Returns { ok: false } only for a DocCompileUserError (the
* agent's script is wrong); infra failures (E2B/S3) rethrow so callers surface a
* 5xx instead of telling the agent to fix its script.
*/
export async function runE2BCompiledCheck(args: {
source: string
fileName: string
workspaceId: string
ext: string
principal: Principal
}): Promise<CompiledCheckResult> {
try {
const compiled = await compileDoc({
source: args.source,
fileName: args.fileName,
workspaceId: args.workspaceId,
filePrincipal: args.principal,
})
if (args.ext === 'xlsx') {
const recalc = await recalcXlsx({ binary: compiled.buffer, workspaceId: args.workspaceId })
return recalc.ok
? { ok: true }
: { ok: false, error: formatXlsxErrors(recalc.errors), errors: recalc.errors }
}
return { ok: true }
} catch (err) {
if (err instanceof DocCompileUserError) return { ok: false, error: err.message }
throw err
}
}
-110
View File
@@ -1,110 +0,0 @@
import { db } from '@sim/db'
import {
permissionGroup,
permissionGroupMember,
permissionGroupWorkspace,
workspace,
} from '@sim/db/schema'
import { asc, count, desc, eq, inArray } from 'drizzle-orm'
import {
type ActivePermissionGroupRestriction,
getActivePermissionGroupRestrictions,
} from '@/lib/permission-groups/features'
import type { PermissionGroupConfig } from '@/lib/permission-groups/fields'
/** A workspace reference (id + display name). */
export interface OrgWorkspaceRef {
id: string
name: string
}
/**
* List an organization's workspaces ({id, name}), ordered by name.
*
* Lifted from the permission-groups route utils so non-route surfaces (the
* copilot VFS) can read the org's workspace map without importing app/api.
*/
export async function listOrganizationWorkspaceRefs(
organizationId: string
): Promise<OrgWorkspaceRef[]> {
return db
.select({ id: workspace.id, name: workspace.name })
.from(workspace)
.where(eq(workspace.organizationId, organizationId))
.orderBy(asc(workspace.name))
}
export interface PermissionGroupRosterEntry {
id: string
name: string
description: string | null
isDefault: boolean
memberCount: number
workspaces: OrgWorkspaceRef[]
activeRestrictions: ActivePermissionGroupRestriction[]
}
/**
* The org-admin roster: every permission group with its member count, the
* workspaces it targets, and the restrictions its config actually activates.
* The same joins the settings surface runs, without the route envelope.
*/
export async function listPermissionGroupRoster(
organizationId: string
): Promise<PermissionGroupRosterEntry[]> {
const groups = await db
.select({
id: permissionGroup.id,
name: permissionGroup.name,
description: permissionGroup.description,
config: permissionGroup.config,
isDefault: permissionGroup.isDefault,
})
.from(permissionGroup)
.where(eq(permissionGroup.organizationId, organizationId))
.orderBy(desc(permissionGroup.createdAt))
const groupIds = groups.map((group) => group.id)
const memberCounts = groupIds.length
? await db
.select({
permissionGroupId: permissionGroupMember.permissionGroupId,
count: count(),
})
.from(permissionGroupMember)
.where(inArray(permissionGroupMember.permissionGroupId, groupIds))
.groupBy(permissionGroupMember.permissionGroupId)
: []
const countByGroupId = new Map(memberCounts.map((row) => [row.permissionGroupId, row.count]))
const workspaceRows = groupIds.length
? await db
.select({
groupId: permissionGroupWorkspace.permissionGroupId,
id: workspace.id,
name: workspace.name,
})
.from(permissionGroupWorkspace)
.innerJoin(workspace, eq(permissionGroupWorkspace.workspaceId, workspace.id))
.where(inArray(permissionGroupWorkspace.permissionGroupId, groupIds))
.orderBy(asc(workspace.name))
: []
const workspacesByGroupId = new Map<string, OrgWorkspaceRef[]>()
for (const row of workspaceRows) {
const list = workspacesByGroupId.get(row.groupId) ?? []
list.push({ id: row.id, name: row.name })
workspacesByGroupId.set(row.groupId, list)
}
return groups.map((group) => ({
id: group.id,
name: group.name,
description: group.description,
isDefault: group.isDefault,
memberCount: countByGroupId.get(group.id) ?? 0,
workspaces: workspacesByGroupId.get(group.id) ?? [],
activeRestrictions: getActivePermissionGroupRestrictions(
(group.config as PermissionGroupConfig | null) ?? null
),
}))
}
@@ -1,5 +0,0 @@
export {
type PerformRestoreResourceParams,
performRestoreResource,
type RestorableResourceType,
} from './restore-resource'
@@ -1,217 +0,0 @@
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import type { FolderResourceType } from '@/lib/api/contracts/folders'
import { restoreFolder } from '@/lib/folders/orchestration'
import {
getRestorableKnowledgeBase,
performRestoreKnowledgeBase,
} from '@/lib/knowledge/orchestration'
import type { MothershipResource } from '@/lib/mothership/resources/types'
import type { ToolExecutionResult } from '@/lib/mothership/tool-executor/types'
import { performRestoreTable } from '@/lib/table/orchestration'
import { getTableById } from '@/lib/table/service'
import { getWorkspaceFile } from '@/lib/uploads/contexts/workspace/workspace-file-manager'
import { performRestoreWorkflow } from '@/lib/workflows/orchestration'
import { getWorkflowById } from '@/lib/workflows/utils'
import {
performRestoreWorkspaceFile,
performRestoreWorkspaceFileFolder,
} from '@/lib/workspace-files/orchestration'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
const logger = createLogger('RestoreResourceOrchestration')
export type RestorableResourceType =
| 'workflow'
| 'table'
| 'file'
| 'knowledgebase'
| 'folder'
| 'file_folder'
| 'knowledge_folder'
| 'table_folder'
/**
* Folder trees the generic engine restores, keyed by the restorable type that names them.
* `'folder'` stays the workflow tree so the existing tool contract does not shift meaning;
* the other trees get their own names, mirroring how `file_folder` already sits beside it.
*
* `knowledge_folder` and `table_folder` are accepted here and by the tool handler, but the
* model cannot emit them yet: the `restore_resource` parameter enum lives in the copilot
* service's `contracts/tool-catalog-v1.json`, which is a different repository and is mirrored
* into `lib/mothership/generated/**` by `scripts/sync-tool-catalog.ts`. Widening it there makes
* these reachable with no further change on this side.
*/
type RestorableFolderType = 'folder' | 'knowledge_folder' | 'table_folder'
/**
* Deliberately a total `Record` over the folder types, not a `Partial` one: adding a tree to
* `RestorableFolderType` without a mapping has to fail the build *here*, at the mapping. A
* `Partial` still compiles with the tree missing — the lookup widens to
* `FolderResourceType | undefined`, so the error moves to the `restoreFolder` call site, and
* suppressing it there leaves the cascade resolving an undefined folder config.
*/
const FOLDER_RESOURCE_TYPE_BY_RESTORABLE: Record<RestorableFolderType, FolderResourceType> = {
folder: 'workflow',
knowledge_folder: 'knowledge_base',
table_folder: 'table',
}
export interface PerformRestoreResourceParams {
type: RestorableResourceType
id: string
userId: string
workspaceId: string
requestId?: string
}
async function hasWriteAccess(
userId: string,
callerWorkspaceId: string,
resourceWorkspaceId: string | null | undefined
): Promise<boolean> {
if (!resourceWorkspaceId || resourceWorkspaceId !== callerWorkspaceId) return false
const permission = await getUserEntityPermissions(userId, 'workspace', resourceWorkspaceId)
return permission === 'write' || permission === 'admin'
}
function success(
output: Record<string, unknown>,
resources?: MothershipResource[]
): ToolExecutionResult {
return { success: true, output, resources }
}
export async function performRestoreResource(
params: PerformRestoreResourceParams
): Promise<ToolExecutionResult> {
const { type, id, userId, workspaceId } = params
const requestId = params.requestId ?? generateId().slice(0, 8)
try {
switch (type) {
case 'workflow': {
const existing = await getWorkflowById(id, { includeArchived: true })
if (!existing || !(await hasWriteAccess(userId, workspaceId, existing.workspaceId))) {
return { success: false, error: 'Workflow not found' }
}
const result = await performRestoreWorkflow({ workflowId: id, userId, requestId })
if (!result.success || !result.workflow) {
return { success: false, error: result.error || 'Failed to restore workflow' }
}
logger.info('Workflow restored via restore_resource', { workflowId: id })
return success({ type, id, name: result.workflow.name }, [
{ type: 'workflow', id, title: result.workflow.name || id },
])
}
case 'table': {
const existing = await getTableById(id, { includeArchived: true })
if (!existing || !(await hasWriteAccess(userId, workspaceId, existing.workspaceId))) {
return { success: false, error: 'Table not found' }
}
const result = await performRestoreTable({ tableId: id, userId, requestId })
if (!result.success || !result.table) {
return { success: false, error: result.error || 'Failed to restore table' }
}
logger.info('Table restored via restore_resource', { tableId: id, name: result.table.name })
return success({ type, id, name: result.table.name }, [
{ type: 'table', id, title: result.table.name },
])
}
case 'file': {
if (!(await hasWriteAccess(userId, workspaceId, workspaceId))) {
return { success: false, error: 'File not found' }
}
const result = await performRestoreWorkspaceFile({ workspaceId, fileId: id, userId })
if (!result.success) {
return { success: false, error: result.error || 'Failed to restore file' }
}
const file = await getWorkspaceFile(workspaceId, id)
const fileName = file?.name || id
logger.info('File restored via restore_resource', { fileId: id, name: fileName })
return success({ type, id, name: fileName }, [{ type: 'file', id, title: fileName }])
}
case 'knowledgebase': {
const existing = await getRestorableKnowledgeBase(id)
if (!existing || !(await hasWriteAccess(userId, workspaceId, existing.workspaceId))) {
return { success: false, error: 'Knowledge base not found' }
}
const result = await performRestoreKnowledgeBase({
knowledgeBaseId: id,
userId,
source: 'agent',
requestId,
})
if (!result.success) {
return { success: false, error: result.error }
}
logger.info('Knowledge base restored via restore_resource', { knowledgeBaseId: id })
return success({ type, id, name: result.knowledgeBase.name }, [
{ type: 'knowledgebase', id, title: result.knowledgeBase.name },
])
}
case 'folder':
case 'knowledge_folder':
case 'table_folder': {
if (!(await hasWriteAccess(userId, workspaceId, workspaceId))) {
return { success: false, error: 'Folder not found' }
}
const result = await restoreFolder({
folderId: id,
workspaceId,
userId,
resourceType: FOLDER_RESOURCE_TYPE_BY_RESTORABLE[type],
})
if (!result.success) {
return { success: false, error: result.error || 'Failed to restore folder' }
}
logger.info('Folder restored via restore_resource', { folderId: id, type })
return success({ type, id, restoredItems: result.restoredItems })
}
case 'file_folder': {
if (!(await hasWriteAccess(userId, workspaceId, workspaceId))) {
return { success: false, error: 'File folder not found' }
}
const result = await performRestoreWorkspaceFileFolder({
workspaceId,
folderId: id,
userId,
})
if (!result.success || !result.folder || !result.restoredItems) {
return { success: false, error: result.error || 'Failed to restore file folder' }
}
logger.info('File folder restored via restore_resource', {
folderId: id,
restoredItems: result.restoredItems,
})
return success({
type,
id,
name: result.folder.name,
restoredItems: result.restoredItems,
})
}
}
} catch (error) {
logger.error('Failed to restore resource via restore_resource', { type, id, error })
return { success: false, error: toError(error).message }
}
}
@@ -432,14 +432,6 @@ export const workflowOperations = {
capability: 'none',
...WORKFLOW_READ_PRINCIPAL_POLICY,
}),
// permission-group-exempt: comparing references across two versions reads workflow content the caller may already open
compareReferences: defineWorkspaceOperation({
id: 'workflows.versions.compare_references',
minimumRole: 'read',
workspaceApiKey: 'deny',
capability: 'none',
...COPILOT_WORKFLOW_PRINCIPAL_POLICY,
}),
// permission-group-exempt: an export returns the graph its reader can already open; logs.export withholds execution logs, not definitions
export: defineWorkspaceOperation({
id: 'workflows.export',
@@ -1,72 +0,0 @@
import type { Principal } from '@sim/auth/principal'
import { OrchestrationError } from '@/lib/core/orchestration/types'
import { defineAuthorizedWorkflowUseCase } from '@/lib/workflows/application/authorized-workflow-use-case'
import { resolveActiveWorkflowApplicationContext } from '@/lib/workflows/application/context'
import { workflowOperations } from '@/lib/workflows/application/operations'
import { assertedWorkflowWorkspaceId } from '@/lib/workflows/application/principal-scope'
import {
getWorkflowDeploymentVersion,
loadWorkflowFromNormalizedTables,
} from '@/lib/workflows/persistence/utils'
import type { WorkflowState } from '@/stores/workflows/workflow/types'
export type WorkflowStateReference = number | 'live' | 'draft'
export interface ResolvedWorkflowStateReference {
state: WorkflowState
ref: string
version?: number
isActive?: boolean
createdAt?: string
}
export interface ReadWorkflowStateReferencesInput {
workflowId: string
assertedWorkspaceId?: string
references: [WorkflowStateReference, WorkflowStateReference]
}
async function loadReference(
workflowId: string,
reference: WorkflowStateReference
): Promise<ResolvedWorkflowStateReference> {
if (reference === 'draft') {
const state = await loadWorkflowFromNormalizedTables(workflowId)
if (!state) throw new OrchestrationError('not_found', 'Workflow has no draft state')
return { state: state as WorkflowState, ref: 'draft' }
}
const row = await getWorkflowDeploymentVersion(
workflowId,
reference === 'live' ? 'active' : reference
)
if (!row?.state) throw new OrchestrationError('not_found', 'Deployment version not found')
return {
state: row.state as WorkflowState,
ref: reference === 'live' ? 'live' : String(reference),
version: row.version,
isActive: row.isActive,
createdAt: row.createdAt?.toISOString(),
}
}
export const readWorkflowStateReferences = defineAuthorizedWorkflowUseCase({
operation: workflowOperations.compareReferences,
resolveContext: ({
principal,
input,
}: {
principal: Principal
input: ReadWorkflowStateReferencesInput
}) =>
resolveActiveWorkflowApplicationContext({
workflowId: input.workflowId,
assertedWorkspaceId: assertedWorkflowWorkspaceId(principal, input.assertedWorkspaceId),
}),
async execute({ input, context }) {
const [first, second] = await Promise.all(
input.references.map((reference) => loadReference(context.workflowId, reference))
)
return { references: [first, second] as const }
},
})
@@ -140,30 +140,6 @@ export const fileOperations = {
capability: 'files.use',
...ALL_FILE_TOOL_PRINCIPAL_POLICY,
}),
createVfsFolders: defineWorkspaceOperation({
id: 'files.vfs.folders.create',
minimumRole: 'write',
workspaceApiKey: 'deny',
capability: 'files.use',
principalKinds: ['delegated'],
delegatedServices: ['copilot'],
}),
relocateVfsItems: defineWorkspaceOperation({
id: 'files.vfs.relocate',
minimumRole: 'write',
workspaceApiKey: 'deny',
capability: 'files.use',
principalKinds: ['delegated'],
delegatedServices: ['copilot'],
}),
deleteVfsItems: defineWorkspaceOperation({
id: 'files.vfs.delete',
minimumRole: 'write',
workspaceApiKey: 'deny',
capability: 'files.use',
principalKinds: ['delegated'],
delegatedServices: ['copilot'],
}),
delete: defineWorkspaceOperation({
id: 'files.delete',
oauthScope: 'api:write',
@@ -1,487 +0,0 @@
import { AuditAction, AuditResourceType } from '@sim/audit'
import { resolvePrincipalAttribution } from '@sim/auth/principal'
import { asOrchestrationError, OrchestrationError } from '@/lib/core/orchestration/types'
import { buildFolderPath } from '@/lib/folders/paths'
import { notifyWorkspaceFilesChanged } from '@/lib/realtime/notify'
import {
bulkArchiveWorkspaceFileItems,
createWorkspaceFileFolderAtPath,
findWorkspaceFileFolderIdByPath,
getWorkspaceFileByName,
loadWorkspaceFileOperationContext,
moveRenameWorkspaceFile,
relocateWorkspaceFileFolderByPath,
WorkspaceFileFolderConflictError,
} from '@/lib/uploads/contexts/workspace'
import { VfsPathLimitError, validateVfsPathSegments } from '@/lib/vfs/limits'
import { defineAuthorizedWorkspaceFileUseCase } from '@/lib/workspace-files/application/authorized-workspace-file-use-case'
import { fileOperations } from '@/lib/workspace-files/application/operations'
const MAX_FILE_VFS_ITEMS = 100
export interface WorkspaceFileVfsPathReference {
source: string
segments: string[]
}
export interface WorkspaceFileVfsDestination {
segments: string[]
trailingSlash: boolean
}
export interface WorkspaceFileVfsOutcome {
source: string
targetSegments?: string[]
resourceType: 'file' | 'folder'
resourceId?: string
error?: string
}
interface CreatedFolder {
id: string
name: string
path: string
}
interface RelocatedFile {
id: string
name: string
moved: boolean
renamed: boolean
}
interface RelocatedFolder {
id: string
name: string
sourcePath: string
destinationPath: string
}
function normalizeReferences(
references: readonly WorkspaceFileVfsPathReference[]
): WorkspaceFileVfsPathReference[] {
if (references.length > MAX_FILE_VFS_ITEMS) {
throw new OrchestrationError(
'validation',
`File VFS commands cannot exceed ${MAX_FILE_VFS_ITEMS} items`
)
}
const unique = new Map<string, WorkspaceFileVfsPathReference>()
for (const reference of references) {
try {
validateVfsPathSegments(reference.segments)
} catch (error) {
if (error instanceof VfsPathLimitError) {
throw new OrchestrationError('validation', error.message)
}
throw error
}
const key = buildFolderPath(reference.segments)
if (!unique.has(key)) unique.set(key, reference)
}
if (unique.size === 0) throw new OrchestrationError('validation', 'At least one path is required')
return [...unique.values()]
}
/**
* The per-item message for a failure the caller can act on, or a rethrow for a fault
* that should fail the whole command.
*
* Every caller-fixable failure here carries an orchestration class — the folder and
* move conflicts classify themselves as `conflict`, and `FolderPathError` as
* `validation` — so the classified branch covers them all. Anything unclassified, or
* classified `internal`, is a fault and propagates.
*/
function expectedOutcomeMessage(error: unknown): string {
const classified = asOrchestrationError(error)
if (classified && classified.code !== 'internal') return classified.message
throw error
}
async function ensureFolderPath(params: {
workspaceId: string
userId: string
segments: readonly string[]
createdFolders: CreatedFolder[]
}): Promise<string | null> {
let folderId: string | null = null
for (let index = 0; index < params.segments.length; index += 1) {
const segments = params.segments.slice(0, index + 1)
const existing = await findWorkspaceFileFolderIdByPath(params.workspaceId, [...segments])
if (existing) {
folderId = existing
continue
}
const path = buildFolderPath(segments)
try {
const created = await createWorkspaceFileFolderAtPath({
workspaceId: params.workspaceId,
userId: params.userId,
path,
})
folderId = created.folder.id
params.createdFolders.push({
id: created.folder.id,
name: created.folder.name,
path: created.path,
})
} catch (error) {
if (error instanceof WorkspaceFileFolderConflictError) {
const concurrentlyCreated = await findWorkspaceFileFolderIdByPath(params.workspaceId, [
...segments,
])
if (concurrentlyCreated) {
folderId = concurrentlyCreated
continue
}
}
throw error
}
}
return folderId
}
async function resolveSource(
workspaceId: string,
reference: WorkspaceFileVfsPathReference
): Promise<
| { source: string; file: NonNullable<Awaited<ReturnType<typeof getWorkspaceFileByName>>> }
| { source: string; folderId: string }
| { source: string; error: string }
> {
if (reference.segments.length === 0) {
return { source: reference.source, error: 'Source must name a file or folder under files/' }
}
const parentSegments = reference.segments.slice(0, -1)
const folderId =
parentSegments.length === 0
? null
: await findWorkspaceFileFolderIdByPath(workspaceId, parentSegments)
if (parentSegments.length === 0 || folderId) {
const file = await getWorkspaceFileByName(workspaceId, reference.segments.at(-1) as string, {
folderId,
})
if (file) return { source: reference.source, file }
}
const sourceFolderId = await findWorkspaceFileFolderIdByPath(workspaceId, reference.segments)
return sourceFolderId
? { source: reference.source, folderId: sourceFolderId }
: { source: reference.source, error: `Not found: ${reference.source}` }
}
function createdFolderAudits(folders: readonly CreatedFolder[]) {
return folders.map((folder) => ({
action: AuditAction.FOLDER_CREATED,
resourceType: AuditResourceType.FOLDER,
resourceId: folder.id,
resourceName: folder.name,
description: `Created file folder "${folder.path}"`,
metadata: { path: folder.path, folderResourceType: 'file' },
}))
}
export interface CreateWorkspaceFileVfsFoldersInput {
workspaceId: string
paths: WorkspaceFileVfsPathReference[]
}
export const createWorkspaceFileVfsFolders = defineAuthorizedWorkspaceFileUseCase({
operation: fileOperations.createVfsFolders,
resolveContext: ({ input }: { input: CreateWorkspaceFileVfsFoldersInput }) =>
loadWorkspaceFileOperationContext(input.workspaceId).then((context) => {
if (!context) throw new OrchestrationError('not_found', 'Workspace not found')
return context
}),
async execute({ principal, input, context }) {
const paths = normalizeReferences(input.paths)
const userId = resolvePrincipalAttribution(principal, {
workspaceBillingOwnerUserId: context.billedAccountUserId,
}).attributedUserId
const createdFolders: CreatedFolder[] = []
const outcomes: WorkspaceFileVfsOutcome[] = []
for (const path of paths) {
if (path.segments.length === 0) {
outcomes.push({
source: path.source,
resourceType: 'folder',
error: 'Path must include at least one folder segment',
})
continue
}
try {
const folderId = await ensureFolderPath({
workspaceId: context.workspaceId,
userId,
segments: path.segments,
createdFolders,
})
outcomes.push({
source: path.source,
targetSegments: path.segments,
resourceType: 'folder',
resourceId: folderId ?? undefined,
})
} catch (error) {
outcomes.push({
source: path.source,
resourceType: 'folder',
error: expectedOutcomeMessage(error),
})
}
}
return { outcomes, createdFolders }
},
projectAudit: ({ result }) => createdFolderAudits(result.createdFolders),
afterSuccess: ({ context, result }) =>
result.createdFolders.length > 0 ? notifyWorkspaceFilesChanged(context.workspaceId) : undefined,
})
export interface RelocateWorkspaceFileVfsItemsInput {
workspaceId: string
sources: WorkspaceFileVfsPathReference[]
destination: WorkspaceFileVfsDestination
}
export const relocateWorkspaceFileVfsItems = defineAuthorizedWorkspaceFileUseCase({
operation: fileOperations.relocateVfsItems,
resolveContext: ({ input }: { input: RelocateWorkspaceFileVfsItemsInput }) =>
loadWorkspaceFileOperationContext(input.workspaceId).then((context) => {
if (!context) throw new OrchestrationError('not_found', 'Workspace not found')
return context
}),
async execute({ principal, input, context }) {
const sources = normalizeReferences(input.sources)
try {
validateVfsPathSegments(input.destination.segments)
} catch (error) {
if (error instanceof VfsPathLimitError) {
throw new OrchestrationError('validation', error.message)
}
throw error
}
const references = []
for (const source of sources) {
references.push(await resolveSource(context.workspaceId, source))
}
const userId = resolvePrincipalAttribution(principal, {
workspaceBillingOwnerUserId: context.billedAccountUserId,
}).attributedUserId
const destinationPath = buildFolderPath(input.destination.segments)
const existingDestination = await findWorkspaceFileFolderIdByPath(
context.workspaceId,
input.destination.segments
)
const dirMode =
input.destination.segments.length === 0 ||
input.destination.trailingSlash ||
existingDestination !== null
if (!dirMode && sources.length > 1) {
throw new OrchestrationError(
'validation',
`With multiple sources the destination must be a folder. "${destinationPath}" does not exist — end it with "/" to create it.`
)
}
const folderSegments = dirMode
? input.destination.segments
: input.destination.segments.slice(0, -1)
const leafName = dirMode ? undefined : input.destination.segments.at(-1)
const createdFolders: CreatedFolder[] = []
let targetFolderPromise: Promise<string | null> | undefined
const targetFolderId = () =>
(targetFolderPromise ??= ensureFolderPath({
workspaceId: context.workspaceId,
userId,
segments: folderSegments,
createdFolders,
}))
const outcomes: WorkspaceFileVfsOutcome[] = []
const relocatedFiles: RelocatedFile[] = []
const relocatedFolders: RelocatedFolder[] = []
for (const reference of references) {
if ('error' in reference) {
outcomes.push({ source: reference.source, resourceType: 'file', error: reference.error })
continue
}
try {
const targetId = await targetFolderId()
if ('file' in reference) {
const name = leafName ?? reference.file.name
const result = await moveRenameWorkspaceFile({
workspaceId: context.workspaceId,
fileId: reference.file.id,
targetFolderId: targetId,
newName: name,
})
relocatedFiles.push({
id: result.file.id,
name: result.file.name,
moved: result.moved,
renamed: result.renamed,
})
outcomes.push({
source: reference.source,
targetSegments: [...folderSegments, result.file.name],
resourceType: 'file',
resourceId: result.file.id,
})
continue
}
if (targetId === reference.folderId) {
outcomes.push({
source: reference.source,
resourceType: 'folder',
error: 'Cannot move a folder into itself',
})
continue
}
const sourcePath = buildFolderPath(
sources.find((source) => source.source === reference.source)?.segments ?? []
)
const name =
leafName ?? sources.find((source) => source.source === reference.source)?.segments.at(-1)
if (!name) throw new OrchestrationError('validation', 'Folder name is required')
const nextPath = buildFolderPath([...folderSegments, name])
const result = await relocateWorkspaceFileFolderByPath({
workspaceId: context.workspaceId,
path: sourcePath,
destinationPath: nextPath,
})
relocatedFolders.push({
id: result.folder.id,
name: result.folder.name,
sourcePath,
destinationPath: result.path,
})
outcomes.push({
source: reference.source,
targetSegments: [...folderSegments, result.folder.name],
resourceType: 'folder',
resourceId: result.folder.id,
})
} catch (error) {
outcomes.push({
source: reference.source,
resourceType: 'file' in reference ? 'file' : 'folder',
error: expectedOutcomeMessage(error),
})
}
}
return { outcomes, createdFolders, relocatedFiles, relocatedFolders }
},
projectAudit: ({ result }) => [
...createdFolderAudits(result.createdFolders),
...result.relocatedFiles
.filter((file) => file.moved || file.renamed)
.map((file) => ({
action: file.moved ? AuditAction.FILE_MOVED : AuditAction.FILE_UPDATED,
resourceType: AuditResourceType.FILE,
resourceId: file.id,
resourceName: file.name,
description: `Relocated file "${file.name}"`,
metadata: { moved: file.moved, renamed: file.renamed },
})),
...result.relocatedFolders.map((folder) => ({
action: AuditAction.FOLDER_MOVED,
resourceType: AuditResourceType.FOLDER,
resourceId: folder.id,
resourceName: folder.name,
description: `Moved file folder to "${folder.destinationPath}"`,
metadata: { sourcePath: folder.sourcePath, destinationPath: folder.destinationPath },
})),
],
afterSuccess: ({ context, result }) =>
result.createdFolders.length > 0 ||
result.relocatedFiles.some((file) => file.moved || file.renamed) ||
result.relocatedFolders.length > 0
? notifyWorkspaceFilesChanged(context.workspaceId)
: undefined,
})
export interface DeleteWorkspaceFileVfsItemsInput {
workspaceId: string
paths: WorkspaceFileVfsPathReference[]
}
export const deleteWorkspaceFileVfsItems = defineAuthorizedWorkspaceFileUseCase({
operation: fileOperations.deleteVfsItems,
resolveContext: ({ input }: { input: DeleteWorkspaceFileVfsItemsInput }) =>
loadWorkspaceFileOperationContext(input.workspaceId).then((context) => {
if (!context) throw new OrchestrationError('not_found', 'Workspace not found')
return context
}),
async execute({ input, context }) {
const paths = normalizeReferences(input.paths)
const references = []
for (const path of paths) {
references.push(await resolveSource(context.workspaceId, path))
}
const outcomes: WorkspaceFileVfsOutcome[] = []
const deletedFiles: Array<{ id: string; name: string }> = []
const deletedFolders: Array<{ id: string; path: string }> = []
for (const reference of references) {
if ('error' in reference) {
outcomes.push({ source: reference.source, resourceType: 'file', error: reference.error })
continue
}
try {
if ('file' in reference) {
const archived = await bulkArchiveWorkspaceFileItems({
workspaceId: context.workspaceId,
fileIds: [reference.file.id],
})
if (!archived.fileIds.includes(reference.file.id)) {
throw new OrchestrationError('not_found', 'File not found')
}
deletedFiles.push({ id: reference.file.id, name: reference.file.name })
outcomes.push({
source: reference.source,
resourceType: 'file',
resourceId: reference.file.id,
})
continue
}
const archived = await bulkArchiveWorkspaceFileItems({
workspaceId: context.workspaceId,
folderIds: [reference.folderId],
})
if (!archived.folderIds.includes(reference.folderId)) {
throw new OrchestrationError('not_found', 'Folder not found')
}
deletedFolders.push({ id: reference.folderId, path: reference.source })
outcomes.push({
source: reference.source,
resourceType: 'folder',
resourceId: reference.folderId,
})
} catch (error) {
outcomes.push({
source: reference.source,
resourceType: 'file' in reference ? 'file' : 'folder',
error: expectedOutcomeMessage(error),
})
}
}
return { outcomes, deletedFiles, deletedFolders }
},
projectAudit: ({ result }) => [
...result.deletedFiles.map((file) => ({
action: AuditAction.FILE_DELETED,
resourceType: AuditResourceType.FILE,
resourceId: file.id,
resourceName: file.name,
description: `Archived file "${file.name}"`,
})),
...result.deletedFolders.map((folder) => ({
action: AuditAction.FOLDER_DELETED,
resourceType: AuditResourceType.FOLDER,
resourceId: folder.id,
description: `Archived file folder "${folder.path}"`,
metadata: { path: folder.path },
})),
],
afterSuccess: ({ context, result }) =>
result.deletedFiles.length > 0 || result.deletedFolders.length > 0
? notifyWorkspaceFilesChanged(context.workspaceId)
: undefined,
})
@@ -35,4 +35,3 @@ export {
performRestoreWorkspaceFileFolder,
performUpdateWorkspaceFileFolder,
} from './file-folder-lifecycle'
export {} from './share'
@@ -1,3 +0,0 @@
import { createLogger } from '@sim/logger'
const logger = createLogger('WorkspaceFileShareOrchestration')
+6 -2
View File
@@ -70,9 +70,9 @@
"@better-auth/sso": "1.6.27",
"@better-auth/stripe": "1.6.27",
"@browserbasehq/stagehand": "^3.2.1",
"@calcom/embed-react": "1.5.3",
"@c15t/nextjs": "2.2.1",
"@c15t/scripts": "2.2.0",
"@calcom/embed-react": "1.5.3",
"@cerebras/cerebras_cloud_sdk": "^1.23.0",
"@daytona/sdk": "0.207.0",
"@e2b/code-interpreter": "2.7.1",
@@ -140,10 +140,12 @@
"@tiptap/extension-code-block": "3.30.5",
"@tiptap/extension-collaboration": "3.30.5",
"@tiptap/extension-collaboration-caret": "3.30.5",
"@tiptap/extension-document": "3.30.5",
"@tiptap/extension-hard-break": "3.30.5",
"@tiptap/extension-image": "3.30.5",
"@tiptap/extension-list": "3.30.5",
"@tiptap/extension-placeholder": "3.30.5",
"@tiptap/extension-paragraph": "3.30.5",
"@tiptap/extension-placeholder": "3.30.5",
"@tiptap/extension-table": "3.30.5",
"@tiptap/markdown": "3.30.5",
"@tiptap/pm": "3.30.5",
@@ -262,6 +264,7 @@
},
"devDependencies": {
"@next/env": "16.3.4",
"@opentelemetry/context-async-hooks": "2.10.0",
"@sim/testing": "workspace:*",
"@sim/tsconfig": "workspace:*",
"@tailwindcss/postcss": "^4.3.3",
@@ -270,6 +273,7 @@
"@trigger.dev/build": "4.5.16",
"@types/archiver": "8.0.0",
"@types/busboy": "1.5.4",
"@types/hast": "3.0.4",
"@types/heic-convert": "2.1.1",
"@types/html-to-text": "9.0.4",
"@types/js-yaml": "4.0.9",
@@ -38,8 +38,6 @@ vi.mock('@/providers/azure-openai/utils', () => ({
extractBaseUrl: vi.fn((url: string) => url),
extractDeploymentFromUrl: vi.fn(() => null),
extractApiVersionFromUrl: vi.fn(() => null),
createReadableStreamFromAzureOpenAIStream: vi.fn(),
checkForForcedToolUsage: vi.fn(() => ({ hasUsedForcedTool: false, usedForcedTools: [] })),
}))
vi.mock('@/providers/models', () => providersModelsMock)
vi.mock('@/providers/attachments', () => providersAttachmentsMock)
+15 -13
View File
@@ -19,8 +19,6 @@ import type { StreamingExecution } from '@/executor/types'
import { MAX_TOOL_ITERATIONS } from '@/providers'
import { prepareProviderAttachments } from '@/providers/attachments'
import {
checkForForcedToolUsage,
createReadableStreamFromAzureOpenAIStream,
extractApiVersionFromUrl,
extractBaseUrl,
extractDeploymentFromUrl,
@@ -42,6 +40,7 @@ import {
import { getProviderDefaultModel, getProviderModels } from '@/providers/models'
import { executeResponsesProviderRequest } from '@/providers/openai/core'
import { getChatCompletionConversationUsage } from '@/providers/openai-compat/conversation-usage'
import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events'
import { executeProviderTool } from '@/providers/runtime-context'
import { createSettledAgentEventStream } from '@/providers/stream-events'
import { createStreamingExecution } from '@/providers/streaming-execution'
@@ -59,6 +58,7 @@ import type {
import { ProviderError } from '@/providers/types'
import {
calculateCost,
checkForForcedToolUsageOpenAI,
isFunctionToolCall,
prepareToolExecution,
prepareToolsWithUsageControl,
@@ -241,9 +241,10 @@ async function executeChatCompletionsRequest(
initialCost: { input: 0, output: 0, total: 0 },
streamFormat: 'agent-events-v1',
createStream: ({ output, finalizeTiming }) =>
createReadableStreamFromAzureOpenAIStream(
streamResponse,
(content, usage) => {
createOpenAICompatibleAgentEventStream(streamResponse, {
providerName: 'Azure OpenAI',
request,
onComplete: ({ content, usage }) => {
output.content = content
output.tokens = {
input: usage.prompt_tokens,
@@ -264,8 +265,7 @@ async function executeChatCompletionsRequest(
finalizeTiming()
},
request
),
}),
})
return streamingResult
@@ -321,12 +321,13 @@ async function executeChatCompletionsRequest(
{ model: request.model, provider: 'azure_openai' }
)
const firstCheckResult = checkForForcedToolUsage(
const firstCheckResult = checkForForcedToolUsageOpenAI(
currentResponse,
originalToolChoice ?? 'auto',
logger,
'Azure OpenAI',
forcedTools,
usedForcedTools
usedForcedTools,
logger
)
hasUsedForcedTool = firstCheckResult.hasUsedForcedTool
usedForcedTools = firstCheckResult.usedForcedTools
@@ -543,12 +544,13 @@ async function executeChatCompletionsRequest(
)
}
const nextCheckResult = checkForForcedToolUsage(
const nextCheckResult = checkForForcedToolUsageOpenAI(
currentResponse,
nextPayload.tool_choice ?? 'auto',
logger,
'Azure OpenAI',
forcedTools,
usedForcedTools
usedForcedTools,
logger
)
hasUsedForcedTool = nextCheckResult.hasUsedForcedTool
usedForcedTools = nextCheckResult.usedForcedTools
-49
View File
@@ -1,52 +1,3 @@
import type { Logger } from '@sim/logger'
import type OpenAI from 'openai'
import type { ChatCompletionChunk } from 'openai/resources/chat/completions'
import type { CompletionUsage } from 'openai/resources/completions'
import type { Stream } from 'openai/streaming'
import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events'
import type { AgentStreamEvent } from '@/providers/stream-events'
import type { ProviderRequest } from '@/providers/types'
import { checkForForcedToolUsageOpenAI } from '@/providers/utils'
/**
* Creates an agent-events stream from an Azure OpenAI streaming response.
* Uses the shared OpenAI-compatible agent event streaming utility.
*/
export function createReadableStreamFromAzureOpenAIStream(
azureOpenAIStream: Stream<ChatCompletionChunk>,
onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void,
request?: ProviderRequest
): ReadableStream<AgentStreamEvent> {
return createOpenAICompatibleAgentEventStream(azureOpenAIStream, {
request,
providerName: 'Azure OpenAI',
onComplete: onComplete
? (result) => onComplete(result.content, result.usage, result.thinking)
: undefined,
})
}
/**
* Checks if a forced tool was used in an Azure OpenAI response.
* Uses the shared OpenAI-compatible forced tool usage helper.
*/
export function checkForForcedToolUsage(
response: OpenAI.Chat.Completions.ChatCompletion,
toolChoice: string | { type: string; function?: { name: string }; name?: string },
_logger: Logger,
forcedTools: string[],
usedForcedTools: string[]
): { hasUsedForcedTool: boolean; usedForcedTools: string[] } {
return checkForForcedToolUsageOpenAI(
response,
toolChoice,
'Azure OpenAI',
forcedTools,
usedForcedTools,
_logger
)
}
/**
* Determines if an Azure OpenAI endpoint URL is for the chat completions API.
* Returns true for URLs containing /chat/completions pattern.
+1 -30
View File
@@ -8,10 +8,6 @@ import { toolsMock, toolsMockFns } from '@sim/testing/mocks/tools.mock'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { StreamingExecution } from '@/executor/types'
const { mockSupportsNativeStructuredOutputs } = vi.hoisted(() => ({
mockSupportsNativeStructuredOutputs: vi.fn(),
}))
vi.mock('openai', () => openaiMock)
vi.mock('@/providers', () => providersMock)
@@ -20,14 +16,6 @@ vi.mock('@/providers/models', () => providersModelsMock)
vi.mock('@/providers/attachments', () => providersAttachmentsMock)
vi.mock('@/providers/baseten/utils', () => ({
supportsNativeStructuredOutputs: mockSupportsNativeStructuredOutputs,
createReadableStreamFromOpenAIStream: vi.fn(
() => new ReadableStream({ start: (controller) => controller.close() })
),
checkForForcedToolUsage: vi.fn(() => ({ hasUsedForcedTool: false, usedForcedTools: [] })),
}))
vi.mock('@/providers/trace-enrichment', () => providersTraceEnrichmentMock)
vi.mock('@/providers/utils', () => providersUtilsMock)
@@ -83,7 +71,6 @@ const lastCallBody = () => mockCreate.mock.calls.at(-1)?.[0]
describe('basetenProvider', () => {
beforeEach(() => {
mockSupportsNativeStructuredOutputs.mockResolvedValue(true)
mockPrepareToolsWithUsageControl.mockImplementation((tools) => ({
tools,
toolChoice: 'auto',
@@ -120,7 +107,7 @@ describe('basetenProvider', () => {
})
it('streams directly when there are no tools', async () => {
mockCreate.mockResolvedValueOnce({})
mockCreate.mockResolvedValueOnce((async function* () {})())
const result = await basetenProvider.executeRequest({ ...baseRequest, stream: true })
@@ -144,22 +131,6 @@ describe('basetenProvider', () => {
expect(lastCallBody().response_format.json_schema).not.toHaveProperty('strict')
})
it('falls back to json_object with prompt instructions when native is unsupported', async () => {
mockSupportsNativeStructuredOutputs.mockResolvedValue(false)
mockCreate.mockResolvedValueOnce(textResponse('{}'))
await basetenProvider.executeRequest({
...baseRequest,
responseFormat: { name: 'my_schema', schema: { type: 'object' } },
})
expect(lastCallBody().response_format).toEqual({ type: 'json_object' })
expect(lastCallBody().messages.at(-1)).toEqual({
role: 'user',
content: 'SCHEMA_INSTRUCTIONS',
})
})
it('defers response_format to a final call when tools are active', async () => {
mockCreate
.mockResolvedValueOnce(textResponse('intermediate'))
+29 -682
View File
@@ -1,83 +1,33 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { isRecordLike } from '@sim/utils/object'
import OpenAI from 'openai'
import type { ChatCompletionCreateParamsStreaming } from 'openai/resources/chat/completions'
import type { ChatCompletionMessageParam } from 'openai/resources/chat/completions'
import type { StreamingExecution } from '@/executor/types'
import { MAX_TOOL_ITERATIONS } from '@/providers'
import { formatMessagesForProvider } from '@/providers/attachments'
import {
checkForForcedToolUsage,
createReadableStreamFromOpenAIStream,
supportsNativeStructuredOutputs,
} from '@/providers/baseten/utils'
import {
isConversationContextError,
prepareConversationGeneration,
} from '@/providers/conversation-generation'
import {
captureProviderConversationStep,
recordProviderConversationToolError,
} from '@/providers/conversation-history'
import { getProviderDefaultModel, getProviderModels } from '@/providers/models'
import { createOpenAICompatAssistantHistory } from '@/providers/openai-compat/assistant-history'
import { getChatCompletionConversationUsage } from '@/providers/openai-compat/conversation-usage'
import { executeProviderTool } from '@/providers/runtime-context'
import { createSettledAgentEventStream } from '@/providers/stream-events'
import { createStreamingExecution } from '@/providers/streaming-execution'
import { isAbortError, parseToolArguments } from '@/providers/streaming-tool-loop-shared'
import { adaptOpenAIChatToolSchema } from '@/providers/tool-schema-adapter'
import { enrichLastModelSegmentFromChatCompletions } from '@/providers/trace-enrichment'
import { openAICompatTransport } from '@/providers/transport'
import type {
FunctionCallResponse,
Message,
ProviderConfig,
ProviderRequest,
ProviderResponse,
TimeSegment,
} from '@/providers/types'
import { ProviderError } from '@/providers/types'
import {
calculateCost,
generateSchemaInstructions,
isFunctionToolCall,
prepareToolExecution,
prepareToolsWithUsageControl,
sumToolCosts,
} from '@/providers/utils'
type ChatCompletionPayload,
executeChatCompletionRequest,
} from '@/providers/openai-compat/chat-completions'
import { openAICompatTransport } from '@/providers/transport'
import type { ProviderConfig, ProviderRequest, ProviderResponse } from '@/providers/types'
const logger = createLogger('BasetenProvider')
/**
* Applies structured output configuration to a payload based on model capabilities.
* Uses native json_schema for supported models, falls back to json_object with prompt instructions.
*/
/** Baseten accepts native JSON schemas. */
async function applyResponseFormat(
targetPayload: any,
messages: any[],
responseFormat: any,
targetPayload: ChatCompletionPayload,
messages: ChatCompletionMessageParam[],
responseFormat: NonNullable<ProviderRequest['responseFormat']>,
model: string
): Promise<any[]> {
const useNative = await supportsNativeStructuredOutputs(model)
if (useNative) {
logger.info('Using native structured outputs for Baseten model', { model })
targetPayload.response_format = {
type: 'json_schema',
json_schema: {
name: responseFormat.name || 'response_schema',
schema: responseFormat.schema || responseFormat,
},
}
return messages
): Promise<ChatCompletionMessageParam[]> {
logger.info('Using native structured outputs for Baseten model', { model })
targetPayload.response_format = {
type: 'json_schema',
json_schema: {
name: responseFormat.name || 'response_schema',
schema: responseFormat.schema || responseFormat,
},
}
logger.info('Using json_object mode with prompt instructions for Baseten model', { model })
const schema = responseFormat.schema || responseFormat
const schemaInstructions = generateSchemaInstructions(schema, responseFormat.name)
targetPayload.response_format = { type: 'json_object' }
return [...messages, { role: 'user', content: schemaInstructions }]
return messages
}
export const basetenProvider: ProviderConfig = {
@@ -113,618 +63,15 @@ export const basetenProvider: ProviderConfig = {
stream: !!request.stream,
})
const allMessages: Message[] = []
if (request.systemPrompt) {
allMessages.push({ role: 'system', content: request.systemPrompt })
}
if (request.context) {
allMessages.push({ role: 'user', content: request.context })
}
if (request.messages) {
allMessages.push(...request.messages)
}
const formattedMessages = formatMessagesForProvider(allMessages, 'baseten') as Message[]
const tools = request.tools?.length
? request.tools.map((tool) => adaptOpenAIChatToolSchema(tool))
: undefined
const payload: any = {
model: requestedModel,
messages: formattedMessages,
}
if (request.temperature !== undefined) payload.temperature = request.temperature
if (request.maxTokens != null) payload.max_tokens = request.maxTokens
let preparedTools: ReturnType<typeof prepareToolsWithUsageControl> | null = null
let hasActiveTools = false
if (tools?.length) {
preparedTools = prepareToolsWithUsageControl(tools, request.tools, logger, 'baseten')
const { tools: filteredTools, toolChoice } = preparedTools
if (filteredTools?.length && toolChoice) {
payload.tools = filteredTools
payload.tool_choice = toolChoice
hasActiveTools = true
}
}
const providerStartTime = Date.now()
const providerStartTimeISO = new Date(providerStartTime).toISOString()
try {
if (request.responseFormat && !hasActiveTools) {
payload.messages = await applyResponseFormat(
payload,
payload.messages,
request.responseFormat,
requestedModel
)
}
if (request.stream && (!tools || tools.length === 0 || !hasActiveTools)) {
const streamingParams: ChatCompletionCreateParamsStreaming = {
...payload,
stream: true,
stream_options: { include_usage: true },
}
const streamResponse = await client.chat.completions.create(
await prepareConversationGeneration(request, 'chat-completions', streamingParams),
request.abortSignal ? { signal: request.abortSignal } : undefined
)
const streamingResult = createStreamingExecution({
model: requestedModel,
providerStartTime,
providerStartTimeISO,
timing: { kind: 'simple', segmentName: request.model },
initialTokens: { input: 0, output: 0, total: 0 },
initialCost: { input: 0, output: 0, total: 0 },
streamFormat: 'agent-events-v1',
createStream: ({ output, finalizeTiming }) =>
createReadableStreamFromOpenAIStream(
streamResponse,
(content, usage) => {
output.content = content
output.tokens = {
input: usage.prompt_tokens,
output: usage.completion_tokens,
total: usage.total_tokens,
}
const costResult = calculateCost(
requestedModel,
usage.prompt_tokens,
usage.completion_tokens
)
output.cost = {
input: costResult.input,
output: costResult.output,
total: costResult.total,
}
finalizeTiming()
},
request
),
})
return streamingResult
}
const initialCallTime = Date.now()
const originalToolChoice = payload.tool_choice
const forcedTools = preparedTools?.forcedTools || []
let usedForcedTools: string[] = []
let currentResponse = await client.chat.completions.create(
await prepareConversationGeneration(request, 'chat-completions', payload),
request.abortSignal ? { signal: request.abortSignal } : undefined
)
if (!currentResponse.choices[0]?.message?.tool_calls?.length) {
await captureProviderConversationStep(
request,
'chat-completions',
currentResponse.choices[0]?.message,
getChatCompletionConversationUsage(currentResponse.usage)
)
}
const firstResponseTime = Date.now() - initialCallTime
let content = currentResponse.choices[0]?.message?.content || ''
const tokens = {
input: currentResponse.usage?.prompt_tokens || 0,
output: currentResponse.usage?.completion_tokens || 0,
total: currentResponse.usage?.total_tokens || 0,
}
const toolCalls: FunctionCallResponse[] = []
const toolResults: Record<string, unknown>[] = []
const currentMessages = [...formattedMessages]
let iterationCount = 0
let modelTime = firstResponseTime
let toolsTime = 0
let hasUsedForcedTool = false
const timeSegments: TimeSegment[] = [
{
type: 'model',
name: request.model,
startTime: initialCallTime,
endTime: initialCallTime + firstResponseTime,
duration: firstResponseTime,
},
]
const forcedToolResult = checkForForcedToolUsage(
currentResponse,
originalToolChoice,
forcedTools,
usedForcedTools
)
hasUsedForcedTool = forcedToolResult.hasUsedForcedTool
usedForcedTools = forcedToolResult.usedForcedTools
while (iterationCount < MAX_TOOL_ITERATIONS) {
if (currentResponse.choices[0]?.message?.content) {
content = currentResponse.choices[0].message.content
}
const toolCallsInResponse =
currentResponse.choices[0]?.message?.tool_calls?.filter(isFunctionToolCall)
enrichLastModelSegmentFromChatCompletions(
timeSegments,
currentResponse,
toolCallsInResponse,
{ model: request.model, provider: 'baseten' }
)
if (!toolCallsInResponse || toolCallsInResponse.length === 0) {
break
}
const toolsStartTime = Date.now()
await captureProviderConversationStep(
request,
'chat-completions',
currentResponse.choices[0]?.message,
getChatCompletionConversationUsage(currentResponse.usage)
)
const toolExecutionPromises = toolCallsInResponse.map(async (toolCall) => {
const toolCallStartTime = Date.now()
const toolName = toolCall.function.name
try {
const toolArgs = parseToolArguments(toolCall.function.arguments, toolName)
const tool = request.tools?.find((t) => t.id === toolName)
if (!tool) {
await recordProviderConversationToolError(
request,
toolCall.id,
toolName,
`Tool "${toolName}" is not available`
)
const toolCallEndTime = Date.now()
return {
toolCall,
toolName,
toolParams: {},
result: {
success: false,
output: undefined,
error: `Tool "${toolName}" is not available`,
},
startTime: toolCallStartTime,
endTime: toolCallEndTime,
duration: toolCallEndTime - toolCallStartTime,
}
}
const { toolParams, executionParams } = prepareToolExecution(
tool,
toolArgs,
request,
toolCall.id
)
const { rawResponse, modelResponse } = await executeProviderTool(
toolName,
executionParams,
{
signal: request.abortSignal,
}
)
const toolCallEndTime = Date.now()
return {
toolCall,
toolName,
toolParams,
result: rawResponse,
modelResult: modelResponse,
startTime: toolCallStartTime,
endTime: toolCallEndTime,
duration: toolCallEndTime - toolCallStartTime,
}
} catch (error) {
if (isAbortError(error) || request.abortSignal?.aborted) {
throw error
}
await recordProviderConversationToolError(
request,
toolCall.id,
toolName,
getErrorMessage(error, 'Tool execution failed')
)
const toolCallEndTime = Date.now()
logger.error('Error processing tool call (Baseten):', {
error: toError(error).message,
toolName,
})
return {
toolCall,
toolName,
toolParams: {},
result: {
success: false,
output: undefined,
error: getErrorMessage(error, 'Tool execution failed'),
},
startTime: toolCallStartTime,
endTime: toolCallEndTime,
duration: toolCallEndTime - toolCallStartTime,
}
}
})
const executionResults = await Promise.all(toolExecutionPromises)
const assistantMessage = currentResponse.choices[0]?.message
if (assistantMessage) {
currentMessages.push(
createOpenAICompatAssistantHistory({
message: assistantMessage,
toolCalls: toolCallsInResponse,
reasoningFields: ['reasoning_content'],
})
)
}
for (const executionResult of executionResults) {
const { toolCall, toolName, toolParams, result, startTime, endTime, duration } =
executionResult
const modelResult =
'modelResult' in executionResult ? (executionResult.modelResult ?? result) : result
timeSegments.push({
type: 'tool',
name: toolName,
startTime: startTime,
endTime: endTime,
duration: duration,
toolCallId: toolCall.id,
})
let resultContent: unknown
if (result.success) {
if (isRecordLike(result.output)) {
toolResults.push(result.output)
}
resultContent = result.output ?? null
} else {
resultContent = {
error: true,
message: result.error || 'Tool execution failed',
tool: toolName,
}
}
const modelResultContent = modelResult.success
? (modelResult.output ?? null)
: {
error: true,
message: modelResult.error || 'Tool execution failed',
tool: toolName,
}
toolCalls.push({
name: toolName,
arguments: toolParams,
startTime: new Date(startTime).toISOString(),
endTime: new Date(endTime).toISOString(),
duration: duration,
result: resultContent,
success: result.success,
})
currentMessages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify(modelResultContent),
})
}
const thisToolsTime = Date.now() - toolsStartTime
toolsTime += thisToolsTime
const nextPayload = {
...payload,
messages: currentMessages,
}
if (typeof originalToolChoice === 'object' && hasUsedForcedTool && forcedTools.length > 0) {
const remainingTools = forcedTools.filter((tool) => !usedForcedTools.includes(tool))
if (remainingTools.length > 0) {
nextPayload.tool_choice = { type: 'function', function: { name: remainingTools[0] } }
} else {
nextPayload.tool_choice = 'auto'
}
}
const nextModelStartTime = Date.now()
currentResponse = await client.chat.completions.create(
await prepareConversationGeneration(request, 'chat-completions', nextPayload),
request.abortSignal ? { signal: request.abortSignal } : undefined
)
if (!currentResponse.choices[0]?.message?.tool_calls?.length) {
await captureProviderConversationStep(
request,
'chat-completions',
currentResponse.choices[0]?.message,
getChatCompletionConversationUsage(currentResponse.usage)
)
}
const nextForcedToolResult = checkForForcedToolUsage(
currentResponse,
nextPayload.tool_choice,
forcedTools,
usedForcedTools
)
hasUsedForcedTool = nextForcedToolResult.hasUsedForcedTool
usedForcedTools = nextForcedToolResult.usedForcedTools
const nextModelEndTime = Date.now()
const thisModelTime = nextModelEndTime - nextModelStartTime
timeSegments.push({
type: 'model',
name: request.model,
startTime: nextModelStartTime,
endTime: nextModelEndTime,
duration: thisModelTime,
})
modelTime += thisModelTime
if (currentResponse.choices[0]?.message?.content) {
content = currentResponse.choices[0].message.content
}
if (currentResponse.usage) {
tokens.input += currentResponse.usage.prompt_tokens || 0
tokens.output += currentResponse.usage.completion_tokens || 0
tokens.total += currentResponse.usage.total_tokens || 0
}
iterationCount++
}
if (iterationCount === MAX_TOOL_ITERATIONS) {
const pendingToolCalls =
currentResponse.choices[0]?.message?.tool_calls?.filter(isFunctionToolCall)
enrichLastModelSegmentFromChatCompletions(timeSegments, currentResponse, pendingToolCalls, {
model: request.model,
provider: 'baseten',
})
if (pendingToolCalls?.length && !(request.responseFormat && hasActiveTools)) {
const finalPayload: any = {
...payload,
messages: [...currentMessages],
tool_choice: 'none',
}
if (request.responseFormat) {
finalPayload.messages = await applyResponseFormat(
finalPayload,
finalPayload.messages,
request.responseFormat,
requestedModel
)
}
const finalStartTime = Date.now()
const finalResponse = await client.chat.completions.create(
await prepareConversationGeneration(request, 'chat-completions', finalPayload),
request.abortSignal ? { signal: request.abortSignal } : undefined
)
if (!finalResponse.choices[0]?.message?.tool_calls?.length) {
await captureProviderConversationStep(
request,
'chat-completions',
finalResponse.choices[0]?.message,
getChatCompletionConversationUsage(finalResponse.usage)
)
}
const finalEndTime = Date.now()
const finalDuration = finalEndTime - finalStartTime
timeSegments.push({
type: 'model',
name: 'Final answer after tool iteration limit',
startTime: finalStartTime,
endTime: finalEndTime,
duration: finalDuration,
})
modelTime += finalDuration
if (finalResponse.choices[0]?.message?.content) {
content = finalResponse.choices[0].message.content
}
if (finalResponse.usage) {
tokens.input += finalResponse.usage.prompt_tokens || 0
tokens.output += finalResponse.usage.completion_tokens || 0
tokens.total += finalResponse.usage.total_tokens || 0
}
enrichLastModelSegmentFromChatCompletions(
timeSegments,
finalResponse,
finalResponse.choices[0]?.message?.tool_calls?.filter(isFunctionToolCall),
{ model: request.model, provider: 'baseten' }
)
}
}
if (request.responseFormat && hasActiveTools) {
const finalPayload: any = {
model: payload.model,
messages: [...currentMessages],
}
if (payload.temperature !== undefined) {
finalPayload.temperature = payload.temperature
}
if (payload.max_tokens !== undefined) {
finalPayload.max_tokens = payload.max_tokens
}
finalPayload.messages = await applyResponseFormat(
finalPayload,
finalPayload.messages,
request.responseFormat,
requestedModel
)
const finalStartTime = Date.now()
const finalResponse = await client.chat.completions.create(
await prepareConversationGeneration(request, 'chat-completions', finalPayload),
request.abortSignal ? { signal: request.abortSignal } : undefined
)
if (!finalResponse.choices[0]?.message?.tool_calls?.length) {
await captureProviderConversationStep(
request,
'chat-completions',
finalResponse.choices[0]?.message,
getChatCompletionConversationUsage(finalResponse.usage)
)
}
const finalEndTime = Date.now()
const finalDuration = finalEndTime - finalStartTime
timeSegments.push({
type: 'model',
name: 'Final structured response',
startTime: finalStartTime,
endTime: finalEndTime,
duration: finalDuration,
})
modelTime += finalDuration
if (finalResponse.choices[0]?.message?.content) {
content = finalResponse.choices[0].message.content
}
if (finalResponse.usage) {
tokens.input += finalResponse.usage.prompt_tokens || 0
tokens.output += finalResponse.usage.completion_tokens || 0
tokens.total += finalResponse.usage.total_tokens || 0
}
enrichLastModelSegmentFromChatCompletions(
timeSegments,
finalResponse,
finalResponse.choices[0]?.message?.tool_calls?.filter(isFunctionToolCall),
{ model: request.model, provider: 'baseten' }
)
}
if (request.stream) {
const accumulatedCost = calculateCost(requestedModel, tokens.input, tokens.output)
const toolCost = sumToolCosts(toolResults)
const finalCost = {
input: accumulatedCost.input,
output: accumulatedCost.output,
toolCost: toolCost || undefined,
total: accumulatedCost.total + toolCost,
}
const streamingResult = createStreamingExecution({
model: requestedModel,
providerStartTime,
providerStartTimeISO,
timing: {
kind: 'accumulated',
modelTime,
toolsTime,
firstResponseTime,
iterations: timeSegments.filter((segment) => segment.type === 'model').length,
timeSegments,
},
initialTokens: { input: tokens.input, output: tokens.output, total: tokens.total },
initialCost: finalCost,
toolCalls:
toolCalls.length > 0 ? { list: toolCalls, count: toolCalls.length } : undefined,
streamFormat: 'agent-events-v1',
createStream: ({ output, finalizeTiming }) => {
output.content = content
output.tokens = { input: tokens.input, output: tokens.output, total: tokens.total }
output.cost = finalCost
finalizeTiming()
return createSettledAgentEventStream(content)
},
})
return streamingResult
}
const providerEndTime = Date.now()
const providerEndTimeISO = new Date(providerEndTime).toISOString()
const totalDuration = providerEndTime - providerStartTime
return {
content,
model: requestedModel,
tokens,
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
toolResults: toolResults.length > 0 ? toolResults : undefined,
timing: {
startTime: providerStartTimeISO,
endTime: providerEndTimeISO,
duration: totalDuration,
modelTime: modelTime,
toolsTime: toolsTime,
firstResponseTime: firstResponseTime,
iterations: timeSegments.filter((segment) => segment.type === 'model').length,
timeSegments: timeSegments,
},
}
} catch (error) {
const providerEndTime = Date.now()
const providerEndTimeISO = new Date(providerEndTime).toISOString()
const totalDuration = providerEndTime - providerStartTime
const errorDetails: Record<string, any> = {
error: toError(error).message,
duration: totalDuration,
}
if (error && typeof error === 'object') {
const err = error as any
if (err.status) errorDetails.status = err.status
if (err.code) errorDetails.code = err.code
if (err.type) errorDetails.type = err.type
if (err.error?.message) errorDetails.providerMessage = err.error.message
if (err.error?.metadata) errorDetails.metadata = err.error.metadata
}
logger.error('Error in Baseten request:', errorDetails)
if (
isAbortError(error) ||
request.abortSignal?.aborted ||
isConversationContextError(error)
) {
throw error
}
throw new ProviderError(toError(error).message, {
startTime: providerStartTimeISO,
endTime: providerEndTimeISO,
duration: totalDuration,
})
}
return executeChatCompletionRequest(request, {
providerId: 'baseten',
providerName: 'Baseten',
client,
requestedModel,
reportedModel: requestedModel,
logger,
applyResponseFormat,
reasoningFields: ['reasoning_content'],
})
},
}
-51
View File
@@ -1,51 +0,0 @@
import type { ChatCompletionChunk } from 'openai/resources/chat/completions'
import type { CompletionUsage } from 'openai/resources/completions'
import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events'
import type { AgentStreamEvent } from '@/providers/stream-events'
import type { ProviderRequest } from '@/providers/types'
import { checkForForcedToolUsageOpenAI } from '@/providers/utils'
/**
* Checks if a model supports native structured outputs (json_schema).
* Baseten Model APIs support structured outputs across their OpenAI-compatible inference API.
*/
export async function supportsNativeStructuredOutputs(_modelId: string): Promise<boolean> {
return true
}
/**
* Creates an agent-events stream from a Baseten streaming response.
* Uses the shared OpenAI-compatible agent event streaming utility.
*/
export function createReadableStreamFromOpenAIStream(
openaiStream: AsyncIterable<ChatCompletionChunk>,
onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void,
request?: ProviderRequest
): ReadableStream<AgentStreamEvent> {
return createOpenAICompatibleAgentEventStream(openaiStream, {
request,
providerName: 'Baseten',
onComplete: onComplete
? (result) => onComplete(result.content, result.usage, result.thinking)
: undefined,
})
}
/**
* Checks if a forced tool was used in a Baseten response.
* Uses the shared OpenAI-compatible forced tool usage helper.
*/
export function checkForForcedToolUsage(
response: any,
toolChoice: string | { type: string; function?: { name: string }; name?: string; any?: any },
forcedTools: string[],
usedForcedTools: string[]
): { hasUsedForcedTool: boolean; usedForcedTools: string[] } {
return checkForForcedToolUsageOpenAI(
response,
toolChoice,
'Baseten',
forcedTools,
usedForcedTools
)
}
+6 -6
View File
@@ -6,7 +6,6 @@ import type { StreamingExecution } from '@/executor/types'
import { MAX_TOOL_ITERATIONS } from '@/providers'
import { formatMessagesForProvider } from '@/providers/attachments'
import type { CerebrasResponse } from '@/providers/cerebras/types'
import { createReadableStreamFromCerebrasStream } from '@/providers/cerebras/utils'
import {
isConversationContextError,
prepareConversationGeneration,
@@ -18,6 +17,7 @@ import {
import { getProviderDefaultModel, getProviderModels } from '@/providers/models'
import { createOpenAICompatAssistantHistory } from '@/providers/openai-compat/assistant-history'
import { getChatCompletionConversationUsage } from '@/providers/openai-compat/conversation-usage'
import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events'
import { executeProviderTool } from '@/providers/runtime-context'
import { createSettledAgentEventStream } from '@/providers/stream-events'
import { createStreamingExecution } from '@/providers/streaming-execution'
@@ -154,9 +154,10 @@ export const cerebrasProvider: ProviderConfig = {
isStreaming: true,
streamFormat: 'agent-events-v1',
createStream: ({ output }) =>
createReadableStreamFromCerebrasStream(
streamResponse,
(content, usage) => {
createOpenAICompatibleAgentEventStream(streamResponse, {
providerName: 'Cerebras',
request,
onComplete: ({ content, usage }) => {
output.content = content
output.tokens = {
input: usage.prompt_tokens,
@@ -175,8 +176,7 @@ export const cerebrasProvider: ProviderConfig = {
total: costResult.total,
}
},
request
),
}),
})
return streamingResult
-35
View File
@@ -1,35 +0,0 @@
import type { CompletionUsage } from 'openai/resources/completions'
import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events'
import type { AgentStreamEvent } from '@/providers/stream-events'
import type { ProviderRequest } from '@/providers/types'
interface CerebrasChunk {
choices?: Array<{
delta?: {
content?: string
}
}>
usage?: {
prompt_tokens?: number
completion_tokens?: number
total_tokens?: number
}
}
/**
* Creates an agent-events stream from a Cerebras streaming response.
* Uses the shared OpenAI-compatible agent event streaming utility.
*/
export function createReadableStreamFromCerebrasStream(
cerebrasStream: AsyncIterable<CerebrasChunk>,
onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void,
request?: ProviderRequest
): ReadableStream<AgentStreamEvent> {
return createOpenAICompatibleAgentEventStream(cerebrasStream as any, {
request,
providerName: 'Cerebras',
onComplete: onComplete
? (result) => onComplete(result.content, result.usage, result.thinking)
: undefined,
})
}
@@ -119,6 +119,7 @@ describe('provider generation context coverage', () => {
visit(source)
}
expect(uncovered).toEqual([])
expect(guarded).toBeGreaterThan(90)
/** Guard discovery must be nonempty; consolidation can reduce the number of SDK sends. */
expect(guarded).toBeGreaterThan(0)
})
})
@@ -17,10 +17,6 @@ vi.mock('@/providers/models', () => providersModelsMock)
vi.mock('@/providers/attachments', () => providersAttachmentsMock)
vi.mock('@/providers/deepseek/utils', () => ({
createReadableStreamFromDeepseekStream: vi.fn(),
}))
vi.mock('@/providers/openai-compat/streaming-tool-loop', () => ({
createOpenAICompatStreamingToolLoopStream: vi.fn(),
}))
+32 -29
View File
@@ -13,9 +13,9 @@ import {
captureProviderConversationStep,
recordProviderConversationToolError,
} from '@/providers/conversation-history'
import { createReadableStreamFromDeepseekStream } from '@/providers/deepseek/utils'
import { getProviderDefaultModel, getProviderModels } from '@/providers/models'
import { getChatCompletionConversationUsage } from '@/providers/openai-compat/conversation-usage'
import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events'
import { createOpenAICompatStreamingToolLoopStream } from '@/providers/openai-compat/streaming-tool-loop'
import { executeProviderTool } from '@/providers/runtime-context'
import { createStreamingExecution } from '@/providers/streaming-execution'
@@ -240,37 +240,40 @@ export const deepseekProvider: ProviderConfig = {
isStreaming: true,
streamFormat: 'agent-events-v1',
createStream: ({ output, finalizeTiming }) =>
createReadableStreamFromDeepseekStream(
createOpenAICompatibleAgentEventStream(
// double-cast-allowed: payload is untyped so the SDK cannot resolve the streaming overload; the stream yields OpenAI ChatCompletionChunk objects
streamResponse as unknown as AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>,
(content, usage, thinking) => {
output.content = content
output.tokens = {
input: usage.prompt_tokens,
output: usage.completion_tokens,
total: usage.total_tokens,
}
const costResult = calculateCost(
request.model,
usage.prompt_tokens,
usage.completion_tokens
)
output.cost = {
input: costResult.input,
output: costResult.output,
total: costResult.total,
}
if (thinking) {
const segment = output.providerTiming?.timeSegments?.[0]
if (segment) {
segment.thinkingContent = thinking
{
providerName: 'Deepseek',
request,
onComplete: ({ content, usage, thinking }) => {
output.content = content
output.tokens = {
input: usage.prompt_tokens,
output: usage.completion_tokens,
total: usage.total_tokens,
}
}
finalizeTiming()
},
request
const costResult = calculateCost(
request.model,
usage.prompt_tokens,
usage.completion_tokens
)
output.cost = {
input: costResult.input,
output: costResult.output,
total: costResult.total,
}
if (thinking) {
const segment = output.providerTiming?.timeSegments?.[0]
if (segment) {
segment.thinkingContent = thinking
}
}
finalizeTiming()
},
}
),
})
-23
View File
@@ -1,23 +0,0 @@
import type { ChatCompletionChunk } from 'openai/resources/chat/completions'
import type { CompletionUsage } from 'openai/resources/completions'
import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events'
import type { AgentStreamEvent } from '@/providers/stream-events'
import type { ProviderRequest } from '@/providers/types'
/**
* Creates an agent-events stream from a DeepSeek streaming response.
* Uses the shared OpenAI-compatible agent event streaming utility.
*/
export function createReadableStreamFromDeepseekStream(
deepseekStream: AsyncIterable<ChatCompletionChunk>,
onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void,
request?: ProviderRequest
): ReadableStream<AgentStreamEvent> {
return createOpenAICompatibleAgentEventStream(deepseekStream, {
request,
providerName: 'Deepseek',
onComplete: onComplete
? (result) => onComplete(result.content, result.usage, result.thinking)
: undefined,
})
}
+2 -25
View File
@@ -8,8 +8,7 @@ import { toolsMock, toolsMockFns } from '@sim/testing/mocks/tools.mock'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { StreamingExecution } from '@/executor/types'
const { mockSupportsNativeStructuredOutputs, mockResolveFireworksWireModel } = vi.hoisted(() => ({
mockSupportsNativeStructuredOutputs: vi.fn(),
const { mockResolveFireworksWireModel } = vi.hoisted(() => ({
mockResolveFireworksWireModel: vi.fn(),
}))
@@ -22,11 +21,6 @@ vi.mock('@/providers/models', () => providersModelsMock)
vi.mock('@/providers/attachments', () => providersAttachmentsMock)
vi.mock('@/providers/fireworks/utils', () => ({
supportsNativeStructuredOutputs: mockSupportsNativeStructuredOutputs,
createReadableStreamFromOpenAIStream: vi.fn(
() => new ReadableStream({ start: (controller) => controller.close() })
),
checkForForcedToolUsage: vi.fn(() => ({ hasUsedForcedTool: false, usedForcedTools: [] })),
resolveFireworksWireModel: mockResolveFireworksWireModel,
}))
@@ -85,7 +79,6 @@ const lastCallBody = () => mockCreate.mock.calls.at(-1)?.[0]
describe('fireworksProvider', () => {
beforeEach(() => {
mockSupportsNativeStructuredOutputs.mockResolvedValue(true)
mockPrepareToolsWithUsageControl.mockImplementation((tools) => ({
tools,
toolChoice: 'auto',
@@ -143,7 +136,7 @@ describe('fireworksProvider', () => {
})
it('streams directly when there are no tools', async () => {
mockCreate.mockResolvedValueOnce({})
mockCreate.mockResolvedValueOnce((async function* () {})())
const result = await fireworksProvider.executeRequest({ ...baseRequest, stream: true })
@@ -167,22 +160,6 @@ describe('fireworksProvider', () => {
expect(lastCallBody().response_format.json_schema).not.toHaveProperty('strict')
})
it('falls back to json_object with prompt instructions when native is unsupported', async () => {
mockSupportsNativeStructuredOutputs.mockResolvedValue(false)
mockCreate.mockResolvedValueOnce(textResponse('{}'))
await fireworksProvider.executeRequest({
...baseRequest,
responseFormat: { name: 'my_schema', schema: { type: 'object' } },
})
expect(lastCallBody().response_format).toEqual({ type: 'json_object' })
expect(lastCallBody().messages.at(-1)).toEqual({
role: 'user',
content: 'SCHEMA_INSTRUCTIONS',
})
})
it('defers response_format to a final call when tools are active', async () => {
mockCreate
.mockResolvedValueOnce(textResponse('intermediate'))
+30 -689
View File
@@ -1,84 +1,34 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { isRecordLike } from '@sim/utils/object'
import OpenAI from 'openai'
import type { ChatCompletionCreateParamsStreaming } from 'openai/resources/chat/completions'
import type { ChatCompletionMessageParam } from 'openai/resources/chat/completions'
import type { StreamingExecution } from '@/executor/types'
import { MAX_TOOL_ITERATIONS } from '@/providers'
import { formatMessagesForProvider } from '@/providers/attachments'
import {
isConversationContextError,
prepareConversationGeneration,
} from '@/providers/conversation-generation'
import {
captureProviderConversationStep,
recordProviderConversationToolError,
} from '@/providers/conversation-history'
import {
checkForForcedToolUsage,
createReadableStreamFromOpenAIStream,
resolveFireworksWireModel,
supportsNativeStructuredOutputs,
} from '@/providers/fireworks/utils'
import { resolveFireworksWireModel } from '@/providers/fireworks/utils'
import { getProviderDefaultModel, getProviderModels } from '@/providers/models'
import { createOpenAICompatAssistantHistory } from '@/providers/openai-compat/assistant-history'
import { getChatCompletionConversationUsage } from '@/providers/openai-compat/conversation-usage'
import { executeProviderTool } from '@/providers/runtime-context'
import { createSettledAgentEventStream } from '@/providers/stream-events'
import { createStreamingExecution } from '@/providers/streaming-execution'
import { isAbortError, parseToolArguments } from '@/providers/streaming-tool-loop-shared'
import { adaptOpenAIChatToolSchema } from '@/providers/tool-schema-adapter'
import { enrichLastModelSegmentFromChatCompletions } from '@/providers/trace-enrichment'
import { openAICompatTransport } from '@/providers/transport'
import type {
FunctionCallResponse,
Message,
ProviderConfig,
ProviderRequest,
ProviderResponse,
TimeSegment,
} from '@/providers/types'
import { ProviderError } from '@/providers/types'
import {
calculateCost,
generateSchemaInstructions,
isFunctionToolCall,
prepareToolExecution,
prepareToolsWithUsageControl,
sumToolCosts,
} from '@/providers/utils'
type ChatCompletionPayload,
executeChatCompletionRequest,
} from '@/providers/openai-compat/chat-completions'
import { openAICompatTransport } from '@/providers/transport'
import type { ProviderConfig, ProviderRequest, ProviderResponse } from '@/providers/types'
const logger = createLogger('FireworksProvider')
/**
* Applies structured output configuration to a payload based on model capabilities.
* Uses native json_schema for supported models, falls back to json_object with prompt instructions.
*/
/** Fireworks accepts native JSON schemas. */
async function applyResponseFormat(
targetPayload: any,
messages: any[],
responseFormat: any,
targetPayload: ChatCompletionPayload,
messages: ChatCompletionMessageParam[],
responseFormat: NonNullable<ProviderRequest['responseFormat']>,
model: string
): Promise<any[]> {
const useNative = await supportsNativeStructuredOutputs(model)
if (useNative) {
logger.info('Using native structured outputs for Fireworks model', { model })
targetPayload.response_format = {
type: 'json_schema',
json_schema: {
name: responseFormat.name || 'response_schema',
schema: responseFormat.schema || responseFormat,
},
}
return messages
): Promise<ChatCompletionMessageParam[]> {
logger.info('Using native structured outputs for Fireworks model', { model })
targetPayload.response_format = {
type: 'json_schema',
json_schema: {
name: responseFormat.name || 'response_schema',
schema: responseFormat.schema || responseFormat,
},
}
logger.info('Using json_object mode with prompt instructions for Fireworks model', { model })
const schema = responseFormat.schema || responseFormat
const schemaInstructions = generateSchemaInstructions(schema, responseFormat.name)
targetPayload.response_format = { type: 'json_object' }
return [...messages, { role: 'user', content: schemaInstructions }]
return messages
}
export const fireworksProvider: ProviderConfig = {
@@ -114,624 +64,15 @@ export const fireworksProvider: ProviderConfig = {
stream: !!request.stream,
})
const allMessages: Message[] = []
if (request.systemPrompt) {
allMessages.push({ role: 'system', content: request.systemPrompt })
}
if (request.context) {
allMessages.push({ role: 'user', content: request.context })
}
if (request.messages) {
allMessages.push(...request.messages)
}
const formattedMessages = formatMessagesForProvider(allMessages, 'fireworks') as Message[]
const tools = request.tools?.length
? request.tools.map((tool) => adaptOpenAIChatToolSchema(tool))
: undefined
const payload: any = {
model: requestedModel,
messages: formattedMessages,
}
if (request.temperature !== undefined) payload.temperature = request.temperature
if (request.maxTokens != null) payload.max_tokens = request.maxTokens
let preparedTools: ReturnType<typeof prepareToolsWithUsageControl> | null = null
let hasActiveTools = false
if (tools?.length) {
preparedTools = prepareToolsWithUsageControl(tools, request.tools, logger, 'fireworks')
const { tools: filteredTools, toolChoice } = preparedTools
if (filteredTools?.length && toolChoice) {
payload.tools = filteredTools
payload.tool_choice = toolChoice
hasActiveTools = true
}
}
const providerStartTime = Date.now()
const providerStartTimeISO = new Date(providerStartTime).toISOString()
try {
if (request.responseFormat && !hasActiveTools) {
payload.messages = await applyResponseFormat(
payload,
payload.messages,
request.responseFormat,
requestedModel
)
}
if (request.stream && (!tools || tools.length === 0 || !hasActiveTools)) {
const streamingParams: ChatCompletionCreateParamsStreaming = {
...payload,
stream: true,
stream_options: { include_usage: true },
}
const streamResponse = await client.chat.completions.create(
await prepareConversationGeneration(request, 'chat-completions', streamingParams),
request.abortSignal ? { signal: request.abortSignal } : undefined
)
const streamingResult = createStreamingExecution({
// Echo the catalog id, never the wire name: it is the billing and
// logging identity (cost policy, ledger row, trace span, model
// breakdown) the way every other Sim-hosted provider reports it.
model: request.model,
providerStartTime,
providerStartTimeISO,
timing: { kind: 'simple', segmentName: request.model },
initialTokens: { input: 0, output: 0, total: 0 },
initialCost: { input: 0, output: 0, total: 0 },
streamFormat: 'agent-events-v1',
createStream: ({ output, finalizeTiming }) =>
createReadableStreamFromOpenAIStream(
streamResponse,
(content, usage) => {
output.content = content
output.tokens = {
input: usage.prompt_tokens,
output: usage.completion_tokens,
total: usage.total_tokens,
}
// Pricing keys on the catalog id (fireworks/<name>), not the wire
// name — static hosted entries price; dynamic ids stay unpriced.
const costResult = calculateCost(
request.model,
usage.prompt_tokens,
usage.completion_tokens
)
output.cost = {
input: costResult.input,
output: costResult.output,
total: costResult.total,
}
finalizeTiming()
},
request
),
})
return streamingResult
}
const initialCallTime = Date.now()
const originalToolChoice = payload.tool_choice
const forcedTools = preparedTools?.forcedTools || []
let usedForcedTools: string[] = []
let currentResponse = await client.chat.completions.create(
await prepareConversationGeneration(request, 'chat-completions', payload),
request.abortSignal ? { signal: request.abortSignal } : undefined
)
if (!currentResponse.choices[0]?.message?.tool_calls?.length) {
await captureProviderConversationStep(
request,
'chat-completions',
currentResponse.choices[0]?.message,
getChatCompletionConversationUsage(currentResponse.usage)
)
}
const firstResponseTime = Date.now() - initialCallTime
let content = currentResponse.choices[0]?.message?.content || ''
const tokens = {
input: currentResponse.usage?.prompt_tokens || 0,
output: currentResponse.usage?.completion_tokens || 0,
total: currentResponse.usage?.total_tokens || 0,
}
const toolCalls: FunctionCallResponse[] = []
const toolResults: Record<string, unknown>[] = []
const currentMessages = [...formattedMessages]
let iterationCount = 0
let modelTime = firstResponseTime
let toolsTime = 0
let hasUsedForcedTool = false
const timeSegments: TimeSegment[] = [
{
type: 'model',
name: request.model,
startTime: initialCallTime,
endTime: initialCallTime + firstResponseTime,
duration: firstResponseTime,
},
]
const forcedToolResult = checkForForcedToolUsage(
currentResponse,
originalToolChoice,
forcedTools,
usedForcedTools
)
hasUsedForcedTool = forcedToolResult.hasUsedForcedTool
usedForcedTools = forcedToolResult.usedForcedTools
while (iterationCount < MAX_TOOL_ITERATIONS) {
if (currentResponse.choices[0]?.message?.content) {
content = currentResponse.choices[0].message.content
}
const toolCallsInResponse =
currentResponse.choices[0]?.message?.tool_calls?.filter(isFunctionToolCall)
enrichLastModelSegmentFromChatCompletions(
timeSegments,
currentResponse,
toolCallsInResponse,
{ model: request.model, provider: 'fireworks' }
)
if (!toolCallsInResponse || toolCallsInResponse.length === 0) {
break
}
const toolsStartTime = Date.now()
await captureProviderConversationStep(
request,
'chat-completions',
currentResponse.choices[0]?.message,
getChatCompletionConversationUsage(currentResponse.usage)
)
const toolExecutionPromises = toolCallsInResponse.map(async (toolCall) => {
const toolCallStartTime = Date.now()
const toolName = toolCall.function.name
try {
const toolArgs = parseToolArguments(toolCall.function.arguments, toolName)
const tool = request.tools?.find((t) => t.id === toolName)
if (!tool) {
await recordProviderConversationToolError(
request,
toolCall.id,
toolName,
`Tool "${toolName}" is not available`
)
const toolCallEndTime = Date.now()
return {
toolCall,
toolName,
toolParams: {},
result: {
success: false,
output: undefined,
error: `Tool "${toolName}" is not available`,
},
startTime: toolCallStartTime,
endTime: toolCallEndTime,
duration: toolCallEndTime - toolCallStartTime,
}
}
const { toolParams, executionParams } = prepareToolExecution(
tool,
toolArgs,
request,
toolCall.id
)
const { rawResponse, modelResponse } = await executeProviderTool(
toolName,
executionParams,
{
signal: request.abortSignal,
}
)
const toolCallEndTime = Date.now()
return {
toolCall,
toolName,
toolParams,
result: rawResponse,
modelResult: modelResponse,
startTime: toolCallStartTime,
endTime: toolCallEndTime,
duration: toolCallEndTime - toolCallStartTime,
}
} catch (error) {
if (isAbortError(error) || request.abortSignal?.aborted) {
throw error
}
await recordProviderConversationToolError(
request,
toolCall.id,
toolName,
getErrorMessage(error, 'Tool execution failed')
)
const toolCallEndTime = Date.now()
logger.error('Error processing tool call (Fireworks):', {
error: toError(error).message,
toolName,
})
return {
toolCall,
toolName,
toolParams: {},
result: {
success: false,
output: undefined,
error: getErrorMessage(error, 'Tool execution failed'),
},
startTime: toolCallStartTime,
endTime: toolCallEndTime,
duration: toolCallEndTime - toolCallStartTime,
}
}
})
const executionResults = await Promise.all(toolExecutionPromises)
const assistantMessage = currentResponse.choices[0]?.message
if (assistantMessage) {
currentMessages.push(
createOpenAICompatAssistantHistory({
message: assistantMessage,
toolCalls: toolCallsInResponse,
reasoningFields: ['reasoning_content'],
})
)
}
for (const executionResult of executionResults) {
const { toolCall, toolName, toolParams, result, startTime, endTime, duration } =
executionResult
const modelResult =
'modelResult' in executionResult ? (executionResult.modelResult ?? result) : result
timeSegments.push({
type: 'tool',
name: toolName,
startTime: startTime,
endTime: endTime,
duration: duration,
toolCallId: toolCall.id,
})
let resultContent: unknown
if (result.success) {
if (isRecordLike(result.output)) {
toolResults.push(result.output)
}
resultContent = result.output ?? null
} else {
resultContent = {
error: true,
message: result.error || 'Tool execution failed',
tool: toolName,
}
}
const modelResultContent = modelResult.success
? (modelResult.output ?? null)
: {
error: true,
message: modelResult.error || 'Tool execution failed',
tool: toolName,
}
toolCalls.push({
name: toolName,
arguments: toolParams,
startTime: new Date(startTime).toISOString(),
endTime: new Date(endTime).toISOString(),
duration: duration,
result: resultContent,
success: result.success,
})
currentMessages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify(modelResultContent),
})
}
const thisToolsTime = Date.now() - toolsStartTime
toolsTime += thisToolsTime
const nextPayload = {
...payload,
messages: currentMessages,
}
if (typeof originalToolChoice === 'object' && hasUsedForcedTool && forcedTools.length > 0) {
const remainingTools = forcedTools.filter((tool) => !usedForcedTools.includes(tool))
if (remainingTools.length > 0) {
nextPayload.tool_choice = { type: 'function', function: { name: remainingTools[0] } }
} else {
nextPayload.tool_choice = 'auto'
}
}
const nextModelStartTime = Date.now()
currentResponse = await client.chat.completions.create(
await prepareConversationGeneration(request, 'chat-completions', nextPayload),
request.abortSignal ? { signal: request.abortSignal } : undefined
)
if (!currentResponse.choices[0]?.message?.tool_calls?.length) {
await captureProviderConversationStep(
request,
'chat-completions',
currentResponse.choices[0]?.message,
getChatCompletionConversationUsage(currentResponse.usage)
)
}
const nextForcedToolResult = checkForForcedToolUsage(
currentResponse,
nextPayload.tool_choice,
forcedTools,
usedForcedTools
)
hasUsedForcedTool = nextForcedToolResult.hasUsedForcedTool
usedForcedTools = nextForcedToolResult.usedForcedTools
const nextModelEndTime = Date.now()
const thisModelTime = nextModelEndTime - nextModelStartTime
timeSegments.push({
type: 'model',
name: request.model,
startTime: nextModelStartTime,
endTime: nextModelEndTime,
duration: thisModelTime,
})
modelTime += thisModelTime
if (currentResponse.choices[0]?.message?.content) {
content = currentResponse.choices[0].message.content
}
if (currentResponse.usage) {
tokens.input += currentResponse.usage.prompt_tokens || 0
tokens.output += currentResponse.usage.completion_tokens || 0
tokens.total += currentResponse.usage.total_tokens || 0
}
iterationCount++
}
if (iterationCount === MAX_TOOL_ITERATIONS) {
const pendingToolCalls =
currentResponse.choices[0]?.message?.tool_calls?.filter(isFunctionToolCall)
enrichLastModelSegmentFromChatCompletions(timeSegments, currentResponse, pendingToolCalls, {
model: request.model,
provider: 'fireworks',
})
if (pendingToolCalls?.length && !(request.responseFormat && hasActiveTools)) {
const finalPayload: any = {
...payload,
messages: [...currentMessages],
tool_choice: 'none',
}
if (request.responseFormat) {
finalPayload.messages = await applyResponseFormat(
finalPayload,
finalPayload.messages,
request.responseFormat,
requestedModel
)
}
const finalStartTime = Date.now()
const finalResponse = await client.chat.completions.create(
await prepareConversationGeneration(request, 'chat-completions', finalPayload),
request.abortSignal ? { signal: request.abortSignal } : undefined
)
if (!finalResponse.choices[0]?.message?.tool_calls?.length) {
await captureProviderConversationStep(
request,
'chat-completions',
finalResponse.choices[0]?.message,
getChatCompletionConversationUsage(finalResponse.usage)
)
}
const finalEndTime = Date.now()
const finalDuration = finalEndTime - finalStartTime
timeSegments.push({
type: 'model',
name: 'Final answer after tool iteration limit',
startTime: finalStartTime,
endTime: finalEndTime,
duration: finalDuration,
})
modelTime += finalDuration
if (finalResponse.choices[0]?.message?.content) {
content = finalResponse.choices[0].message.content
}
if (finalResponse.usage) {
tokens.input += finalResponse.usage.prompt_tokens || 0
tokens.output += finalResponse.usage.completion_tokens || 0
tokens.total += finalResponse.usage.total_tokens || 0
}
enrichLastModelSegmentFromChatCompletions(
timeSegments,
finalResponse,
finalResponse.choices[0]?.message?.tool_calls?.filter(isFunctionToolCall),
{ model: request.model, provider: 'fireworks' }
)
}
}
if (request.responseFormat && hasActiveTools) {
const finalPayload: any = {
model: payload.model,
messages: [...currentMessages],
}
if (payload.temperature !== undefined) {
finalPayload.temperature = payload.temperature
}
if (payload.max_tokens !== undefined) {
finalPayload.max_tokens = payload.max_tokens
}
finalPayload.messages = await applyResponseFormat(
finalPayload,
finalPayload.messages,
request.responseFormat,
requestedModel
)
const finalStartTime = Date.now()
const finalResponse = await client.chat.completions.create(
await prepareConversationGeneration(request, 'chat-completions', finalPayload),
request.abortSignal ? { signal: request.abortSignal } : undefined
)
if (!finalResponse.choices[0]?.message?.tool_calls?.length) {
await captureProviderConversationStep(
request,
'chat-completions',
finalResponse.choices[0]?.message,
getChatCompletionConversationUsage(finalResponse.usage)
)
}
const finalEndTime = Date.now()
const finalDuration = finalEndTime - finalStartTime
timeSegments.push({
type: 'model',
name: 'Final structured response',
startTime: finalStartTime,
endTime: finalEndTime,
duration: finalDuration,
})
modelTime += finalDuration
if (finalResponse.choices[0]?.message?.content) {
content = finalResponse.choices[0].message.content
}
if (finalResponse.usage) {
tokens.input += finalResponse.usage.prompt_tokens || 0
tokens.output += finalResponse.usage.completion_tokens || 0
tokens.total += finalResponse.usage.total_tokens || 0
}
enrichLastModelSegmentFromChatCompletions(
timeSegments,
finalResponse,
finalResponse.choices[0]?.message?.tool_calls?.filter(isFunctionToolCall),
{ model: request.model, provider: 'fireworks' }
)
}
if (request.stream) {
// Pricing keys on the catalog id (fireworks/<name>), not the wire name.
const accumulatedCost = calculateCost(request.model, tokens.input, tokens.output)
const toolCost = sumToolCosts(toolResults)
const finalCost = {
input: accumulatedCost.input,
output: accumulatedCost.output,
toolCost: toolCost || undefined,
total: accumulatedCost.total + toolCost,
}
const streamingResult = createStreamingExecution({
model: request.model,
providerStartTime,
providerStartTimeISO,
timing: {
kind: 'accumulated',
modelTime,
toolsTime,
firstResponseTime,
iterations: timeSegments.filter((segment) => segment.type === 'model').length,
timeSegments,
},
initialTokens: { input: tokens.input, output: tokens.output, total: tokens.total },
initialCost: finalCost,
toolCalls:
toolCalls.length > 0 ? { list: toolCalls, count: toolCalls.length } : undefined,
streamFormat: 'agent-events-v1',
createStream: ({ output, finalizeTiming }) => {
output.content = content
output.tokens = { input: tokens.input, output: tokens.output, total: tokens.total }
output.cost = finalCost
finalizeTiming()
return createSettledAgentEventStream(content)
},
})
return streamingResult
}
const providerEndTime = Date.now()
const providerEndTimeISO = new Date(providerEndTime).toISOString()
const totalDuration = providerEndTime - providerStartTime
return {
content,
model: request.model,
tokens,
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
toolResults: toolResults.length > 0 ? toolResults : undefined,
timing: {
startTime: providerStartTimeISO,
endTime: providerEndTimeISO,
duration: totalDuration,
modelTime: modelTime,
toolsTime: toolsTime,
firstResponseTime: firstResponseTime,
iterations: timeSegments.filter((segment) => segment.type === 'model').length,
timeSegments: timeSegments,
},
}
} catch (error) {
const providerEndTime = Date.now()
const providerEndTimeISO = new Date(providerEndTime).toISOString()
const totalDuration = providerEndTime - providerStartTime
const errorDetails: Record<string, any> = {
error: toError(error).message,
duration: totalDuration,
}
if (error && typeof error === 'object') {
const err = error as any
if (err.status) errorDetails.status = err.status
if (err.code) errorDetails.code = err.code
if (err.type) errorDetails.type = err.type
if (err.error?.message) errorDetails.providerMessage = err.error.message
if (err.error?.metadata) errorDetails.metadata = err.error.metadata
}
logger.error('Error in Fireworks request:', errorDetails)
if (
isAbortError(error) ||
request.abortSignal?.aborted ||
isConversationContextError(error)
) {
throw error
}
throw new ProviderError(toError(error).message, {
startTime: providerStartTimeISO,
endTime: providerEndTimeISO,
duration: totalDuration,
})
}
return executeChatCompletionRequest(request, {
providerId: 'fireworks',
providerName: 'Fireworks',
client,
requestedModel,
reportedModel: request.model,
logger,
applyResponseFormat,
reasoningFields: ['reasoning_content'],
})
},
}
-52
View File
@@ -1,10 +1,3 @@
import type { ChatCompletionChunk } from 'openai/resources/chat/completions'
import type { CompletionUsage } from 'openai/resources/completions'
import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events'
import type { AgentStreamEvent } from '@/providers/stream-events'
import type { ProviderRequest } from '@/providers/types'
import { checkForForcedToolUsageOpenAI } from '@/providers/utils'
/**
* Wire model names for the static hosted Fireworks catalog entries (the
* sim-auto pool). Catalog ids are the short `fireworks/<name>` form; the
@@ -24,48 +17,3 @@ const FIREWORKS_WIRE_NAMES: Record<string, string> = {
export function resolveFireworksWireModel(strippedModel: string): string {
return FIREWORKS_WIRE_NAMES[strippedModel] ?? strippedModel
}
/**
* Checks if a model supports native structured outputs (json_schema).
* Fireworks AI supports structured outputs across their inference API.
*/
export async function supportsNativeStructuredOutputs(_modelId: string): Promise<boolean> {
return true
}
/**
* Creates an agent-events stream from a Fireworks streaming response.
* Uses the shared OpenAI-compatible agent event streaming utility.
*/
export function createReadableStreamFromOpenAIStream(
openaiStream: AsyncIterable<ChatCompletionChunk>,
onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void,
request?: ProviderRequest
): ReadableStream<AgentStreamEvent> {
return createOpenAICompatibleAgentEventStream(openaiStream, {
request,
providerName: 'Fireworks',
onComplete: onComplete
? (result) => onComplete(result.content, result.usage, result.thinking)
: undefined,
})
}
/**
* Checks if a forced tool was used in a Fireworks response.
* Uses the shared OpenAI-compatible forced tool usage helper.
*/
export function checkForForcedToolUsage(
response: any,
toolChoice: string | { type: string; function?: { name: string }; name?: string; any?: any },
forcedTools: string[],
usedForcedTools: string[]
): { hasUsedForcedTool: boolean; usedForcedTools: string[] } {
return checkForForcedToolUsageOpenAI(
response,
toolChoice,
'Fireworks',
forcedTools,
usedForcedTools
)
}
-4
View File
@@ -32,10 +32,6 @@ vi.mock('@/providers/models', () => providersModelsMock)
vi.mock('@/providers/attachments', () => providersAttachmentsMock)
vi.mock('@/providers/groq/utils', () => ({
createReadableStreamFromGroqStream: vi.fn(),
}))
vi.mock('@/providers/openai-compat/streaming-tool-loop', () => ({
createOpenAICompatStreamingToolLoopStream: vi.fn(),
}))
+32 -29
View File
@@ -19,9 +19,9 @@ import {
recordProviderConversationToolError,
recordProviderConversationUsage,
} from '@/providers/conversation-history'
import { createReadableStreamFromGroqStream } from '@/providers/groq/utils'
import { getProviderDefaultModel, getProviderModels } from '@/providers/models'
import { getChatCompletionConversationUsage } from '@/providers/openai-compat/conversation-usage'
import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events'
import { createOpenAICompatStreamingToolLoopStream } from '@/providers/openai-compat/streaming-tool-loop'
import { executeProviderTool } from '@/providers/runtime-context'
import { createStreamingExecution } from '@/providers/streaming-execution'
@@ -240,37 +240,40 @@ export const groqProvider: ProviderConfig = {
isStreaming: true,
streamFormat: 'agent-events-v1',
createStream: ({ output, finalizeTiming }) =>
createReadableStreamFromGroqStream(
createOpenAICompatibleAgentEventStream(
// double-cast-allowed: payload is untyped so the SDK cannot resolve the streaming overload; groq-sdk stream chunks are wire-compatible with the OpenAI ChatCompletionChunk shape the adapter consumes
streamResponse as unknown as AsyncIterable<ChatCompletionChunk>,
(content, usage, thinking) => {
output.content = content
output.tokens = {
input: usage.prompt_tokens,
output: usage.completion_tokens,
total: usage.total_tokens,
}
const costResult = calculateCost(
request.model,
usage.prompt_tokens,
usage.completion_tokens
)
output.cost = {
input: costResult.input,
output: costResult.output,
total: costResult.total,
}
if (thinking) {
const segment = output.providerTiming?.timeSegments?.[0]
if (segment) {
segment.thinkingContent = thinking
{
providerName: 'Groq',
request,
onComplete: ({ content, usage, thinking }) => {
output.content = content
output.tokens = {
input: usage.prompt_tokens,
output: usage.completion_tokens,
total: usage.total_tokens,
}
}
finalizeTiming()
},
request
const costResult = calculateCost(
request.model,
usage.prompt_tokens,
usage.completion_tokens
)
output.cost = {
input: costResult.input,
output: costResult.output,
total: costResult.total,
}
if (thinking) {
const segment = output.providerTiming?.timeSegments?.[0]
if (segment) {
segment.thinkingContent = thinking
}
}
finalizeTiming()
},
}
),
})
-23
View File
@@ -1,23 +0,0 @@
import type { ChatCompletionChunk } from 'openai/resources/chat/completions'
import type { CompletionUsage } from 'openai/resources/completions'
import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events'
import type { AgentStreamEvent } from '@/providers/stream-events'
import type { ProviderRequest } from '@/providers/types'
/**
* Creates an agent-events stream from a Groq streaming response.
* Uses the shared OpenAI-compatible agent event streaming utility.
*/
export function createReadableStreamFromGroqStream(
groqStream: AsyncIterable<ChatCompletionChunk>,
onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void,
request?: ProviderRequest
): ReadableStream<AgentStreamEvent> {
return createOpenAICompatibleAgentEventStream(groqStream, {
request,
providerName: 'Groq',
onComplete: onComplete
? (result) => onComplete(result.content, result.usage, result.thinking)
: undefined,
})
}
+24 -21
View File
@@ -14,7 +14,6 @@ import {
captureProviderConversationStep,
recordProviderConversationToolError,
} from '@/providers/conversation-history'
import { createReadableStreamFromKimiStream } from '@/providers/kimi/utils'
import {
getModelCapabilities,
getProviderDefaultModel,
@@ -22,6 +21,7 @@ import {
} from '@/providers/models'
import { createOpenAICompatAssistantHistory } from '@/providers/openai-compat/assistant-history'
import { getChatCompletionConversationUsage } from '@/providers/openai-compat/conversation-usage'
import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events'
import { executeProviderTool } from '@/providers/runtime-context'
import { createSettledAgentEventStream } from '@/providers/stream-events'
import { createStreamingExecution } from '@/providers/streaming-execution'
@@ -239,29 +239,32 @@ export const kimiProvider: ProviderConfig = {
isStreaming: true,
streamFormat: 'agent-events-v1',
createStream: ({ output }) =>
createReadableStreamFromKimiStream(
createOpenAICompatibleAgentEventStream(
// double-cast-allowed: payload is untyped so the SDK cannot resolve the streaming overload; the stream yields OpenAI ChatCompletionChunk objects
streamResponse as unknown as AsyncIterable<ChatCompletionChunk>,
(content, usage) => {
output.content = content
output.tokens = {
input: usage.prompt_tokens,
output: usage.completion_tokens,
total: usage.total_tokens,
}
{
providerName: 'Kimi',
request,
onComplete: ({ content, usage }) => {
output.content = content
output.tokens = {
input: usage.prompt_tokens,
output: usage.completion_tokens,
total: usage.total_tokens,
}
const costResult = calculateCost(
request.model,
usage.prompt_tokens,
usage.completion_tokens
)
output.cost = {
input: costResult.input,
output: costResult.output,
total: costResult.total,
}
},
request
const costResult = calculateCost(
request.model,
usage.prompt_tokens,
usage.completion_tokens
)
output.cost = {
input: costResult.input,
output: costResult.output,
total: costResult.total,
}
},
}
),
})
-23
View File
@@ -1,23 +0,0 @@
import type { ChatCompletionChunk } from 'openai/resources/chat/completions'
import type { CompletionUsage } from 'openai/resources/completions'
import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events'
import type { AgentStreamEvent } from '@/providers/stream-events'
import type { ProviderRequest } from '@/providers/types'
/**
* Creates an agent-events stream from a Kimi (Moonshot AI) streaming response.
* Uses the shared OpenAI-compatible agent event streaming utility.
*/
export function createReadableStreamFromKimiStream(
kimiStream: AsyncIterable<ChatCompletionChunk>,
onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void,
request?: ProviderRequest
): ReadableStream<AgentStreamEvent> {
return createOpenAICompatibleAgentEventStream(kimiStream, {
request,
providerName: 'Kimi',
onComplete: onComplete
? (result) => onComplete(result.content, result.usage, result.thinking)
: undefined,
})
}
+1 -6
View File
@@ -30,12 +30,6 @@ vi.mock('@/providers/attachments', () => providersAttachmentsMock)
vi.mock('@/providers/trace-enrichment', () => providersTraceEnrichmentMock)
vi.mock('@/providers/litellm/utils', () => ({
createReadableStreamFromLiteLLMStream: vi.fn(
() => new ReadableStream({ start: (c) => c.close() })
),
}))
vi.mock('@/providers/utils', () => providersUtilsMock)
import { litellmProvider } from '@/providers/litellm'
@@ -347,6 +341,7 @@ describe('litellmProvider.executeRequest', () => {
})
it('returns a streaming execution when streaming without active tools', async () => {
mockCreate.mockResolvedValueOnce((async function* () {})())
const result = await run({ stream: true })
expect(firstPayload().stream).toBe(true)
+6 -6
View File
@@ -15,10 +15,10 @@ import {
captureProviderConversationStep,
recordProviderConversationToolError,
} from '@/providers/conversation-history'
import { createReadableStreamFromLiteLLMStream } from '@/providers/litellm/utils'
import { getProviderDefaultModel, getProviderModels } from '@/providers/models'
import { createOpenAICompatAssistantHistory } from '@/providers/openai-compat/assistant-history'
import { getChatCompletionConversationUsage } from '@/providers/openai-compat/conversation-usage'
import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events'
import { executeProviderTool } from '@/providers/runtime-context'
import { createSettledAgentEventStream } from '@/providers/stream-events'
import { createStreamingExecution } from '@/providers/streaming-execution'
@@ -237,9 +237,10 @@ export const litellmProvider: ProviderConfig = {
isStreaming: true,
streamFormat: 'agent-events-v1',
createStream: ({ output, finalizeTiming }) =>
createReadableStreamFromLiteLLMStream(
streamResponse,
(content, usage) => {
createOpenAICompatibleAgentEventStream(streamResponse, {
providerName: 'LiteLLM',
request,
onComplete: ({ content, usage }) => {
let cleanContent = content
if (cleanContent && request.responseFormat) {
cleanContent = cleanContent.replace(/```json\n?|\n?```/g, '').trim()
@@ -265,8 +266,7 @@ export const litellmProvider: ProviderConfig = {
finalizeTiming()
},
request
),
}),
})
return streamingResult
-23
View File
@@ -1,23 +0,0 @@
import type { ChatCompletionChunk } from 'openai/resources/chat/completions'
import type { CompletionUsage } from 'openai/resources/completions'
import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events'
import type { AgentStreamEvent } from '@/providers/stream-events'
import type { ProviderRequest } from '@/providers/types'
/**
* Creates an agent-events stream from a LiteLLM streaming response.
* Uses the shared OpenAI-compatible agent event streaming utility.
*/
export function createReadableStreamFromLiteLLMStream(
litellmStream: AsyncIterable<ChatCompletionChunk>,
onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void,
request?: ProviderRequest
): ReadableStream<AgentStreamEvent> {
return createOpenAICompatibleAgentEventStream(litellmStream, {
request,
providerName: 'LiteLLM',
onComplete: onComplete
? (result) => onComplete(result.content, result.usage, result.thinking)
: undefined,
})
}
+24 -21
View File
@@ -14,9 +14,9 @@ import {
captureProviderConversationStep,
recordProviderConversationToolError,
} from '@/providers/conversation-history'
import { createReadableStreamFromMetaStream } from '@/providers/meta/utils'
import { getProviderDefaultModel, getProviderModels } from '@/providers/models'
import { getChatCompletionConversationUsage } from '@/providers/openai-compat/conversation-usage'
import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events'
import { executeProviderTool } from '@/providers/runtime-context'
import { createSettledAgentEventStream } from '@/providers/stream-events'
import { createStreamingExecution } from '@/providers/streaming-execution'
@@ -190,29 +190,32 @@ export const metaProvider: ProviderConfig = {
isStreaming: true,
streamFormat: 'agent-events-v1',
createStream: ({ output }) =>
createReadableStreamFromMetaStream(
createOpenAICompatibleAgentEventStream(
// double-cast-allowed: payload is untyped so the SDK cannot resolve the streaming overload; the stream yields OpenAI ChatCompletionChunk objects
streamResponse as unknown as AsyncIterable<ChatCompletionChunk>,
(content, usage) => {
output.content = content
output.tokens = {
input: usage.prompt_tokens,
output: usage.completion_tokens,
total: usage.total_tokens,
}
{
providerName: 'Meta',
request,
onComplete: ({ content, usage }) => {
output.content = content
output.tokens = {
input: usage.prompt_tokens,
output: usage.completion_tokens,
total: usage.total_tokens,
}
const costResult = calculateCost(
request.model,
usage.prompt_tokens,
usage.completion_tokens
)
output.cost = {
input: costResult.input,
output: costResult.output,
total: costResult.total,
}
},
request
const costResult = calculateCost(
request.model,
usage.prompt_tokens,
usage.completion_tokens
)
output.cost = {
input: costResult.input,
output: costResult.output,
total: costResult.total,
}
},
}
),
})
-23
View File
@@ -1,23 +0,0 @@
import type { ChatCompletionChunk } from 'openai/resources/chat/completions'
import type { CompletionUsage } from 'openai/resources/completions'
import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events'
import type { AgentStreamEvent } from '@/providers/stream-events'
import type { ProviderRequest } from '@/providers/types'
/**
* Creates an agent-events stream from a Meta Model API streaming response.
* Uses the shared OpenAI-compatible agent event streaming utility.
*/
export function createReadableStreamFromMetaStream(
metaStream: AsyncIterable<ChatCompletionChunk>,
onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void,
request?: ProviderRequest
): ReadableStream<AgentStreamEvent> {
return createOpenAICompatibleAgentEventStream(metaStream, {
request,
providerName: 'Meta',
onComplete: onComplete
? (result) => onComplete(result.content, result.usage, result.thinking)
: undefined,
})
}
+1 -3
View File
@@ -12,9 +12,7 @@ import type { ProviderToolConfig } from '@/providers/types'
vi.mock('openai', () => openaiMock)
vi.mock('@/providers', () => providersMock)
vi.mock('@/providers/attachments', () => providersAttachmentsMock)
vi.mock('@/providers/mistral/utils', () => ({
createReadableStreamFromMistralStream: vi.fn(),
}))
vi.mock('@/providers/models', () => providersModelsMock)
vi.mock('@/providers/trace-enrichment', () => providersTraceEnrichmentMock)
vi.mock('@/providers/utils', () => providersUtilsMock)
+6 -6
View File
@@ -14,9 +14,9 @@ import {
captureProviderConversationStep,
recordProviderConversationToolError,
} from '@/providers/conversation-history'
import { createReadableStreamFromMistralStream } from '@/providers/mistral/utils'
import { getProviderDefaultModel, getProviderModels } from '@/providers/models'
import { getChatCompletionConversationUsage } from '@/providers/openai-compat/conversation-usage'
import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events'
import { executeProviderTool } from '@/providers/runtime-context'
import { createSettledAgentEventStream } from '@/providers/stream-events'
import { createStreamingExecution } from '@/providers/streaming-execution'
@@ -177,9 +177,10 @@ export const mistralProvider: ProviderConfig = {
initialCost: { input: 0, output: 0, total: 0 },
streamFormat: 'agent-events-v1',
createStream: ({ output, finalizeTiming }) =>
createReadableStreamFromMistralStream(
streamResponse,
(content, usage) => {
createOpenAICompatibleAgentEventStream(streamResponse, {
providerName: 'Mistral',
request,
onComplete: ({ content, usage }) => {
output.content = content
output.tokens = {
input: usage.prompt_tokens,
@@ -200,8 +201,7 @@ export const mistralProvider: ProviderConfig = {
finalizeTiming()
},
request
),
}),
})
return streamingResult
-23
View File
@@ -1,23 +0,0 @@
import type { ChatCompletionChunk } from 'openai/resources/chat/completions'
import type { CompletionUsage } from 'openai/resources/completions'
import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events'
import type { AgentStreamEvent } from '@/providers/stream-events'
import type { ProviderRequest } from '@/providers/types'
/**
* Creates an agent-events stream from a Mistral streaming response.
* Uses the shared OpenAI-compatible agent event streaming utility.
*/
export function createReadableStreamFromMistralStream(
mistralStream: AsyncIterable<ChatCompletionChunk>,
onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void,
request?: ProviderRequest
): ReadableStream<AgentStreamEvent> {
return createOpenAICompatibleAgentEventStream(mistralStream, {
request,
providerName: 'Mistral',
onComplete: onComplete
? (result) => onComplete(result.content, result.usage, result.thinking)
: undefined,
})
}
+24 -21
View File
@@ -19,9 +19,9 @@ import {
getProviderDefaultModel,
getProviderModels,
} from '@/providers/models'
import { createReadableStreamFromNvidiaStream } from '@/providers/nvidia/utils'
import { createOpenAICompatAssistantHistory } from '@/providers/openai-compat/assistant-history'
import { getChatCompletionConversationUsage } from '@/providers/openai-compat/conversation-usage'
import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events'
import { executeProviderTool } from '@/providers/runtime-context'
import { createSettledAgentEventStream } from '@/providers/stream-events'
import { createStreamingExecution } from '@/providers/streaming-execution'
@@ -190,29 +190,32 @@ export const nvidiaProvider: ProviderConfig = {
isStreaming: true,
streamFormat: 'agent-events-v1',
createStream: ({ output }) =>
createReadableStreamFromNvidiaStream(
createOpenAICompatibleAgentEventStream(
// double-cast-allowed: payload is untyped so the SDK cannot resolve the streaming overload; the stream yields OpenAI ChatCompletionChunk objects
streamResponse as unknown as AsyncIterable<ChatCompletionChunk>,
(content, usage) => {
output.content = content
output.tokens = {
input: usage.prompt_tokens,
output: usage.completion_tokens,
total: usage.total_tokens,
}
{
providerName: 'NVIDIA',
request,
onComplete: ({ content, usage }) => {
output.content = content
output.tokens = {
input: usage.prompt_tokens,
output: usage.completion_tokens,
total: usage.total_tokens,
}
const costResult = calculateCost(
request.model,
usage.prompt_tokens,
usage.completion_tokens
)
output.cost = {
input: costResult.input,
output: costResult.output,
total: costResult.total,
}
},
request
const costResult = calculateCost(
request.model,
usage.prompt_tokens,
usage.completion_tokens
)
output.cost = {
input: costResult.input,
output: costResult.output,
total: costResult.total,
}
},
}
),
})
-23
View File
@@ -1,23 +0,0 @@
import type { ChatCompletionChunk } from 'openai/resources/chat/completions'
import type { CompletionUsage } from 'openai/resources/completions'
import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events'
import type { AgentStreamEvent } from '@/providers/stream-events'
import type { ProviderRequest } from '@/providers/types'
/**
* Creates an agent-events stream from an NVIDIA NIM streaming response.
* Uses the shared OpenAI-compatible agent event streaming utility.
*/
export function createReadableStreamFromNvidiaStream(
nvidiaStream: AsyncIterable<ChatCompletionChunk>,
onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void,
request?: ProviderRequest
): ReadableStream<AgentStreamEvent> {
return createOpenAICompatibleAgentEventStream(nvidiaStream, {
request,
providerName: 'NVIDIA',
onComplete: onComplete
? (result) => onComplete(result.content, result.usage, result.thinking)
: undefined,
})
}
+1 -20
View File
@@ -9,30 +9,12 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
type StreamUsage = { prompt_tokens: number; completion_tokens: number; total_tokens: number }
const { streamOnComplete } = vi.hoisted(() => ({
streamOnComplete: {
current: undefined as undefined | ((content: string, usage: StreamUsage) => void),
},
}))
vi.mock('openai', () => openaiMock)
vi.mock('@/providers', () => providersMock)
vi.mock('@/providers/models', () => providersModelsMock)
vi.mock('@/providers/attachments', () => providersAttachmentsMock)
vi.mock('@/providers/trace-enrichment', () => providersTraceEnrichmentMock)
vi.mock('@/providers/ollama-cloud/utils', () => ({
createReadableStreamFromOllamaCloudStream: (
_stream: unknown,
onComplete: (content: string, usage: StreamUsage) => void
) => {
streamOnComplete.current = onComplete
return new ReadableStream<Uint8Array>({
start(controller) {
controller.close()
},
})
},
}))
vi.mock('@/providers/utils', () => providersUtilsMock)
vi.mock('@/tools', () => toolsMock)
@@ -81,7 +63,6 @@ const baseRequest: ProviderRequest = {
describe('ollamaCloudProvider.executeRequest', () => {
beforeEach(() => {
streamOnComplete.current = undefined
mockCreate.mockResolvedValue(completion({ content: 'hello' }))
mockExecuteTool.mockResolvedValue({ success: true, output: { ok: true } })
})
-2
View File
@@ -4,7 +4,6 @@ import type { StreamingExecution } from '@/executor/types'
import { inheritConversationGenerationContext } from '@/providers/conversation-generation'
import { getProviderDefaultModel, getProviderModels } from '@/providers/models'
import { executeOllamaProviderRequest } from '@/providers/ollama/core'
import { createReadableStreamFromOllamaCloudStream } from '@/providers/ollama-cloud/utils'
import { openAICompatTransport } from '@/providers/transport'
import type { ProviderConfig, ProviderRequest, ProviderResponse } from '@/providers/types'
@@ -42,7 +41,6 @@ export const ollamaCloudProvider: ProviderConfig = {
apiKey,
baseURL: OLLAMA_CLOUD_BASE_URL,
}),
createStream: createReadableStreamFromOllamaCloudStream,
logger,
}
)
-23
View File
@@ -1,23 +0,0 @@
import type { ChatCompletionChunk } from 'openai/resources/chat/completions'
import type { CompletionUsage } from 'openai/resources/completions'
import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events'
import type { AgentStreamEvent } from '@/providers/stream-events'
import type { ProviderRequest } from '@/providers/types'
/**
* Creates an agent-events stream from an Ollama Cloud streaming response.
* Uses the shared OpenAI-compatible agent event streaming utility.
*/
export function createReadableStreamFromOllamaCloudStream(
ollamaStream: AsyncIterable<ChatCompletionChunk>,
onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void,
request?: ProviderRequest
): ReadableStream<AgentStreamEvent> {
return createOpenAICompatibleAgentEventStream(ollamaStream, {
request,
providerName: 'Ollama Cloud',
onComplete: onComplete
? (result) => onComplete(result.content, result.usage, result.thinking)
: undefined,
})
}
+7 -16
View File
@@ -2,11 +2,7 @@ import type { Logger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { isRecordLike } from '@sim/utils/object'
import OpenAI from 'openai'
import type {
ChatCompletionChunk,
ChatCompletionCreateParamsStreaming,
} from 'openai/resources/chat/completions'
import type { CompletionUsage } from 'openai/resources/completions'
import type { ChatCompletionCreateParamsStreaming } from 'openai/resources/chat/completions'
import type { StreamingExecution } from '@/executor/types'
import { MAX_TOOL_ITERATIONS } from '@/providers'
import { formatMessagesForProvider } from '@/providers/attachments'
@@ -20,8 +16,8 @@ import {
} from '@/providers/conversation-history'
import { createOpenAICompatAssistantHistory } from '@/providers/openai-compat/assistant-history'
import { getChatCompletionConversationUsage } from '@/providers/openai-compat/conversation-usage'
import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events'
import { executeProviderTool } from '@/providers/runtime-context'
import type { AgentStreamEvent } from '@/providers/stream-events'
import { createSettledAgentEventStream } from '@/providers/stream-events'
import { createStreamingExecution } from '@/providers/streaming-execution'
import { isAbortError, parseToolArguments } from '@/providers/streaming-tool-loop-shared'
@@ -67,11 +63,6 @@ export interface OllamaCoreConfig {
providerLabel: string
/** Builds the OpenAI-compatible client (base URL + credentials per provider). */
createClient: () => OpenAI
createStream: (
stream: AsyncIterable<ChatCompletionChunk>,
onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void,
request?: ProviderRequest
) => ReadableStream<AgentStreamEvent>
logger: Logger
}
@@ -199,9 +190,10 @@ export async function executeOllamaProviderRequest(
initialCost: { input: 0, output: 0, total: 0 },
streamFormat: 'agent-events-v1',
createStream: ({ output, finalizeTiming }) =>
config.createStream(
streamResponse,
(content, usage) => {
createOpenAICompatibleAgentEventStream(streamResponse, {
providerName: providerLabel,
request,
onComplete: ({ content, usage }) => {
output.content = content
if (content && request.responseFormat) {
@@ -227,8 +219,7 @@ export async function executeOllamaProviderRequest(
finalizeTiming()
},
request
),
}),
})
return streamingResult
+29 -30
View File
@@ -8,30 +8,12 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
type StreamUsage = { prompt_tokens: number; completion_tokens: number; total_tokens: number }
const { streamOnComplete } = vi.hoisted(() => ({
streamOnComplete: {
current: undefined as undefined | ((content: string, usage: StreamUsage) => void),
},
}))
vi.mock('openai', () => openaiMock)
vi.mock('@/providers', () => providersMock)
vi.mock('@/providers/attachments', () => providersAttachmentsMock)
vi.mock('@/providers/trace-enrichment', () => providersTraceEnrichmentMock)
vi.mock('@/providers/ollama/utils', () => ({
createReadableStreamFromOllamaStream: (
_stream: unknown,
onComplete: (content: string, usage: StreamUsage) => void
) => {
streamOnComplete.current = onComplete
return new ReadableStream<Uint8Array>({
start(controller) {
controller.close()
},
})
},
}))
vi.mock('@/providers/utils', () => providersUtilsMock)
vi.mock('@/tools', () => toolsMock)
vi.mock('@/stores/providers', () => ({
@@ -114,7 +96,6 @@ const baseRequest: ProviderRequest = {
describe('ollamaProvider.executeRequest', () => {
beforeEach(() => {
streamOnComplete.current = undefined
mockCreate.mockResolvedValue(completion({ content: 'hello' }))
mockExecuteTool.mockResolvedValue({ success: true, output: { ok: true } })
})
@@ -361,6 +342,19 @@ describe('ollamaProvider.executeRequest', () => {
})
it('streams content and usage when no tools are used', async () => {
mockCreate.mockResolvedValueOnce(
(async function* () {
yield {
choices: [{ delta: { content: 'streamed text' } }],
usage: {
prompt_tokens: 4,
completion_tokens: 6,
total_tokens: 10,
},
}
})()
)
const result = (await ollamaProvider.executeRequest({
...baseRequest,
stream: true,
@@ -369,27 +363,32 @@ describe('ollamaProvider.executeRequest', () => {
expect(result.stream).toBeInstanceOf(ReadableStream)
expect(mockCreate.mock.calls[0][0].stream_options).toEqual({ include_usage: true })
streamOnComplete.current?.('streamed text', {
prompt_tokens: 4,
completion_tokens: 6,
total_tokens: 10,
})
await readAgentEvents(result.stream as ReadableStream<AgentStreamEvent>)
expect(result.execution.output.content).toBe('streamed text')
expect(result.execution.output.tokens).toMatchObject({ input: 4, output: 6, total: 10 })
})
it('strips ```json fences from streamed content when responseFormat is set', async () => {
mockCreate.mockResolvedValueOnce(
(async function* () {
yield {
choices: [{ delta: { content: '```json\n{"a":1}\n```' } }],
usage: {
prompt_tokens: 1,
completion_tokens: 2,
total_tokens: 3,
},
}
})()
)
const result = (await ollamaProvider.executeRequest({
...baseRequest,
stream: true,
responseFormat: { name: 'r', schema: { type: 'object' }, strict: true },
})) as unknown as StreamingResult
streamOnComplete.current?.('```json\n{"a":1}\n```', {
prompt_tokens: 1,
completion_tokens: 2,
total_tokens: 3,
})
await readAgentEvents(result.stream as ReadableStream<AgentStreamEvent>)
expect(result.execution.output.content).toBe('{"a":1}')
})
-2
View File
@@ -6,7 +6,6 @@ import type { StreamingExecution } from '@/executor/types'
import { inheritConversationGenerationContext } from '@/providers/conversation-generation'
import { executeOllamaProviderRequest } from '@/providers/ollama/core'
import type { ModelsObject } from '@/providers/ollama/types'
import { createReadableStreamFromOllamaStream } from '@/providers/ollama/utils'
import { openAICompatTransport } from '@/providers/transport'
import type { ProviderConfig, ProviderRequest, ProviderResponse } from '@/providers/types'
import { useProvidersStore } from '@/stores/providers'
@@ -63,7 +62,6 @@ export const ollamaProvider: ProviderConfig = {
apiKey: 'empty',
baseURL: `${OLLAMA_HOST}/v1`,
}),
createStream: createReadableStreamFromOllamaStream,
logger,
}
)
-23
View File
@@ -1,23 +0,0 @@
import type { ChatCompletionChunk } from 'openai/resources/chat/completions'
import type { CompletionUsage } from 'openai/resources/completions'
import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events'
import type { AgentStreamEvent } from '@/providers/stream-events'
import type { ProviderRequest } from '@/providers/types'
/**
* Creates an agent-events stream from an Ollama streaming response.
* Uses the shared OpenAI-compatible agent event streaming utility.
*/
export function createReadableStreamFromOllamaStream(
ollamaStream: AsyncIterable<ChatCompletionChunk>,
onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void,
request?: ProviderRequest
): ReadableStream<AgentStreamEvent> {
return createOpenAICompatibleAgentEventStream(ollamaStream, {
request,
providerName: 'Ollama',
onComplete: onComplete
? (result) => onComplete(result.content, result.usage, result.thinking)
: undefined,
})
}
@@ -0,0 +1,717 @@
import type { Logger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { isRecordLike } from '@sim/utils/object'
import type OpenAI from 'openai'
import type {
ChatCompletionCreateParamsNonStreaming,
ChatCompletionCreateParamsStreaming,
ChatCompletionMessage,
ChatCompletionMessageParam,
ChatCompletionToolChoiceOption,
} from 'openai/resources/chat/completions'
import type { StreamingExecution } from '@/executor/types'
import { MAX_TOOL_ITERATIONS } from '@/providers'
import { formatMessagesForProvider } from '@/providers/attachments'
import {
isConversationContextError,
prepareConversationGeneration,
} from '@/providers/conversation-generation'
import {
captureProviderConversationStep,
recordProviderConversationToolError,
recordProviderConversationUsage,
} from '@/providers/conversation-history'
import {
createOpenAICompatAssistantHistory,
type OpenAICompatReasoningField,
} from '@/providers/openai-compat/assistant-history'
import { getChatCompletionConversationUsage } from '@/providers/openai-compat/conversation-usage'
import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events'
import { executeProviderTool } from '@/providers/runtime-context'
import { createSettledAgentEventStream } from '@/providers/stream-events'
import { createStreamingExecution } from '@/providers/streaming-execution'
import { isAbortError, parseToolArguments } from '@/providers/streaming-tool-loop-shared'
import { adaptOpenAIChatToolSchema } from '@/providers/tool-schema-adapter'
import { enrichLastModelSegmentFromChatCompletions } from '@/providers/trace-enrichment'
import type {
FunctionCallResponse,
Message,
ProviderRequest,
ProviderResponse,
TimeSegment,
} from '@/providers/types'
import { ProviderError } from '@/providers/types'
import {
calculateCost,
checkForForcedToolUsageOpenAI,
isFunctionToolCall,
prepareToolExecution,
prepareToolsWithUsageControl,
sumToolCosts,
} from '@/providers/utils'
export type ChatCompletionPayload = ChatCompletionCreateParamsNonStreaming & {
provider?: { require_parameters?: boolean }
}
interface ChatCompletionExecutionConfig {
providerId: 'baseten' | 'together' | 'fireworks' | 'openrouter'
providerName: string
client: OpenAI
requestedModel: string
/** Fireworks sends a wire model but retains its catalog identity for billing and output. */
reportedModel: string
logger: Logger
applyResponseFormat: (
payload: ChatCompletionPayload,
messages: ChatCompletionMessageParam[],
responseFormat: NonNullable<ProviderRequest['responseFormat']>,
model: string
) => Promise<ChatCompletionMessageParam[]>
reasoningFields: readonly OpenAICompatReasoningField[]
preserveReasoningDetails?: boolean
/** Together and OpenRouter account for the pending turn when their tool loop reaches its cap. */
recordPendingUsage?: boolean
}
/** Runs the shared Chat Completions request, tool loop and settled-stream lifecycle. */
export async function executeChatCompletionRequest(
request: ProviderRequest,
config: ChatCompletionExecutionConfig
): Promise<ProviderResponse | StreamingExecution> {
const {
client,
requestedModel,
reportedModel,
logger,
applyResponseFormat,
providerId,
providerName,
} = config
const allMessages: Message[] = []
if (request.systemPrompt) {
allMessages.push({ role: 'system', content: request.systemPrompt })
}
if (request.context) {
allMessages.push({ role: 'user', content: request.context })
}
if (request.messages) {
allMessages.push(...request.messages)
}
const formattedMessages = formatMessagesForProvider(
allMessages,
providerId
) as ChatCompletionMessageParam[]
const tools = request.tools?.length
? request.tools.map((tool) => adaptOpenAIChatToolSchema(tool))
: undefined
const payload: ChatCompletionPayload = {
model: requestedModel,
messages: formattedMessages,
}
if (request.temperature !== undefined) payload.temperature = request.temperature
if (request.maxTokens != null) payload.max_tokens = request.maxTokens
let preparedTools: ReturnType<typeof prepareToolsWithUsageControl> | null = null
let hasActiveTools = false
if (tools?.length) {
preparedTools = prepareToolsWithUsageControl(tools, request.tools, logger, providerId)
const { tools: filteredTools, toolChoice } = preparedTools
if (filteredTools?.length && toolChoice) {
payload.tools = filteredTools
payload.tool_choice = toolChoice as ChatCompletionToolChoiceOption
hasActiveTools = true
}
}
const providerStartTime = Date.now()
const providerStartTimeISO = new Date(providerStartTime).toISOString()
try {
if (request.responseFormat && !hasActiveTools) {
payload.messages = await applyResponseFormat(
payload,
payload.messages,
request.responseFormat,
requestedModel
)
}
if (request.stream && (!tools || tools.length === 0 || !hasActiveTools)) {
const streamingParams: ChatCompletionCreateParamsStreaming = {
...payload,
stream: true,
stream_options: { include_usage: true },
}
const streamResponse = await client.chat.completions.create(
await prepareConversationGeneration(request, 'chat-completions', streamingParams),
request.abortSignal ? { signal: request.abortSignal } : undefined
)
const streamingResult = createStreamingExecution({
model: reportedModel,
providerStartTime,
providerStartTimeISO,
timing: { kind: 'simple', segmentName: request.model },
initialTokens: { input: 0, output: 0, total: 0 },
initialCost: { input: 0, output: 0, total: 0 },
streamFormat: 'agent-events-v1',
createStream: ({ output, finalizeTiming }) =>
createOpenAICompatibleAgentEventStream(streamResponse, {
request,
providerName,
onComplete: ({ content, usage }) => {
output.content = content
output.tokens = {
input: usage.prompt_tokens,
output: usage.completion_tokens,
total: usage.total_tokens,
}
const costResult = calculateCost(
reportedModel,
usage.prompt_tokens,
usage.completion_tokens
)
output.cost = {
input: costResult.input,
output: costResult.output,
total: costResult.total,
}
finalizeTiming()
},
}),
})
return streamingResult
}
const initialCallTime = Date.now()
const originalToolChoice = payload.tool_choice
const forcedTools = preparedTools?.forcedTools || []
let usedForcedTools: string[] = []
let currentResponse = await client.chat.completions.create(
await prepareConversationGeneration(request, 'chat-completions', payload),
request.abortSignal ? { signal: request.abortSignal } : undefined
)
if (!currentResponse.choices[0]?.message?.tool_calls?.length) {
await captureProviderConversationStep(
request,
'chat-completions',
currentResponse.choices[0]?.message,
getChatCompletionConversationUsage(currentResponse.usage)
)
}
const firstResponseTime = Date.now() - initialCallTime
let content = currentResponse.choices[0]?.message?.content || ''
const tokens = {
input: currentResponse.usage?.prompt_tokens || 0,
output: currentResponse.usage?.completion_tokens || 0,
total: currentResponse.usage?.total_tokens || 0,
}
const toolCalls: FunctionCallResponse[] = []
const toolResults: Record<string, unknown>[] = []
const currentMessages = [...formattedMessages]
let iterationCount = 0
let modelTime = firstResponseTime
let toolsTime = 0
let hasUsedForcedTool = false
const timeSegments: TimeSegment[] = [
{
type: 'model',
name: request.model,
startTime: initialCallTime,
endTime: initialCallTime + firstResponseTime,
duration: firstResponseTime,
},
]
const forcedToolResult = checkForForcedToolUsageOpenAI(
currentResponse,
originalToolChoice ?? 'auto',
providerName,
forcedTools,
usedForcedTools
)
hasUsedForcedTool = forcedToolResult.hasUsedForcedTool
usedForcedTools = forcedToolResult.usedForcedTools
while (iterationCount < MAX_TOOL_ITERATIONS) {
if (currentResponse.choices[0]?.message?.content) {
content = currentResponse.choices[0].message.content
}
const toolCallsInResponse =
currentResponse.choices[0]?.message?.tool_calls?.filter(isFunctionToolCall)
enrichLastModelSegmentFromChatCompletions(
timeSegments,
currentResponse,
toolCallsInResponse,
{ model: request.model, provider: providerId }
)
if (!toolCallsInResponse || toolCallsInResponse.length === 0) {
break
}
const toolsStartTime = Date.now()
await captureProviderConversationStep(
request,
'chat-completions',
currentResponse.choices[0]?.message,
getChatCompletionConversationUsage(currentResponse.usage)
)
const toolExecutionPromises = toolCallsInResponse.map(async (toolCall) => {
const toolCallStartTime = Date.now()
const toolName = toolCall.function.name
try {
const toolArgs = parseToolArguments(toolCall.function.arguments, toolName)
const tool = request.tools?.find((t) => t.id === toolName)
if (!tool) {
await recordProviderConversationToolError(
request,
toolCall.id,
toolName,
`Tool "${toolName}" is not available`
)
const toolCallEndTime = Date.now()
return {
toolCall,
toolName,
toolParams: {},
result: {
success: false,
output: undefined,
error: `Tool "${toolName}" is not available`,
},
startTime: toolCallStartTime,
endTime: toolCallEndTime,
duration: toolCallEndTime - toolCallStartTime,
}
}
const { toolParams, executionParams } = prepareToolExecution(
tool,
toolArgs,
request,
toolCall.id
)
const { rawResponse, modelResponse } = await executeProviderTool(
toolName,
executionParams,
{
signal: request.abortSignal,
}
)
const toolCallEndTime = Date.now()
return {
toolCall,
toolName,
toolParams,
result: rawResponse,
modelResult: modelResponse,
startTime: toolCallStartTime,
endTime: toolCallEndTime,
duration: toolCallEndTime - toolCallStartTime,
}
} catch (error) {
if (isAbortError(error) || request.abortSignal?.aborted) {
throw error
}
await recordProviderConversationToolError(
request,
toolCall.id,
toolName,
getErrorMessage(error, 'Tool execution failed')
)
const toolCallEndTime = Date.now()
logger.error(`Error processing tool call (${providerName}):`, {
error: toError(error).message,
toolName,
})
return {
toolCall,
toolName,
toolParams: {},
result: {
success: false,
output: undefined,
error: getErrorMessage(error, 'Tool execution failed'),
},
startTime: toolCallStartTime,
endTime: toolCallEndTime,
duration: toolCallEndTime - toolCallStartTime,
}
}
})
const executionResults = await Promise.all(toolExecutionPromises)
const assistantMessage = currentResponse.choices[0]?.message
if (assistantMessage) {
const assistantHistory = createOpenAICompatAssistantHistory({
message: assistantMessage,
toolCalls: toolCallsInResponse,
reasoningFields: config.reasoningFields,
})
const extendedMessage = assistantMessage as ChatCompletionMessage & {
reasoning_details?: unknown[]
}
if (config.preserveReasoningDetails && Array.isArray(extendedMessage.reasoning_details)) {
assistantHistory.reasoning_details = extendedMessage.reasoning_details
}
currentMessages.push(assistantHistory)
}
for (const executionResult of executionResults) {
const { toolCall, toolName, toolParams, result, startTime, endTime, duration } =
executionResult
const modelResult =
'modelResult' in executionResult ? (executionResult.modelResult ?? result) : result
timeSegments.push({
type: 'tool',
name: toolName,
startTime: startTime,
endTime: endTime,
duration: duration,
toolCallId: toolCall.id,
})
let resultContent: unknown
if (result.success) {
if (isRecordLike(result.output)) {
toolResults.push(result.output)
}
resultContent = result.output ?? null
} else {
resultContent = {
error: true,
message: result.error || 'Tool execution failed',
tool: toolName,
}
}
const modelResultContent = modelResult.success
? (modelResult.output ?? null)
: {
error: true,
message: modelResult.error || 'Tool execution failed',
tool: toolName,
}
toolCalls.push({
name: toolName,
arguments: toolParams,
startTime: new Date(startTime).toISOString(),
endTime: new Date(endTime).toISOString(),
duration: duration,
result: resultContent,
success: result.success,
})
currentMessages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify(modelResultContent),
})
}
const thisToolsTime = Date.now() - toolsStartTime
toolsTime += thisToolsTime
const nextPayload = {
...payload,
messages: currentMessages,
}
if (typeof originalToolChoice === 'object' && hasUsedForcedTool && forcedTools.length > 0) {
const remainingTools = forcedTools.filter((tool) => !usedForcedTools.includes(tool))
if (remainingTools.length > 0) {
nextPayload.tool_choice = { type: 'function', function: { name: remainingTools[0] } }
} else {
nextPayload.tool_choice = 'auto'
}
}
const nextModelStartTime = Date.now()
currentResponse = await client.chat.completions.create(
await prepareConversationGeneration(request, 'chat-completions', nextPayload),
request.abortSignal ? { signal: request.abortSignal } : undefined
)
if (!currentResponse.choices[0]?.message?.tool_calls?.length) {
await captureProviderConversationStep(
request,
'chat-completions',
currentResponse.choices[0]?.message,
getChatCompletionConversationUsage(currentResponse.usage)
)
}
const nextForcedToolResult = checkForForcedToolUsageOpenAI(
currentResponse,
nextPayload.tool_choice ?? 'auto',
providerName,
forcedTools,
usedForcedTools
)
hasUsedForcedTool = nextForcedToolResult.hasUsedForcedTool
usedForcedTools = nextForcedToolResult.usedForcedTools
const nextModelEndTime = Date.now()
const thisModelTime = nextModelEndTime - nextModelStartTime
timeSegments.push({
type: 'model',
name: request.model,
startTime: nextModelStartTime,
endTime: nextModelEndTime,
duration: thisModelTime,
})
modelTime += thisModelTime
if (currentResponse.choices[0]?.message?.content) {
content = currentResponse.choices[0].message.content
}
if (currentResponse.usage) {
tokens.input += currentResponse.usage.prompt_tokens || 0
tokens.output += currentResponse.usage.completion_tokens || 0
tokens.total += currentResponse.usage.total_tokens || 0
}
iterationCount++
}
if (iterationCount === MAX_TOOL_ITERATIONS) {
if (config.recordPendingUsage && currentResponse.choices[0]?.message?.tool_calls?.length) {
await recordProviderConversationUsage(
request,
getChatCompletionConversationUsage(currentResponse.usage)
)
}
const pendingToolCalls =
currentResponse.choices[0]?.message?.tool_calls?.filter(isFunctionToolCall)
enrichLastModelSegmentFromChatCompletions(timeSegments, currentResponse, pendingToolCalls, {
model: request.model,
provider: providerId,
})
if (pendingToolCalls?.length && !(request.responseFormat && hasActiveTools)) {
const finalPayload: ChatCompletionPayload = {
...payload,
messages: [...currentMessages],
tool_choice: 'none',
}
if (request.responseFormat) {
finalPayload.messages = await applyResponseFormat(
finalPayload,
finalPayload.messages,
request.responseFormat,
requestedModel
)
}
const finalStartTime = Date.now()
const finalResponse = await client.chat.completions.create(
await prepareConversationGeneration(request, 'chat-completions', finalPayload),
request.abortSignal ? { signal: request.abortSignal } : undefined
)
if (!finalResponse.choices[0]?.message?.tool_calls?.length) {
await captureProviderConversationStep(
request,
'chat-completions',
finalResponse.choices[0]?.message,
getChatCompletionConversationUsage(finalResponse.usage)
)
}
const finalEndTime = Date.now()
const finalDuration = finalEndTime - finalStartTime
timeSegments.push({
type: 'model',
name: 'Final answer after tool iteration limit',
startTime: finalStartTime,
endTime: finalEndTime,
duration: finalDuration,
})
modelTime += finalDuration
if (finalResponse.choices[0]?.message?.content) {
content = finalResponse.choices[0].message.content
}
if (finalResponse.usage) {
tokens.input += finalResponse.usage.prompt_tokens || 0
tokens.output += finalResponse.usage.completion_tokens || 0
tokens.total += finalResponse.usage.total_tokens || 0
}
enrichLastModelSegmentFromChatCompletions(
timeSegments,
finalResponse,
finalResponse.choices[0]?.message?.tool_calls?.filter(isFunctionToolCall),
{ model: request.model, provider: providerId }
)
}
}
if (request.responseFormat && hasActiveTools) {
const finalPayload: ChatCompletionPayload = {
model: payload.model,
messages: [...currentMessages],
}
if (payload.temperature !== undefined) {
finalPayload.temperature = payload.temperature
}
if (payload.max_tokens !== undefined) {
finalPayload.max_tokens = payload.max_tokens
}
finalPayload.messages = await applyResponseFormat(
finalPayload,
finalPayload.messages,
request.responseFormat,
requestedModel
)
const finalStartTime = Date.now()
const finalResponse = await client.chat.completions.create(
await prepareConversationGeneration(request, 'chat-completions', finalPayload),
request.abortSignal ? { signal: request.abortSignal } : undefined
)
if (!finalResponse.choices[0]?.message?.tool_calls?.length) {
await captureProviderConversationStep(
request,
'chat-completions',
finalResponse.choices[0]?.message,
getChatCompletionConversationUsage(finalResponse.usage)
)
}
const finalEndTime = Date.now()
const finalDuration = finalEndTime - finalStartTime
timeSegments.push({
type: 'model',
name: 'Final structured response',
startTime: finalStartTime,
endTime: finalEndTime,
duration: finalDuration,
})
modelTime += finalDuration
if (finalResponse.choices[0]?.message?.content) {
content = finalResponse.choices[0].message.content
}
if (finalResponse.usage) {
tokens.input += finalResponse.usage.prompt_tokens || 0
tokens.output += finalResponse.usage.completion_tokens || 0
tokens.total += finalResponse.usage.total_tokens || 0
}
enrichLastModelSegmentFromChatCompletions(
timeSegments,
finalResponse,
finalResponse.choices[0]?.message?.tool_calls?.filter(isFunctionToolCall),
{ model: request.model, provider: providerId }
)
}
if (request.stream) {
const accumulatedCost = calculateCost(reportedModel, tokens.input, tokens.output)
const toolCost = sumToolCosts(toolResults)
const finalCost = {
input: accumulatedCost.input,
output: accumulatedCost.output,
toolCost: toolCost || undefined,
total: accumulatedCost.total + toolCost,
}
const streamingResult = createStreamingExecution({
model: reportedModel,
providerStartTime,
providerStartTimeISO,
timing: {
kind: 'accumulated',
modelTime,
toolsTime,
firstResponseTime,
iterations: timeSegments.filter((segment) => segment.type === 'model').length,
timeSegments,
},
initialTokens: { input: tokens.input, output: tokens.output, total: tokens.total },
initialCost: finalCost,
toolCalls: toolCalls.length > 0 ? { list: toolCalls, count: toolCalls.length } : undefined,
streamFormat: 'agent-events-v1',
createStream: ({ output, finalizeTiming }) => {
output.content = content
output.tokens = { input: tokens.input, output: tokens.output, total: tokens.total }
output.cost = finalCost
finalizeTiming()
return createSettledAgentEventStream(content)
},
})
return streamingResult
}
const providerEndTime = Date.now()
const providerEndTimeISO = new Date(providerEndTime).toISOString()
const totalDuration = providerEndTime - providerStartTime
return {
content,
model: reportedModel,
tokens,
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
toolResults: toolResults.length > 0 ? toolResults : undefined,
timing: {
startTime: providerStartTimeISO,
endTime: providerEndTimeISO,
duration: totalDuration,
modelTime: modelTime,
toolsTime: toolsTime,
firstResponseTime: firstResponseTime,
iterations: timeSegments.filter((segment) => segment.type === 'model').length,
timeSegments: timeSegments,
},
}
} catch (error) {
const providerEndTime = Date.now()
const providerEndTimeISO = new Date(providerEndTime).toISOString()
const totalDuration = providerEndTime - providerStartTime
const errorDetails: Record<string, unknown> = {
error: toError(error).message,
duration: totalDuration,
}
if (isRecordLike(error)) {
const err = error
if (err.status) errorDetails.status = err.status
if (err.code) errorDetails.code = err.code
if (err.type) errorDetails.type = err.type
if (isRecordLike(err.error)) {
if (err.error.message) errorDetails.providerMessage = err.error.message
if (err.error.metadata) errorDetails.metadata = err.error.metadata
}
}
logger.error(`Error in ${providerName} request:`, errorDetails)
if (isAbortError(error) || request.abortSignal?.aborted || isConversationContextError(error)) {
throw error
}
throw new ProviderError(toError(error).message, {
startTime: providerStartTimeISO,
endTime: providerEndTimeISO,
duration: totalDuration,
})
}
}
+6 -15
View File
@@ -14,14 +14,10 @@ import { providersUtilsMock, providersUtilsMockFns } from '@sim/testing/mocks/pr
import { toolsMock, toolsMockFns } from '@sim/testing/mocks/tools.mock'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { mockCapabilities, mockSupportsNative, mockCheckForced, mockCreateStream } = vi.hoisted(
() => ({
mockCapabilities: vi.fn(),
mockSupportsNative: vi.fn(),
mockCheckForced: vi.fn(() => ({ hasUsedForcedTool: false, usedForcedTools: [] })),
mockCreateStream: vi.fn(),
})
)
const { mockCapabilities, mockSupportsNative } = vi.hoisted(() => ({
mockCapabilities: vi.fn(),
mockSupportsNative: vi.fn(),
}))
vi.mock('openai', () => openaiMock)
@@ -38,8 +34,6 @@ vi.mock('@/providers/attachments', () => providersAttachmentsMock)
vi.mock('@/providers/openrouter/utils', () => ({
supportsNativeStructuredOutputs: mockSupportsNative,
getOpenRouterModelCapabilities: mockCapabilities,
createReadableStreamFromOpenAIStream: mockCreateStream,
checkForForcedToolUsage: mockCheckForced,
}))
vi.mock('@/providers/trace-enrichment', () => providersTraceEnrichmentMock)
@@ -51,6 +45,7 @@ import { openRouterProvider } from '@/providers/openrouter/index'
import type { OpenRouterReasoningDetail } from '@/providers/openrouter/reasoning'
import type { ProviderRequest, ProviderResponse, ProviderToolConfig } from '@/providers/types'
const mockCheckForced = providersUtilsMockFns.mockCheckForForcedToolUsageOpenAI
const mockCreate = openaiMockFns.mockChatCompletionsCreate
providersMock.MAX_TOOL_ITERATIONS = 10
providersModelsMockFns.mockGetMaxOutputTokensForModel.mockReturnValue(100)
@@ -137,9 +132,6 @@ describe('openRouterProvider.executeRequest', () => {
mockCreate.mockReset()
mockExecuteTool.mockReset()
mockSupportsNative.mockResolvedValue(false)
mockCreateStream.mockReturnValue(
new ReadableStream({ start: (controller) => controller.close() })
)
})
it.each([
@@ -479,14 +471,13 @@ describe('openRouterProvider.executeRequest', () => {
})
it('streams directly when there are no tools and sends usage opt-in', async () => {
mockCreate.mockResolvedValueOnce({})
mockCreate.mockResolvedValueOnce((async function* () {})())
const res = await openRouterProvider.executeRequest({ ...baseRequest, stream: true })
const payload = mockCreate.mock.calls[0][0]
expect(payload.stream).toBe(true)
expect(payload.stream_options).toEqual({ include_usage: true })
expect(mockCreateStream).toHaveBeenCalledTimes(1)
expect(res).toHaveProperty('stream')
expect(res).toHaveProperty('execution.output.model', 'anthropic/claude-3.5-sonnet')
})
+24 -684
View File
@@ -1,81 +1,34 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { isRecordLike } from '@sim/utils/object'
import OpenAI from 'openai'
import type {
ChatCompletionCreateParamsStreaming,
ChatCompletionMessage,
} from 'openai/resources/chat/completions'
import type { ChatCompletionMessageParam } from 'openai/resources/chat/completions'
import type { StreamingExecution } from '@/executor/types'
import { MAX_TOOL_ITERATIONS } from '@/providers'
import { formatMessagesForProvider } from '@/providers/attachments'
import {
bindConversationGenerationContextWindow,
isConversationContextError,
prepareConversationGeneration,
} from '@/providers/conversation-generation'
import {
captureProviderConversationStep,
getConversationRequestContext,
recordProviderConversationToolError,
recordProviderConversationUsage,
} from '@/providers/conversation-history'
import { bindConversationGenerationContextWindow } from '@/providers/conversation-generation'
import { getConversationRequestContext } from '@/providers/conversation-history'
import { getProviderDefaultModel, getProviderModels } from '@/providers/models'
import {
createOpenAICompatAssistantHistory,
type OpenAICompatAssistantHistoryMessage,
} from '@/providers/openai-compat/assistant-history'
import { getChatCompletionConversationUsage } from '@/providers/openai-compat/conversation-usage'
import type { OpenRouterReasoningDetail } from '@/providers/openrouter/reasoning'
type ChatCompletionPayload,
executeChatCompletionRequest,
} from '@/providers/openai-compat/chat-completions'
import {
checkForForcedToolUsage,
createReadableStreamFromOpenAIStream,
getOpenRouterModelCapabilities,
supportsNativeStructuredOutputs,
} from '@/providers/openrouter/utils'
import { executeProviderTool } from '@/providers/runtime-context'
import { createSettledAgentEventStream } from '@/providers/stream-events'
import { createStreamingExecution } from '@/providers/streaming-execution'
import { isAbortError, parseToolArguments } from '@/providers/streaming-tool-loop-shared'
import { adaptOpenAIChatToolSchema } from '@/providers/tool-schema-adapter'
import { enrichLastModelSegmentFromChatCompletions } from '@/providers/trace-enrichment'
import { openAICompatTransport } from '@/providers/transport'
import type {
FunctionCallResponse,
Message,
ProviderConfig,
ProviderRequest,
ProviderResponse,
TimeSegment,
} from '@/providers/types'
import { ProviderError } from '@/providers/types'
import {
calculateCost,
generateSchemaInstructions,
isFunctionToolCall,
prepareToolExecution,
prepareToolsWithUsageControl,
sumToolCosts,
} from '@/providers/utils'
import type { ProviderConfig, ProviderRequest, ProviderResponse } from '@/providers/types'
import { generateSchemaInstructions } from '@/providers/utils'
const logger = createLogger('OpenRouterProvider')
type OpenRouterAssistantMessage = ChatCompletionMessage & {
reasoning?: string
reasoning_content?: string
reasoning_details?: OpenRouterReasoningDetail[]
}
/**
* Applies structured output configuration to a payload based on model capabilities.
* Uses json_schema with require_parameters for supported models, falls back to json_object with prompt instructions.
*/
async function applyResponseFormat(
targetPayload: any,
messages: any[],
responseFormat: any,
targetPayload: ChatCompletionPayload,
messages: ChatCompletionMessageParam[],
responseFormat: NonNullable<ProviderRequest['responseFormat']>,
model: string
): Promise<any[]> {
): Promise<ChatCompletionMessageParam[]> {
const useNative = await supportsNativeStructuredOutputs(model)
if (useNative) {
@@ -138,630 +91,17 @@ export const openRouterProvider: ProviderConfig = {
stream: !!request.stream,
})
const allMessages: Message[] = []
if (request.systemPrompt) {
allMessages.push({ role: 'system', content: request.systemPrompt })
}
if (request.context) {
allMessages.push({ role: 'user', content: request.context })
}
if (request.messages) {
allMessages.push(...request.messages)
}
const formattedMessages = formatMessagesForProvider(allMessages, 'openrouter') as Message[]
const tools = request.tools?.length
? request.tools.map((tool) => adaptOpenAIChatToolSchema(tool))
: undefined
const payload: any = {
model: requestedModel,
messages: formattedMessages,
}
if (request.temperature !== undefined) payload.temperature = request.temperature
if (request.maxTokens != null) payload.max_tokens = request.maxTokens
let preparedTools: ReturnType<typeof prepareToolsWithUsageControl> | null = null
let hasActiveTools = false
if (tools?.length) {
preparedTools = prepareToolsWithUsageControl(tools, request.tools, logger, 'openrouter')
const { tools: filteredTools, toolChoice } = preparedTools
if (filteredTools?.length && toolChoice) {
payload.tools = filteredTools
payload.tool_choice = toolChoice
hasActiveTools = true
}
}
const providerStartTime = Date.now()
const providerStartTimeISO = new Date(providerStartTime).toISOString()
try {
if (request.responseFormat && !hasActiveTools) {
payload.messages = await applyResponseFormat(
payload,
payload.messages,
request.responseFormat,
requestedModel
)
}
if (request.stream && (!tools || tools.length === 0 || !hasActiveTools)) {
const streamingParams: ChatCompletionCreateParamsStreaming = {
...payload,
stream: true,
stream_options: { include_usage: true },
}
const streamResponse = await client.chat.completions.create(
await prepareConversationGeneration(request, 'chat-completions', streamingParams),
request.abortSignal ? { signal: request.abortSignal } : undefined
)
const streamingResult = createStreamingExecution({
model: requestedModel,
providerStartTime,
providerStartTimeISO,
timing: { kind: 'simple', segmentName: request.model },
initialTokens: { input: 0, output: 0, total: 0 },
initialCost: { input: 0, output: 0, total: 0 },
streamFormat: 'agent-events-v1',
createStream: ({ output, finalizeTiming }) =>
createReadableStreamFromOpenAIStream(
streamResponse,
(content, usage) => {
output.content = content
output.tokens = {
input: usage.prompt_tokens,
output: usage.completion_tokens,
total: usage.total_tokens,
}
const costResult = calculateCost(
requestedModel,
usage.prompt_tokens,
usage.completion_tokens
)
output.cost = {
input: costResult.input,
output: costResult.output,
total: costResult.total,
}
finalizeTiming()
},
request
),
})
return streamingResult
}
const initialCallTime = Date.now()
const originalToolChoice = payload.tool_choice
const forcedTools = preparedTools?.forcedTools || []
let usedForcedTools: string[] = []
let currentResponse = await client.chat.completions.create(
await prepareConversationGeneration(request, 'chat-completions', payload),
request.abortSignal ? { signal: request.abortSignal } : undefined
)
if (!currentResponse.choices[0]?.message?.tool_calls?.length) {
await captureProviderConversationStep(
request,
'chat-completions',
currentResponse.choices[0]?.message,
getChatCompletionConversationUsage(currentResponse.usage)
)
}
const firstResponseTime = Date.now() - initialCallTime
let content = currentResponse.choices[0]?.message?.content || ''
const tokens = {
input: currentResponse.usage?.prompt_tokens || 0,
output: currentResponse.usage?.completion_tokens || 0,
total: currentResponse.usage?.total_tokens || 0,
}
const toolCalls: FunctionCallResponse[] = []
const toolResults: Record<string, unknown>[] = []
const currentMessages = [...formattedMessages]
let iterationCount = 0
let modelTime = firstResponseTime
let toolsTime = 0
let hasUsedForcedTool = false
const timeSegments: TimeSegment[] = [
{
type: 'model',
name: request.model,
startTime: initialCallTime,
endTime: initialCallTime + firstResponseTime,
duration: firstResponseTime,
},
]
const forcedToolResult = checkForForcedToolUsage(
currentResponse,
originalToolChoice,
forcedTools,
usedForcedTools
)
hasUsedForcedTool = forcedToolResult.hasUsedForcedTool
usedForcedTools = forcedToolResult.usedForcedTools
while (iterationCount < MAX_TOOL_ITERATIONS) {
if (currentResponse.choices[0]?.message?.content) {
content = currentResponse.choices[0].message.content
}
const toolCallsInResponse =
currentResponse.choices[0]?.message?.tool_calls?.filter(isFunctionToolCall)
enrichLastModelSegmentFromChatCompletions(
timeSegments,
currentResponse,
toolCallsInResponse,
{ model: request.model, provider: 'openrouter' }
)
if (!toolCallsInResponse || toolCallsInResponse.length === 0) {
break
}
const toolsStartTime = Date.now()
await captureProviderConversationStep(
request,
'chat-completions',
currentResponse.choices[0]?.message,
getChatCompletionConversationUsage(currentResponse.usage)
)
const toolExecutionPromises = toolCallsInResponse.map(async (toolCall) => {
const toolCallStartTime = Date.now()
const toolName = toolCall.function.name
try {
const toolArgs = parseToolArguments(toolCall.function.arguments, toolName)
const tool = request.tools?.find((t) => t.id === toolName)
if (!tool) {
await recordProviderConversationToolError(
request,
toolCall.id,
toolName,
`Tool "${toolName}" is not available`
)
const toolCallEndTime = Date.now()
return {
toolCall,
toolName,
toolParams: {},
result: {
success: false,
output: undefined,
error: `Tool "${toolName}" is not available`,
},
startTime: toolCallStartTime,
endTime: toolCallEndTime,
duration: toolCallEndTime - toolCallStartTime,
}
}
const { toolParams, executionParams } = prepareToolExecution(
tool,
toolArgs,
request,
toolCall.id
)
const { rawResponse, modelResponse } = await executeProviderTool(
toolName,
executionParams,
{
signal: request.abortSignal,
}
)
const toolCallEndTime = Date.now()
return {
toolCall,
toolName,
toolParams,
result: rawResponse,
modelResult: modelResponse,
startTime: toolCallStartTime,
endTime: toolCallEndTime,
duration: toolCallEndTime - toolCallStartTime,
}
} catch (error) {
if (isAbortError(error) || request.abortSignal?.aborted) {
throw error
}
await recordProviderConversationToolError(
request,
toolCall.id,
toolName,
getErrorMessage(error, 'Tool execution failed')
)
const toolCallEndTime = Date.now()
logger.error('Error processing tool call (OpenRouter):', {
error: toError(error).message,
toolName,
})
return {
toolCall,
toolName,
toolParams: {},
result: {
success: false,
output: undefined,
error: getErrorMessage(error, 'Tool execution failed'),
},
startTime: toolCallStartTime,
endTime: toolCallEndTime,
duration: toolCallEndTime - toolCallStartTime,
}
}
})
const executionResults = await Promise.all(toolExecutionPromises)
const assistantMessage = currentResponse.choices[0]?.message as
| OpenRouterAssistantMessage
| undefined
if (assistantMessage) {
const assistantHistory: OpenAICompatAssistantHistoryMessage & {
reasoning_details?: OpenRouterReasoningDetail[]
} = createOpenAICompatAssistantHistory({
message: assistantMessage,
toolCalls: toolCallsInResponse,
reasoningFields: ['reasoning', 'reasoning_content'],
})
if (Array.isArray(assistantMessage.reasoning_details)) {
assistantHistory.reasoning_details = assistantMessage.reasoning_details
}
currentMessages.push(assistantHistory)
}
for (const executionResult of executionResults) {
const { toolCall, toolName, toolParams, result, startTime, endTime, duration } =
executionResult
const modelResult =
'modelResult' in executionResult ? (executionResult.modelResult ?? result) : result
timeSegments.push({
type: 'tool',
name: toolName,
startTime: startTime,
endTime: endTime,
duration: duration,
toolCallId: toolCall.id,
})
let resultContent: unknown
if (result.success) {
if (isRecordLike(result.output)) {
toolResults.push(result.output)
}
resultContent = result.output ?? null
} else {
resultContent = {
error: true,
message: result.error || 'Tool execution failed',
tool: toolName,
}
}
const modelResultContent = modelResult.success
? (modelResult.output ?? null)
: {
error: true,
message: modelResult.error || 'Tool execution failed',
tool: toolName,
}
toolCalls.push({
name: toolName,
arguments: toolParams,
startTime: new Date(startTime).toISOString(),
endTime: new Date(endTime).toISOString(),
duration: duration,
result: resultContent,
success: result.success,
})
currentMessages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify(modelResultContent),
})
}
const thisToolsTime = Date.now() - toolsStartTime
toolsTime += thisToolsTime
const nextPayload = {
...payload,
messages: currentMessages,
}
if (typeof originalToolChoice === 'object' && hasUsedForcedTool && forcedTools.length > 0) {
const remainingTools = forcedTools.filter((tool) => !usedForcedTools.includes(tool))
if (remainingTools.length > 0) {
nextPayload.tool_choice = { type: 'function', function: { name: remainingTools[0] } }
} else {
nextPayload.tool_choice = 'auto'
}
}
const nextModelStartTime = Date.now()
currentResponse = await client.chat.completions.create(
await prepareConversationGeneration(request, 'chat-completions', nextPayload),
request.abortSignal ? { signal: request.abortSignal } : undefined
)
if (!currentResponse.choices[0]?.message?.tool_calls?.length) {
await captureProviderConversationStep(
request,
'chat-completions',
currentResponse.choices[0]?.message,
getChatCompletionConversationUsage(currentResponse.usage)
)
}
const nextForcedToolResult = checkForForcedToolUsage(
currentResponse,
nextPayload.tool_choice,
forcedTools,
usedForcedTools
)
hasUsedForcedTool = nextForcedToolResult.hasUsedForcedTool
usedForcedTools = nextForcedToolResult.usedForcedTools
const nextModelEndTime = Date.now()
const thisModelTime = nextModelEndTime - nextModelStartTime
timeSegments.push({
type: 'model',
name: request.model,
startTime: nextModelStartTime,
endTime: nextModelEndTime,
duration: thisModelTime,
})
modelTime += thisModelTime
if (currentResponse.choices[0]?.message?.content) {
content = currentResponse.choices[0].message.content
}
if (currentResponse.usage) {
tokens.input += currentResponse.usage.prompt_tokens || 0
tokens.output += currentResponse.usage.completion_tokens || 0
tokens.total += currentResponse.usage.total_tokens || 0
}
iterationCount++
}
if (iterationCount === MAX_TOOL_ITERATIONS) {
if (currentResponse.choices[0]?.message?.tool_calls?.length) {
await recordProviderConversationUsage(
request,
getChatCompletionConversationUsage(currentResponse.usage)
)
}
const pendingToolCalls =
currentResponse.choices[0]?.message?.tool_calls?.filter(isFunctionToolCall)
enrichLastModelSegmentFromChatCompletions(timeSegments, currentResponse, pendingToolCalls, {
model: request.model,
provider: 'openrouter',
})
if (pendingToolCalls?.length && !(request.responseFormat && hasActiveTools)) {
const finalPayload: any = {
...payload,
messages: [...currentMessages],
tool_choice: 'none',
}
if (request.responseFormat) {
finalPayload.messages = await applyResponseFormat(
finalPayload,
finalPayload.messages,
request.responseFormat,
requestedModel
)
}
const finalStartTime = Date.now()
const finalResponse = await client.chat.completions.create(
await prepareConversationGeneration(request, 'chat-completions', finalPayload),
request.abortSignal ? { signal: request.abortSignal } : undefined
)
if (!finalResponse.choices[0]?.message?.tool_calls?.length) {
await captureProviderConversationStep(
request,
'chat-completions',
finalResponse.choices[0]?.message,
getChatCompletionConversationUsage(finalResponse.usage)
)
}
const finalEndTime = Date.now()
const finalDuration = finalEndTime - finalStartTime
timeSegments.push({
type: 'model',
name: 'Final answer after tool iteration limit',
startTime: finalStartTime,
endTime: finalEndTime,
duration: finalDuration,
})
modelTime += finalDuration
if (finalResponse.choices[0]?.message?.content) {
content = finalResponse.choices[0].message.content
}
if (finalResponse.usage) {
tokens.input += finalResponse.usage.prompt_tokens || 0
tokens.output += finalResponse.usage.completion_tokens || 0
tokens.total += finalResponse.usage.total_tokens || 0
}
enrichLastModelSegmentFromChatCompletions(
timeSegments,
finalResponse,
finalResponse.choices[0]?.message?.tool_calls?.filter(isFunctionToolCall),
{ model: request.model, provider: 'openrouter' }
)
}
}
if (request.responseFormat && hasActiveTools) {
const finalPayload: any = {
model: payload.model,
messages: [...currentMessages],
}
if (payload.temperature !== undefined) {
finalPayload.temperature = payload.temperature
}
if (payload.max_tokens !== undefined) {
finalPayload.max_tokens = payload.max_tokens
}
finalPayload.messages = await applyResponseFormat(
finalPayload,
finalPayload.messages,
request.responseFormat,
requestedModel
)
const finalStartTime = Date.now()
const finalResponse = await client.chat.completions.create(
await prepareConversationGeneration(request, 'chat-completions', finalPayload),
request.abortSignal ? { signal: request.abortSignal } : undefined
)
if (!finalResponse.choices[0]?.message?.tool_calls?.length) {
await captureProviderConversationStep(
request,
'chat-completions',
finalResponse.choices[0]?.message,
getChatCompletionConversationUsage(finalResponse.usage)
)
}
const finalEndTime = Date.now()
const finalDuration = finalEndTime - finalStartTime
timeSegments.push({
type: 'model',
name: 'Final structured response',
startTime: finalStartTime,
endTime: finalEndTime,
duration: finalDuration,
})
modelTime += finalDuration
if (finalResponse.choices[0]?.message?.content) {
content = finalResponse.choices[0].message.content
}
if (finalResponse.usage) {
tokens.input += finalResponse.usage.prompt_tokens || 0
tokens.output += finalResponse.usage.completion_tokens || 0
tokens.total += finalResponse.usage.total_tokens || 0
}
enrichLastModelSegmentFromChatCompletions(
timeSegments,
finalResponse,
finalResponse.choices[0]?.message?.tool_calls?.filter(isFunctionToolCall),
{ model: request.model, provider: 'openrouter' }
)
}
if (request.stream) {
const accumulatedCost = calculateCost(requestedModel, tokens.input, tokens.output)
const toolCost = sumToolCosts(toolResults)
const finalCost = {
input: accumulatedCost.input,
output: accumulatedCost.output,
toolCost: toolCost || undefined,
total: accumulatedCost.total + toolCost,
}
const streamingResult = createStreamingExecution({
model: requestedModel,
providerStartTime,
providerStartTimeISO,
timing: {
kind: 'accumulated',
modelTime,
toolsTime,
firstResponseTime,
iterations: timeSegments.filter((segment) => segment.type === 'model').length,
timeSegments,
},
initialTokens: { input: tokens.input, output: tokens.output, total: tokens.total },
initialCost: finalCost,
toolCalls:
toolCalls.length > 0 ? { list: toolCalls, count: toolCalls.length } : undefined,
streamFormat: 'agent-events-v1',
createStream: ({ output, finalizeTiming }) => {
output.content = content
output.tokens = { input: tokens.input, output: tokens.output, total: tokens.total }
output.cost = finalCost
finalizeTiming()
return createSettledAgentEventStream(content)
},
})
return streamingResult
}
const providerEndTime = Date.now()
const providerEndTimeISO = new Date(providerEndTime).toISOString()
const totalDuration = providerEndTime - providerStartTime
return {
content,
model: requestedModel,
tokens,
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
toolResults: toolResults.length > 0 ? toolResults : undefined,
timing: {
startTime: providerStartTimeISO,
endTime: providerEndTimeISO,
duration: totalDuration,
modelTime: modelTime,
toolsTime: toolsTime,
firstResponseTime: firstResponseTime,
iterations: timeSegments.filter((segment) => segment.type === 'model').length,
timeSegments: timeSegments,
},
}
} catch (error) {
const providerEndTime = Date.now()
const providerEndTimeISO = new Date(providerEndTime).toISOString()
const totalDuration = providerEndTime - providerStartTime
const errorDetails: Record<string, any> = {
error: toError(error).message,
duration: totalDuration,
}
if (error && typeof error === 'object') {
const err = error as any
if (err.status) errorDetails.status = err.status
if (err.code) errorDetails.code = err.code
if (err.type) errorDetails.type = err.type
if (err.error?.message) errorDetails.providerMessage = err.error.message
if (err.error?.metadata) errorDetails.metadata = err.error.metadata
}
logger.error('Error in OpenRouter request:', errorDetails)
if (
isAbortError(error) ||
request.abortSignal?.aborted ||
isConversationContextError(error)
) {
throw error
}
throw new ProviderError(toError(error).message, {
startTime: providerStartTimeISO,
endTime: providerEndTimeISO,
duration: totalDuration,
})
}
return executeChatCompletionRequest(request, {
providerId: 'openrouter',
providerName: 'OpenRouter',
client,
requestedModel,
reportedModel: requestedModel,
logger,
applyResponseFormat,
reasoningFields: ['reasoning', 'reasoning_content'],
preserveReasoningDetails: true,
recordPendingUsage: true,
})
},
}
-35
View File
@@ -1,11 +1,5 @@
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import type { ChatCompletionChunk } from 'openai/resources/chat/completions'
import type { CompletionUsage } from 'openai/resources/completions'
import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events'
import type { AgentStreamEvent } from '@/providers/stream-events'
import type { ProviderRequest } from '@/providers/types'
import { checkForForcedToolUsageOpenAI } from '@/providers/utils'
const logger = createLogger('OpenRouterUtils')
@@ -116,32 +110,3 @@ export async function supportsNativeStructuredOutputs(modelId: string): Promise<
const capabilities = await getOpenRouterModelCapabilities(modelId)
return capabilities?.supportsStructuredOutputs ?? false
}
export function createReadableStreamFromOpenAIStream(
openaiStream: AsyncIterable<ChatCompletionChunk>,
onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void,
request?: ProviderRequest
): ReadableStream<AgentStreamEvent> {
return createOpenAICompatibleAgentEventStream(openaiStream, {
request,
providerName: 'OpenRouter',
onComplete: onComplete
? (result) => onComplete(result.content, result.usage, result.thinking)
: undefined,
})
}
export function checkForForcedToolUsage(
response: any,
toolChoice: string | { type: string; function?: { name: string }; name?: string; any?: any },
forcedTools: string[],
usedForcedTools: string[]
): { hasUsedForcedTool: boolean; usedForcedTools: string[] } {
return checkForForcedToolUsageOpenAI(
response,
toolChoice,
'OpenRouter',
forcedTools,
usedForcedTools
)
}
+24 -21
View File
@@ -17,8 +17,8 @@ import {
import { getProviderDefaultModel, getProviderModels } from '@/providers/models'
import { createOpenAICompatAssistantHistory } from '@/providers/openai-compat/assistant-history'
import { getChatCompletionConversationUsage } from '@/providers/openai-compat/conversation-usage'
import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events'
import { executeProviderTool } from '@/providers/runtime-context'
import { createReadableStreamFromSakanaStream } from '@/providers/sakana/utils'
import { createSettledAgentEventStream } from '@/providers/stream-events'
import { createStreamingExecution } from '@/providers/streaming-execution'
import { isAbortError, parseToolArguments } from '@/providers/streaming-tool-loop-shared'
@@ -170,29 +170,32 @@ export const sakanaProvider: ProviderConfig = {
isStreaming: true,
streamFormat: 'agent-events-v1',
createStream: ({ output }) =>
createReadableStreamFromSakanaStream(
createOpenAICompatibleAgentEventStream(
// double-cast-allowed: payload is untyped so the SDK cannot resolve the streaming overload; the stream yields OpenAI ChatCompletionChunk objects
streamResponse as unknown as AsyncIterable<ChatCompletionChunk>,
(content, usage) => {
output.content = content
output.tokens = {
input: usage.prompt_tokens,
output: usage.completion_tokens,
total: usage.total_tokens,
}
{
providerName: 'Sakana',
request,
onComplete: ({ content, usage }) => {
output.content = content
output.tokens = {
input: usage.prompt_tokens,
output: usage.completion_tokens,
total: usage.total_tokens,
}
const costResult = calculateCost(
request.model,
usage.prompt_tokens,
usage.completion_tokens
)
output.cost = {
input: costResult.input,
output: costResult.output,
total: costResult.total,
}
},
request
const costResult = calculateCost(
request.model,
usage.prompt_tokens,
usage.completion_tokens
)
output.cost = {
input: costResult.input,
output: costResult.output,
total: costResult.total,
}
},
}
),
})
-23
View File
@@ -1,23 +0,0 @@
import type { ChatCompletionChunk } from 'openai/resources/chat/completions'
import type { CompletionUsage } from 'openai/resources/completions'
import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events'
import type { AgentStreamEvent } from '@/providers/stream-events'
import type { ProviderRequest } from '@/providers/types'
/**
* Creates an agent-events stream from a Sakana AI streaming response.
* Uses the shared OpenAI-compatible agent event streaming utility.
*/
export function createReadableStreamFromSakanaStream(
sakanaStream: AsyncIterable<ChatCompletionChunk>,
onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void,
request?: ProviderRequest
): ReadableStream<AgentStreamEvent> {
return createOpenAICompatibleAgentEventStream(sakanaStream, {
request,
providerName: 'Sakana',
onComplete: onComplete
? (result) => onComplete(result.content, result.usage, result.thinking)
: undefined,
})
}
@@ -70,63 +70,14 @@ vi.mock('@/providers/trace-enrichment', () => providersTraceEnrichmentMock)
vi.mock('@/providers/utils', () => providersUtilsMock)
vi.mock('@/providers/baseten/utils', () => ({
checkForForcedToolUsage: vi.fn(() => ({
hasUsedForcedTool: false,
usedForcedTools: [],
})),
createReadableStreamFromOpenAIStream: vi.fn(() => createEmptyStream()),
supportsNativeStructuredOutputs: vi.fn(() => true),
}))
vi.mock('@/providers/fireworks/utils', () => ({
checkForForcedToolUsage: vi.fn(() => ({
hasUsedForcedTool: false,
usedForcedTools: [],
})),
createReadableStreamFromOpenAIStream: vi.fn(() => createEmptyStream()),
supportsNativeStructuredOutputs: vi.fn(() => true),
resolveFireworksWireModel: vi.fn((stripped: string) => stripped),
}))
vi.mock('@/providers/openrouter/utils', () => ({
checkForForcedToolUsage: vi.fn(() => ({
hasUsedForcedTool: false,
usedForcedTools: [],
})),
createReadableStreamFromOpenAIStream: vi.fn(() => createEmptyStream()),
supportsNativeStructuredOutputs: vi.fn(() => true),
}))
vi.mock('@/providers/together/utils', () => ({
checkForForcedToolUsage: vi.fn(() => ({
hasUsedForcedTool: false,
usedForcedTools: [],
})),
createReadableStreamFromOpenAIStream: vi.fn(() => createEmptyStream()),
supportsNativeStructuredOutputs: vi.fn(() => true),
}))
vi.mock('@/providers/cerebras/utils', () => ({
createReadableStreamFromCerebrasStream: vi.fn(() => createEmptyStream()),
}))
vi.mock('@/providers/kimi/utils', () => ({
createReadableStreamFromKimiStream: vi.fn(() => createEmptyStream()),
}))
vi.mock('@/providers/meta/utils', () => ({
createReadableStreamFromMetaStream: vi.fn(() => createEmptyStream()),
}))
vi.mock('@/providers/nvidia/utils', () => ({
createReadableStreamFromNvidiaStream: vi.fn(() => createEmptyStream()),
}))
vi.mock('@/providers/sakana/utils', () => ({
createReadableStreamFromSakanaStream: vi.fn(() => createEmptyStream()),
}))
vi.mock('@/providers/zai/utils', () => ({
createReadableStreamFromZaiStream: vi.fn(() => createEmptyStream()),
}))
vi.mock('@/providers/xai/utils', () => ({
checkForForcedToolUsage: vi.fn(() => ({
hasUsedForcedTool: false,
usedForcedTools: [],
})),
createReadableStreamFromXAIStream: vi.fn(() => createEmptyStream()),
createResponseFormatPayload: vi.fn(() => ({})),
}))
@@ -252,7 +203,7 @@ const STRUCTURED_OUTPUT_PROVIDERS = [
name: 'Together',
provider: togetherProvider,
model: 'together/test-model',
responseFormatType: 'json_schema',
responseFormatType: 'json_object',
disablesTools: 'omit',
},
{
@@ -285,14 +236,6 @@ const STRUCTURED_OUTPUT_PROVIDERS = [
},
] as const
function createEmptyStream(): ReadableStream<Uint8Array> {
return new ReadableStream({
start(controller) {
controller.close()
},
})
}
function toolCall(id: string, argumentsJson = '{}'): ToolCall {
return {
id,
+3 -35
View File
@@ -12,10 +12,6 @@ import { toolsMock, toolsMockFns } from '@sim/testing/mocks/tools.mock'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { StreamingExecution } from '@/executor/types'
const { mockSupportsNativeStructuredOutputs } = vi.hoisted(() => ({
mockSupportsNativeStructuredOutputs: vi.fn(),
}))
vi.mock('openai', () => openaiMock)
vi.mock('@/providers/conversation-history', () => providersConversationHistoryMock)
@@ -26,14 +22,6 @@ vi.mock('@/providers/models', () => providersModelsMock)
vi.mock('@/providers/attachments', () => providersAttachmentsMock)
vi.mock('@/providers/together/utils', () => ({
supportsNativeStructuredOutputs: mockSupportsNativeStructuredOutputs,
createReadableStreamFromOpenAIStream: vi.fn(
() => new ReadableStream({ start: (controller) => controller.close() })
),
checkForForcedToolUsage: vi.fn(() => ({ hasUsedForcedTool: false, usedForcedTools: [] })),
}))
vi.mock('@/providers/trace-enrichment', () => providersTraceEnrichmentMock)
vi.mock('@/providers/utils', () => providersUtilsMock)
@@ -87,7 +75,6 @@ const lastCallBody = () => mockCreate.mock.calls.at(-1)?.[0]
describe('togetherProvider', () => {
beforeEach(() => {
mockSupportsNativeStructuredOutputs.mockResolvedValue(true)
mockPrepareToolsWithUsageControl.mockImplementation((tools) => ({
tools,
toolChoice: 'auto',
@@ -153,7 +140,7 @@ describe('togetherProvider', () => {
)
it('streams directly when there are no tools', async () => {
mockCreate.mockResolvedValueOnce({})
mockCreate.mockResolvedValueOnce((async function* () {})())
const result = await togetherProvider.executeRequest({ ...baseRequest, stream: true })
@@ -170,23 +157,7 @@ describe('togetherProvider', () => {
expect(callBody(0).model).toBe('Org/Custom-Model')
})
it('sends a json_schema response_format with no strict field', async () => {
mockCreate.mockResolvedValueOnce(textResponse('{}'))
await togetherProvider.executeRequest({
...baseRequest,
responseFormat: { name: 'my_schema', schema: { type: 'object' }, strict: true },
})
expect(lastCallBody().response_format).toEqual({
type: 'json_schema',
json_schema: { name: 'my_schema', schema: { type: 'object' } },
})
expect(lastCallBody().response_format.json_schema).not.toHaveProperty('strict')
})
it('falls back to json_object with prompt instructions when native is unsupported', async () => {
mockSupportsNativeStructuredOutputs.mockResolvedValue(false)
it('uses json_object with prompt instructions for structured output', async () => {
mockCreate.mockResolvedValueOnce(textResponse('{}'))
await togetherProvider.executeRequest({
@@ -215,10 +186,7 @@ describe('togetherProvider', () => {
expect(mockCreate).toHaveBeenCalledTimes(2)
expect(callBody(0).response_format).toBeUndefined()
expect(callBody(0).tools).toBeDefined()
expect(callBody(1).response_format).toEqual({
type: 'json_schema',
json_schema: { name: 'my_schema', schema: { type: 'object' } },
})
expect(callBody(1).response_format).toEqual({ type: 'json_object' })
expect(callBody(1).tools).toBeUndefined()
})
+22 -684
View File
@@ -1,79 +1,25 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { isRecordLike } from '@sim/utils/object'
import OpenAI from 'openai'
import type { ChatCompletionCreateParamsStreaming } from 'openai/resources/chat/completions'
import type { ChatCompletionMessageParam } from 'openai/resources/chat/completions'
import type { StreamingExecution } from '@/executor/types'
import { MAX_TOOL_ITERATIONS } from '@/providers'
import { formatMessagesForProvider } from '@/providers/attachments'
import {
isConversationContextError,
prepareConversationGeneration,
} from '@/providers/conversation-generation'
import {
captureProviderConversationStep,
recordProviderConversationToolError,
recordProviderConversationUsage,
} from '@/providers/conversation-history'
import { getProviderDefaultModel, getProviderModels } from '@/providers/models'
import { createOpenAICompatAssistantHistory } from '@/providers/openai-compat/assistant-history'
import { getChatCompletionConversationUsage } from '@/providers/openai-compat/conversation-usage'
import { executeProviderTool } from '@/providers/runtime-context'
import { createSettledAgentEventStream } from '@/providers/stream-events'
import { createStreamingExecution } from '@/providers/streaming-execution'
import { isAbortError, parseToolArguments } from '@/providers/streaming-tool-loop-shared'
import {
checkForForcedToolUsage,
createReadableStreamFromOpenAIStream,
supportsNativeStructuredOutputs,
} from '@/providers/together/utils'
import { adaptOpenAIChatToolSchema } from '@/providers/tool-schema-adapter'
import { enrichLastModelSegmentFromChatCompletions } from '@/providers/trace-enrichment'
type ChatCompletionPayload,
executeChatCompletionRequest,
} from '@/providers/openai-compat/chat-completions'
import { openAICompatTransport } from '@/providers/transport'
import type {
FunctionCallResponse,
Message,
ProviderConfig,
ProviderRequest,
ProviderResponse,
TimeSegment,
} from '@/providers/types'
import { ProviderError } from '@/providers/types'
import {
calculateCost,
generateSchemaInstructions,
isFunctionToolCall,
prepareToolExecution,
prepareToolsWithUsageControl,
sumToolCosts,
} from '@/providers/utils'
import type { ProviderConfig, ProviderRequest, ProviderResponse } from '@/providers/types'
import { generateSchemaInstructions } from '@/providers/utils'
const logger = createLogger('TogetherProvider')
/**
* Applies structured output configuration to a payload based on model capabilities.
* Uses native json_schema for supported models, falls back to json_object with prompt instructions.
*/
/** Together uses JSON-object mode because native schema support varies across its models. */
async function applyResponseFormat(
targetPayload: any,
messages: any[],
responseFormat: any,
targetPayload: ChatCompletionPayload,
messages: ChatCompletionMessageParam[],
responseFormat: NonNullable<ProviderRequest['responseFormat']>,
model: string
): Promise<any[]> {
const useNative = await supportsNativeStructuredOutputs(model)
if (useNative) {
logger.info('Using native structured outputs for Together model', { model })
targetPayload.response_format = {
type: 'json_schema',
json_schema: {
name: responseFormat.name || 'response_schema',
schema: responseFormat.schema || responseFormat,
},
}
return messages
}
): Promise<ChatCompletionMessageParam[]> {
logger.info('Using json_object mode with prompt instructions for Together model', { model })
const schema = responseFormat.schema || responseFormat
const schemaInstructions = generateSchemaInstructions(schema, responseFormat.name)
@@ -114,624 +60,16 @@ export const togetherProvider: ProviderConfig = {
stream: !!request.stream,
})
const allMessages: Message[] = []
if (request.systemPrompt) {
allMessages.push({ role: 'system', content: request.systemPrompt })
}
if (request.context) {
allMessages.push({ role: 'user', content: request.context })
}
if (request.messages) {
allMessages.push(...request.messages)
}
const formattedMessages = formatMessagesForProvider(allMessages, 'together') as Message[]
const tools = request.tools?.length
? request.tools.map((tool) => adaptOpenAIChatToolSchema(tool))
: undefined
const payload: any = {
model: requestedModel,
messages: formattedMessages,
}
if (request.temperature !== undefined) payload.temperature = request.temperature
if (request.maxTokens != null) payload.max_tokens = request.maxTokens
let preparedTools: ReturnType<typeof prepareToolsWithUsageControl> | null = null
let hasActiveTools = false
if (tools?.length) {
preparedTools = prepareToolsWithUsageControl(tools, request.tools, logger, 'together')
const { tools: filteredTools, toolChoice } = preparedTools
if (filteredTools?.length && toolChoice) {
payload.tools = filteredTools
payload.tool_choice = toolChoice
hasActiveTools = true
}
}
const providerStartTime = Date.now()
const providerStartTimeISO = new Date(providerStartTime).toISOString()
try {
if (request.responseFormat && !hasActiveTools) {
payload.messages = await applyResponseFormat(
payload,
payload.messages,
request.responseFormat,
requestedModel
)
}
if (request.stream && (!tools || tools.length === 0 || !hasActiveTools)) {
const streamingParams: ChatCompletionCreateParamsStreaming = {
...payload,
stream: true,
stream_options: { include_usage: true },
}
const streamResponse = await client.chat.completions.create(
await prepareConversationGeneration(request, 'chat-completions', streamingParams),
request.abortSignal ? { signal: request.abortSignal } : undefined
)
const streamingResult = createStreamingExecution({
model: requestedModel,
providerStartTime,
providerStartTimeISO,
timing: { kind: 'simple', segmentName: request.model },
initialTokens: { input: 0, output: 0, total: 0 },
initialCost: { input: 0, output: 0, total: 0 },
streamFormat: 'agent-events-v1',
createStream: ({ output, finalizeTiming }) =>
createReadableStreamFromOpenAIStream(
streamResponse,
(content, usage) => {
output.content = content
output.tokens = {
input: usage.prompt_tokens,
output: usage.completion_tokens,
total: usage.total_tokens,
}
const costResult = calculateCost(
requestedModel,
usage.prompt_tokens,
usage.completion_tokens
)
output.cost = {
input: costResult.input,
output: costResult.output,
total: costResult.total,
}
finalizeTiming()
},
request
),
})
return streamingResult
}
const initialCallTime = Date.now()
const originalToolChoice = payload.tool_choice
const forcedTools = preparedTools?.forcedTools || []
let usedForcedTools: string[] = []
let currentResponse = await client.chat.completions.create(
await prepareConversationGeneration(request, 'chat-completions', payload),
request.abortSignal ? { signal: request.abortSignal } : undefined
)
if (!currentResponse.choices[0]?.message?.tool_calls?.length) {
await captureProviderConversationStep(
request,
'chat-completions',
currentResponse.choices[0]?.message,
getChatCompletionConversationUsage(currentResponse.usage)
)
}
const firstResponseTime = Date.now() - initialCallTime
let content = currentResponse.choices[0]?.message?.content || ''
const tokens = {
input: currentResponse.usage?.prompt_tokens || 0,
output: currentResponse.usage?.completion_tokens || 0,
total: currentResponse.usage?.total_tokens || 0,
}
const toolCalls: FunctionCallResponse[] = []
const toolResults: Record<string, unknown>[] = []
const currentMessages = [...formattedMessages]
let iterationCount = 0
let modelTime = firstResponseTime
let toolsTime = 0
let hasUsedForcedTool = false
const timeSegments: TimeSegment[] = [
{
type: 'model',
name: request.model,
startTime: initialCallTime,
endTime: initialCallTime + firstResponseTime,
duration: firstResponseTime,
},
]
const forcedToolResult = checkForForcedToolUsage(
currentResponse,
originalToolChoice,
forcedTools,
usedForcedTools
)
hasUsedForcedTool = forcedToolResult.hasUsedForcedTool
usedForcedTools = forcedToolResult.usedForcedTools
while (iterationCount < MAX_TOOL_ITERATIONS) {
if (currentResponse.choices[0]?.message?.content) {
content = currentResponse.choices[0].message.content
}
const toolCallsInResponse =
currentResponse.choices[0]?.message?.tool_calls?.filter(isFunctionToolCall)
enrichLastModelSegmentFromChatCompletions(
timeSegments,
currentResponse,
toolCallsInResponse,
{ model: request.model, provider: 'together' }
)
if (!toolCallsInResponse || toolCallsInResponse.length === 0) {
break
}
const toolsStartTime = Date.now()
await captureProviderConversationStep(
request,
'chat-completions',
currentResponse.choices[0]?.message,
getChatCompletionConversationUsage(currentResponse.usage)
)
const toolExecutionPromises = toolCallsInResponse.map(async (toolCall) => {
const toolCallStartTime = Date.now()
const toolName = toolCall.function.name
try {
const toolArgs = parseToolArguments(toolCall.function.arguments, toolName)
const tool = request.tools?.find((t) => t.id === toolName)
if (!tool) {
await recordProviderConversationToolError(
request,
toolCall.id,
toolName,
`Tool "${toolName}" is not available`
)
const toolCallEndTime = Date.now()
return {
toolCall,
toolName,
toolParams: {},
result: {
success: false,
output: undefined,
error: `Tool "${toolName}" is not available`,
},
startTime: toolCallStartTime,
endTime: toolCallEndTime,
duration: toolCallEndTime - toolCallStartTime,
}
}
const { toolParams, executionParams } = prepareToolExecution(
tool,
toolArgs,
request,
toolCall.id
)
const { rawResponse, modelResponse } = await executeProviderTool(
toolName,
executionParams,
{
signal: request.abortSignal,
}
)
const toolCallEndTime = Date.now()
return {
toolCall,
toolName,
toolParams,
result: rawResponse,
modelResult: modelResponse,
startTime: toolCallStartTime,
endTime: toolCallEndTime,
duration: toolCallEndTime - toolCallStartTime,
}
} catch (error) {
if (isAbortError(error) || request.abortSignal?.aborted) {
throw error
}
await recordProviderConversationToolError(
request,
toolCall.id,
toolName,
getErrorMessage(error, 'Tool execution failed')
)
const toolCallEndTime = Date.now()
logger.error('Error processing tool call (Together):', {
error: toError(error).message,
toolName,
})
return {
toolCall,
toolName,
toolParams: {},
result: {
success: false,
output: undefined,
error: getErrorMessage(error, 'Tool execution failed'),
},
startTime: toolCallStartTime,
endTime: toolCallEndTime,
duration: toolCallEndTime - toolCallStartTime,
}
}
})
const executionResults = await Promise.all(toolExecutionPromises)
const assistantMessage = currentResponse.choices[0]?.message
if (assistantMessage) {
currentMessages.push(
createOpenAICompatAssistantHistory({
message: assistantMessage,
toolCalls: toolCallsInResponse,
reasoningFields: ['reasoning', 'reasoning_content'],
})
)
}
for (const executionResult of executionResults) {
const { toolCall, toolName, toolParams, result, startTime, endTime, duration } =
executionResult
const modelResult =
'modelResult' in executionResult ? (executionResult.modelResult ?? result) : result
timeSegments.push({
type: 'tool',
name: toolName,
startTime: startTime,
endTime: endTime,
duration: duration,
toolCallId: toolCall.id,
})
let resultContent: unknown
if (result.success) {
if (isRecordLike(result.output)) {
toolResults.push(result.output)
}
resultContent = result.output ?? null
} else {
resultContent = {
error: true,
message: result.error || 'Tool execution failed',
tool: toolName,
}
}
const modelResultContent = modelResult.success
? (modelResult.output ?? null)
: {
error: true,
message: modelResult.error || 'Tool execution failed',
tool: toolName,
}
toolCalls.push({
name: toolName,
arguments: toolParams,
startTime: new Date(startTime).toISOString(),
endTime: new Date(endTime).toISOString(),
duration: duration,
result: resultContent,
success: result.success,
})
currentMessages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify(modelResultContent),
})
}
const thisToolsTime = Date.now() - toolsStartTime
toolsTime += thisToolsTime
const nextPayload = {
...payload,
messages: currentMessages,
}
if (typeof originalToolChoice === 'object' && hasUsedForcedTool && forcedTools.length > 0) {
const remainingTools = forcedTools.filter((tool) => !usedForcedTools.includes(tool))
if (remainingTools.length > 0) {
nextPayload.tool_choice = { type: 'function', function: { name: remainingTools[0] } }
} else {
nextPayload.tool_choice = 'auto'
}
}
const nextModelStartTime = Date.now()
currentResponse = await client.chat.completions.create(
await prepareConversationGeneration(request, 'chat-completions', nextPayload),
request.abortSignal ? { signal: request.abortSignal } : undefined
)
if (!currentResponse.choices[0]?.message?.tool_calls?.length) {
await captureProviderConversationStep(
request,
'chat-completions',
currentResponse.choices[0]?.message,
getChatCompletionConversationUsage(currentResponse.usage)
)
}
const nextForcedToolResult = checkForForcedToolUsage(
currentResponse,
nextPayload.tool_choice,
forcedTools,
usedForcedTools
)
hasUsedForcedTool = nextForcedToolResult.hasUsedForcedTool
usedForcedTools = nextForcedToolResult.usedForcedTools
const nextModelEndTime = Date.now()
const thisModelTime = nextModelEndTime - nextModelStartTime
timeSegments.push({
type: 'model',
name: request.model,
startTime: nextModelStartTime,
endTime: nextModelEndTime,
duration: thisModelTime,
})
modelTime += thisModelTime
if (currentResponse.choices[0]?.message?.content) {
content = currentResponse.choices[0].message.content
}
if (currentResponse.usage) {
tokens.input += currentResponse.usage.prompt_tokens || 0
tokens.output += currentResponse.usage.completion_tokens || 0
tokens.total += currentResponse.usage.total_tokens || 0
}
iterationCount++
}
if (iterationCount === MAX_TOOL_ITERATIONS) {
if (currentResponse.choices[0]?.message?.tool_calls?.length) {
await recordProviderConversationUsage(
request,
getChatCompletionConversationUsage(currentResponse.usage)
)
}
const pendingToolCalls =
currentResponse.choices[0]?.message?.tool_calls?.filter(isFunctionToolCall)
enrichLastModelSegmentFromChatCompletions(timeSegments, currentResponse, pendingToolCalls, {
model: request.model,
provider: 'together',
})
if (pendingToolCalls?.length && !(request.responseFormat && hasActiveTools)) {
const finalPayload: any = {
...payload,
messages: [...currentMessages],
tool_choice: 'none',
}
if (request.responseFormat) {
finalPayload.messages = await applyResponseFormat(
finalPayload,
finalPayload.messages,
request.responseFormat,
requestedModel
)
}
const finalStartTime = Date.now()
const finalResponse = await client.chat.completions.create(
await prepareConversationGeneration(request, 'chat-completions', finalPayload),
request.abortSignal ? { signal: request.abortSignal } : undefined
)
if (!finalResponse.choices[0]?.message?.tool_calls?.length) {
await captureProviderConversationStep(
request,
'chat-completions',
finalResponse.choices[0]?.message,
getChatCompletionConversationUsage(finalResponse.usage)
)
}
const finalEndTime = Date.now()
const finalDuration = finalEndTime - finalStartTime
timeSegments.push({
type: 'model',
name: 'Final answer after tool iteration limit',
startTime: finalStartTime,
endTime: finalEndTime,
duration: finalDuration,
})
modelTime += finalDuration
if (finalResponse.choices[0]?.message?.content) {
content = finalResponse.choices[0].message.content
}
if (finalResponse.usage) {
tokens.input += finalResponse.usage.prompt_tokens || 0
tokens.output += finalResponse.usage.completion_tokens || 0
tokens.total += finalResponse.usage.total_tokens || 0
}
enrichLastModelSegmentFromChatCompletions(
timeSegments,
finalResponse,
finalResponse.choices[0]?.message?.tool_calls?.filter(isFunctionToolCall),
{ model: request.model, provider: 'together' }
)
}
}
if (request.responseFormat && hasActiveTools) {
const finalPayload: any = {
model: payload.model,
messages: [...currentMessages],
}
if (payload.temperature !== undefined) {
finalPayload.temperature = payload.temperature
}
if (payload.max_tokens !== undefined) {
finalPayload.max_tokens = payload.max_tokens
}
finalPayload.messages = await applyResponseFormat(
finalPayload,
finalPayload.messages,
request.responseFormat,
requestedModel
)
const finalStartTime = Date.now()
const finalResponse = await client.chat.completions.create(
await prepareConversationGeneration(request, 'chat-completions', finalPayload),
request.abortSignal ? { signal: request.abortSignal } : undefined
)
if (!finalResponse.choices[0]?.message?.tool_calls?.length) {
await captureProviderConversationStep(
request,
'chat-completions',
finalResponse.choices[0]?.message,
getChatCompletionConversationUsage(finalResponse.usage)
)
}
const finalEndTime = Date.now()
const finalDuration = finalEndTime - finalStartTime
timeSegments.push({
type: 'model',
name: 'Final structured response',
startTime: finalStartTime,
endTime: finalEndTime,
duration: finalDuration,
})
modelTime += finalDuration
if (finalResponse.choices[0]?.message?.content) {
content = finalResponse.choices[0].message.content
}
if (finalResponse.usage) {
tokens.input += finalResponse.usage.prompt_tokens || 0
tokens.output += finalResponse.usage.completion_tokens || 0
tokens.total += finalResponse.usage.total_tokens || 0
}
enrichLastModelSegmentFromChatCompletions(
timeSegments,
finalResponse,
finalResponse.choices[0]?.message?.tool_calls?.filter(isFunctionToolCall),
{ model: request.model, provider: 'together' }
)
}
if (request.stream) {
const accumulatedCost = calculateCost(requestedModel, tokens.input, tokens.output)
const toolCost = sumToolCosts(toolResults)
const finalCost = {
input: accumulatedCost.input,
output: accumulatedCost.output,
toolCost: toolCost || undefined,
total: accumulatedCost.total + toolCost,
}
const streamingResult = createStreamingExecution({
model: requestedModel,
providerStartTime,
providerStartTimeISO,
timing: {
kind: 'accumulated',
modelTime,
toolsTime,
firstResponseTime,
iterations: timeSegments.filter((segment) => segment.type === 'model').length,
timeSegments,
},
initialTokens: { input: tokens.input, output: tokens.output, total: tokens.total },
initialCost: finalCost,
toolCalls:
toolCalls.length > 0 ? { list: toolCalls, count: toolCalls.length } : undefined,
streamFormat: 'agent-events-v1',
createStream: ({ output, finalizeTiming }) => {
output.content = content
output.tokens = { input: tokens.input, output: tokens.output, total: tokens.total }
output.cost = finalCost
finalizeTiming()
return createSettledAgentEventStream(content)
},
})
return streamingResult
}
const providerEndTime = Date.now()
const providerEndTimeISO = new Date(providerEndTime).toISOString()
const totalDuration = providerEndTime - providerStartTime
return {
content,
model: requestedModel,
tokens,
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
toolResults: toolResults.length > 0 ? toolResults : undefined,
timing: {
startTime: providerStartTimeISO,
endTime: providerEndTimeISO,
duration: totalDuration,
modelTime: modelTime,
toolsTime: toolsTime,
firstResponseTime: firstResponseTime,
iterations: timeSegments.filter((segment) => segment.type === 'model').length,
timeSegments: timeSegments,
},
}
} catch (error) {
const providerEndTime = Date.now()
const providerEndTimeISO = new Date(providerEndTime).toISOString()
const totalDuration = providerEndTime - providerStartTime
const errorDetails: Record<string, any> = {
error: toError(error).message,
duration: totalDuration,
}
if (error && typeof error === 'object') {
const err = error as any
if (err.status) errorDetails.status = err.status
if (err.code) errorDetails.code = err.code
if (err.type) errorDetails.type = err.type
if (err.error?.message) errorDetails.providerMessage = err.error.message
if (err.error?.metadata) errorDetails.metadata = err.error.metadata
}
logger.error('Error in Together request:', errorDetails)
if (
isAbortError(error) ||
request.abortSignal?.aborted ||
isConversationContextError(error)
) {
throw error
}
throw new ProviderError(toError(error).message, {
startTime: providerStartTimeISO,
endTime: providerEndTimeISO,
duration: totalDuration,
})
}
return executeChatCompletionRequest(request, {
providerId: 'together',
providerName: 'Together',
client,
requestedModel,
reportedModel: requestedModel,
logger,
applyResponseFormat,
reasoningFields: ['reasoning', 'reasoning_content'],
recordPendingUsage: true,
})
},
}
-51
View File
@@ -1,51 +0,0 @@
import type { ChatCompletionChunk } from 'openai/resources/chat/completions'
import type { CompletionUsage } from 'openai/resources/completions'
import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events'
import type { AgentStreamEvent } from '@/providers/stream-events'
import type { ProviderRequest } from '@/providers/types'
import { checkForForcedToolUsageOpenAI } from '@/providers/utils'
/**
* Together gates native `json_schema` per-model, so we use the broadly supported
* JSON-object mode for all models to avoid 400s. See https://docs.together.ai/docs/json-mode.
*/
export async function supportsNativeStructuredOutputs(_modelId: string): Promise<boolean> {
return false
}
/**
* Creates an agent-events stream from a Together AI streaming response.
* Uses the shared OpenAI-compatible agent event streaming utility.
*/
export function createReadableStreamFromOpenAIStream(
openaiStream: AsyncIterable<ChatCompletionChunk>,
onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void,
request?: ProviderRequest
): ReadableStream<AgentStreamEvent> {
return createOpenAICompatibleAgentEventStream(openaiStream, {
request,
providerName: 'Together',
onComplete: onComplete
? (result) => onComplete(result.content, result.usage, result.thinking)
: undefined,
})
}
/**
* Checks if a forced tool was used in a Together AI response.
* Uses the shared OpenAI-compatible forced tool usage helper.
*/
export function checkForForcedToolUsage(
response: any,
toolChoice: string | { type: string; function?: { name: string }; name?: string; any?: any },
forcedTools: string[],
usedForcedTools: string[]
): { hasUsedForcedTool: boolean; usedForcedTools: string[] } {
return checkForForcedToolUsageOpenAI(
response,
toolChoice,
'Together',
forcedTools,
usedForcedTools
)
}
+4 -9
View File
@@ -16,9 +16,7 @@ import { providersUtilsMock, providersUtilsMockFns } from '@sim/testing/mocks/pr
import { toolsMock, toolsMockFns } from '@sim/testing/mocks/tools.mock'
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
const { mockCheckForced, mockCreateStream, pinnedFetchFn } = vi.hoisted(() => ({
mockCheckForced: vi.fn(),
mockCreateStream: vi.fn(),
const { pinnedFetchFn } = vi.hoisted(() => ({
pinnedFetchFn: vi.fn(),
}))
@@ -31,10 +29,7 @@ vi.mock('@/providers/models', () => providersModelsMock)
vi.mock('@/providers/attachments', () => providersAttachmentsMock)
vi.mock('@/providers/trace-enrichment', () => providersTraceEnrichmentMock)
vi.mock('@/providers/utils', () => providersUtilsMock)
vi.mock('@/providers/vllm/utils', () => ({
checkForForcedToolUsage: mockCheckForced,
createReadableStreamFromVLLMStream: mockCreateStream,
}))
vi.mock('@/tools', () => toolsMock)
vi.mock('@/stores/providers', () => ({
useProvidersStore: { getState: () => ({ setProviderModels: vi.fn() }) },
@@ -45,6 +40,7 @@ import type { AgentStreamEvent } from '@/providers/stream-events'
import type { ProviderToolConfig } from '@/providers/types'
import { vllmProvider } from '@/providers/vllm/index'
const mockCheckForced = providersUtilsMockFns.mockCheckForForcedToolUsageOpenAI
const mockCreate = openaiMockFns.mockChatCompletionsCreate
const mockCapture = providersConversationHistoryMockFns.mockCaptureProviderConversationStep
const mockRecordUsage = providersConversationHistoryMockFns.mockRecordProviderConversationUsage
@@ -119,7 +115,6 @@ describe('vllmProvider', () => {
hasFilteredTools: false,
})
mockCheckForced.mockReturnValue({ hasUsedForcedTool: false, usedForcedTools: [] })
mockCreateStream.mockReturnValue(new ReadableStream({ start: (c) => c.close() }))
mockExecuteTool.mockResolvedValue({ success: true, output: { result: 'ok' } })
mockValidateUrlWithDNS.mockResolvedValue({ isValid: true, resolvedIP: '203.0.113.10' })
mockCreatePinnedFetch.mockReturnValue(pinnedFetchFn)
@@ -445,7 +440,7 @@ describe('vllmProvider', () => {
})
it('streams directly when there are no tools, requesting usage in the stream', async () => {
mockCreate.mockResolvedValueOnce({})
mockCreate.mockResolvedValueOnce((async function* () {})())
const result = await vllmProvider.executeRequest({
model: 'vllm/llama-3',
+11 -8
View File
@@ -22,6 +22,7 @@ import { getProviderDefaultModel, getProviderModels } from '@/providers/models'
import { createOpenAICompatAssistantHistory } from '@/providers/openai-compat/assistant-history'
import { getOpenAICompatibleApiBaseUrl } from '@/providers/openai-compat/base-url'
import { getChatCompletionConversationUsage } from '@/providers/openai-compat/conversation-usage'
import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events'
import { executeProviderTool } from '@/providers/runtime-context'
import { createSettledAgentEventStream } from '@/providers/stream-events'
import { createStreamingExecution } from '@/providers/streaming-execution'
@@ -39,12 +40,12 @@ import type {
import { ProviderError } from '@/providers/types'
import {
calculateCost,
checkForForcedToolUsageOpenAI,
isFunctionToolCall,
prepareToolExecution,
prepareToolsWithUsageControl,
sumToolCosts,
} from '@/providers/utils'
import { checkForForcedToolUsage, createReadableStreamFromVLLMStream } from '@/providers/vllm/utils'
import { useProvidersStore } from '@/stores/providers'
const logger = createLogger('VLLMProvider')
@@ -262,9 +263,10 @@ export const vllmProvider: ProviderConfig = {
initialCost: { input: 0, output: 0, total: 0 },
streamFormat: 'agent-events-v1',
createStream: ({ output, finalizeTiming }) =>
createReadableStreamFromVLLMStream(
streamResponse,
(content, usage) => {
createOpenAICompatibleAgentEventStream(streamResponse, {
providerName: 'vLLM',
request,
onComplete: ({ content, usage }) => {
let cleanContent = content
if (cleanContent && request.responseFormat) {
cleanContent = cleanContent.replace(/```json\n?|\n?```/g, '').trim()
@@ -290,8 +292,7 @@ export const vllmProvider: ProviderConfig = {
finalizeTiming()
},
request
),
}),
})
return streamingResult
@@ -349,9 +350,10 @@ export const vllmProvider: ProviderConfig = {
]
if (originalToolChoice) {
const forcedResult = checkForForcedToolUsage(
const forcedResult = checkForForcedToolUsageOpenAI(
currentResponse,
originalToolChoice,
'vLLM',
forcedTools,
usedForcedTools
)
@@ -582,9 +584,10 @@ export const vllmProvider: ProviderConfig = {
}
if (nextPayload.tool_choice && typeof nextPayload.tool_choice === 'object') {
const forcedResult = checkForForcedToolUsage(
const forcedResult = checkForForcedToolUsageOpenAI(
currentResponse,
nextPayload.tool_choice,
'vLLM',
forcedTools,
usedForcedTools
)
-37
View File
@@ -1,37 +0,0 @@
import type { ChatCompletionChunk } from 'openai/resources/chat/completions'
import type { CompletionUsage } from 'openai/resources/completions'
import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events'
import type { AgentStreamEvent } from '@/providers/stream-events'
import type { ProviderRequest } from '@/providers/types'
import { checkForForcedToolUsageOpenAI } from '@/providers/utils'
/**
* Creates an agent-events stream from a vLLM streaming response.
* Uses the shared OpenAI-compatible agent event streaming utility.
*/
export function createReadableStreamFromVLLMStream(
vllmStream: AsyncIterable<ChatCompletionChunk>,
onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void,
request?: ProviderRequest
): ReadableStream<AgentStreamEvent> {
return createOpenAICompatibleAgentEventStream(vllmStream, {
request,
providerName: 'vLLM',
onComplete: onComplete
? (result) => onComplete(result.content, result.usage, result.thinking)
: undefined,
})
}
/**
* Checks if a forced tool was used in a vLLM response.
* Uses the shared OpenAI-compatible forced tool usage helper.
*/
export function checkForForcedToolUsage(
response: any,
toolChoice: string | { type: string; function?: { name: string }; name?: string; any?: any },
forcedTools: string[],
usedForcedTools: string[]
): { hasUsedForcedTool: boolean; usedForcedTools: string[] } {
return checkForForcedToolUsageOpenAI(response, toolChoice, 'vLLM', forcedTools, usedForcedTools)
}
+12 -12
View File
@@ -18,6 +18,7 @@ import {
import { getProviderDefaultModel, getProviderModels } from '@/providers/models'
import { createOpenAICompatAssistantHistory } from '@/providers/openai-compat/assistant-history'
import { getChatCompletionConversationUsage } from '@/providers/openai-compat/conversation-usage'
import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events'
import { executeProviderTool } from '@/providers/runtime-context'
import { createSettledAgentEventStream } from '@/providers/stream-events'
import { createStreamingExecution } from '@/providers/streaming-execution'
@@ -35,16 +36,13 @@ import type {
import { ProviderError } from '@/providers/types'
import {
calculateCost,
checkForForcedToolUsageOpenAI,
isFunctionToolCall,
prepareToolExecution,
prepareToolsWithUsageControl,
sumToolCosts,
} from '@/providers/utils'
import {
checkForForcedToolUsage,
createReadableStreamFromXAIStream,
createResponseFormatPayload,
} from '@/providers/xai/utils'
import { createResponseFormatPayload } from '@/providers/xai/utils'
const logger = createLogger('XAIProvider')
@@ -166,9 +164,10 @@ export const xAIProvider: ProviderConfig = {
isStreaming: true,
streamFormat: 'agent-events-v1',
createStream: ({ output }) =>
createReadableStreamFromXAIStream(
streamResponse,
(content, usage) => {
createOpenAICompatibleAgentEventStream(streamResponse, {
providerName: 'xAI',
request,
onComplete: ({ content, usage }) => {
output.content = content
output.tokens = {
input: usage.prompt_tokens,
@@ -187,8 +186,7 @@ export const xAIProvider: ProviderConfig = {
total: costResult.total,
}
},
request
),
}),
})
return streamingResult
@@ -257,9 +255,10 @@ export const xAIProvider: ProviderConfig = {
},
]
if (originalToolChoice) {
const result = checkForForcedToolUsage(
const result = checkForForcedToolUsageOpenAI(
currentResponse,
originalToolChoice,
'xAI',
forcedTools,
usedForcedTools
)
@@ -521,9 +520,10 @@ export const xAIProvider: ProviderConfig = {
)
}
if (nextPayload.tool_choice && typeof nextPayload.tool_choice === 'object') {
const result = checkForForcedToolUsage(
const result = checkForForcedToolUsageOpenAI(
currentResponse,
nextPayload.tool_choice,
'xAI',
forcedTools,
usedForcedTools
)
-38
View File
@@ -1,28 +1,3 @@
import type { ChatCompletionChunk } from 'openai/resources/chat/completions'
import type { CompletionUsage } from 'openai/resources/completions'
import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events'
import type { AgentStreamEvent } from '@/providers/stream-events'
import type { ProviderRequest } from '@/providers/types'
import { checkForForcedToolUsageOpenAI } from '@/providers/utils'
/**
* Creates an agent-events stream from an xAI streaming response.
* Uses the shared OpenAI-compatible agent event streaming utility.
*/
export function createReadableStreamFromXAIStream(
xaiStream: AsyncIterable<ChatCompletionChunk>,
onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void,
request?: ProviderRequest
): ReadableStream<AgentStreamEvent> {
return createOpenAICompatibleAgentEventStream(xaiStream, {
request,
providerName: 'xAI',
onComplete: onComplete
? (result) => onComplete(result.content, result.usage, result.thinking)
: undefined,
})
}
/**
* Creates a response format payload for xAI requests with JSON schema.
*/
@@ -50,16 +25,3 @@ export function createResponseFormatPayload(
return payload
}
/**
* Checks if a forced tool was used in an xAI response.
* Uses the shared OpenAI-compatible forced tool usage helper.
*/
export function checkForForcedToolUsage(
response: any,
toolChoice: string | { type: string; function?: { name: string }; name?: string; any?: any },
forcedTools: string[],
usedForcedTools: string[]
): { hasUsedForcedTool: boolean; usedForcedTools: string[] } {
return checkForForcedToolUsageOpenAI(response, toolChoice, 'xAI', forcedTools, usedForcedTools)
}
+24 -21
View File
@@ -17,6 +17,7 @@ import {
import { getProviderDefaultModel, getProviderModels } from '@/providers/models'
import { createOpenAICompatAssistantHistory } from '@/providers/openai-compat/assistant-history'
import { getChatCompletionConversationUsage } from '@/providers/openai-compat/conversation-usage'
import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events'
import { executeProviderTool } from '@/providers/runtime-context'
import { createSettledAgentEventStream } from '@/providers/stream-events'
import { createStreamingExecution } from '@/providers/streaming-execution'
@@ -38,7 +39,6 @@ import {
prepareToolsWithUsageControl,
sumToolCosts,
} from '@/providers/utils'
import { createReadableStreamFromZaiStream } from '@/providers/zai/utils'
const logger = createLogger('ZaiProvider')
@@ -202,29 +202,32 @@ export const zaiProvider: ProviderConfig = {
isStreaming: true,
streamFormat: 'agent-events-v1',
createStream: ({ output }) =>
createReadableStreamFromZaiStream(
createOpenAICompatibleAgentEventStream(
// double-cast-allowed: payload is untyped so the SDK cannot resolve the streaming overload; the stream yields OpenAI ChatCompletionChunk objects
streamResponse as unknown as AsyncIterable<ChatCompletionChunk>,
(content, usage) => {
output.content = content
output.tokens = {
input: usage.prompt_tokens,
output: usage.completion_tokens,
total: usage.total_tokens,
}
{
providerName: 'Z.ai',
request,
onComplete: ({ content, usage }) => {
output.content = content
output.tokens = {
input: usage.prompt_tokens,
output: usage.completion_tokens,
total: usage.total_tokens,
}
const costResult = calculateCost(
request.model,
usage.prompt_tokens,
usage.completion_tokens
)
output.cost = {
input: costResult.input,
output: costResult.output,
total: costResult.total,
}
},
request
const costResult = calculateCost(
request.model,
usage.prompt_tokens,
usage.completion_tokens
)
output.cost = {
input: costResult.input,
output: costResult.output,
total: costResult.total,
}
},
}
),
})
-23
View File
@@ -1,23 +0,0 @@
import type { ChatCompletionChunk } from 'openai/resources/chat/completions'
import type { CompletionUsage } from 'openai/resources/completions'
import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events'
import type { AgentStreamEvent } from '@/providers/stream-events'
import type { ProviderRequest } from '@/providers/types'
/**
* Creates an agent-events stream from a Z.ai streaming response.
* Uses the shared OpenAI-compatible agent event streaming utility.
*/
export function createReadableStreamFromZaiStream(
zaiStream: AsyncIterable<ChatCompletionChunk>,
onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void,
request?: ProviderRequest
): ReadableStream<AgentStreamEvent> {
return createOpenAICompatibleAgentEventStream(zaiStream, {
request,
providerName: 'Z.ai',
onComplete: onComplete
? (result) => onComplete(result.content, result.usage, result.thinking)
: undefined,
})
}
+385 -21
View File
@@ -21,8 +21,10 @@
"gray-matter": "4.0.3",
"husky": "9.1.7",
"json-schema-to-typescript": "15.0.4",
"knip": "6.37.0",
"lint-staged": "16.0.0",
"opentype.js": "1.3.4",
"pdf-lib": "1.17.1",
"react": "19.2.4",
"remark-gfm": "4.0.1",
"remark-parse": "11.0.0",
@@ -263,6 +265,8 @@
"@tiptap/extension-code-block": "3.30.5",
"@tiptap/extension-collaboration": "3.30.5",
"@tiptap/extension-collaboration-caret": "3.30.5",
"@tiptap/extension-document": "3.30.5",
"@tiptap/extension-hard-break": "3.30.5",
"@tiptap/extension-image": "3.30.5",
"@tiptap/extension-list": "3.30.5",
"@tiptap/extension-paragraph": "3.30.5",
@@ -385,6 +389,7 @@
},
"devDependencies": {
"@next/env": "16.3.4",
"@opentelemetry/context-async-hooks": "2.10.0",
"@sim/testing": "workspace:*",
"@sim/tsconfig": "workspace:*",
"@tailwindcss/postcss": "^4.3.3",
@@ -393,6 +398,7 @@
"@trigger.dev/build": "4.5.16",
"@types/archiver": "8.0.0",
"@types/busboy": "1.5.4",
"@types/hast": "3.0.4",
"@types/heic-convert": "2.1.1",
"@types/html-to-text": "9.0.4",
"@types/js-yaml": "4.0.9",
@@ -1239,11 +1245,11 @@
"@electron/windows-sign": ["@electron/windows-sign@1.2.2", "", { "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", "fs-extra": "^11.1.1", "minimist": "^1.2.8", "postject": "^1.0.0-alpha.6" }, "bin": { "electron-windows-sign": "bin/electron-windows-sign.js" } }, "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ=="],
"@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
"@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="],
"@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
"@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="],
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
"@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="],
@@ -1551,7 +1557,7 @@
"@napi-rs/canvas-win32-x64-msvc": ["@napi-rs/canvas-win32-x64-msvc@0.1.100", "", { "os": "win32", "cpu": "x64" }, "sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.5", "", { "dependencies": { "@tybys/wasm-util": "^0.10.2" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g=="],
"@next/env": ["@next/env@16.3.4", "", {}, "sha512-cjWZnUUa6jZq2kFaNe/ZyJdZonOZ/QoN0Zka2nz/FLOrfx14pQuM9c5RaSVkWMqgdt4ksgPAMWPyHSs/CyV48Q=="],
@@ -1679,7 +1685,83 @@
"@orama/orama": ["@orama/orama@3.1.18", "", {}, "sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA=="],
"@oxc-project/types": ["@oxc-project/types@0.133.0", "", {}, "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA=="],
"@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.150.0", "", { "os": "android", "cpu": "arm" }, "sha512-oQef2Zu4Prz1KLKznz3HqZzU9uVoA5PMoDZuuLmqms7hKmKSAPzlaMnLllJq3t+rgKmfJJ2siPrpZfFrW06btw=="],
"@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.150.0", "", { "os": "android", "cpu": "arm64" }, "sha512-B6ofpoFiAUwIZ0MJ2IgHPvZK8FAtL4qzSZRwOkAKIfxEobE1mQC8nDdiFZj7pUaJiVUVCsfkMsBJKedzO8rZvA=="],
"@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.150.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-J+9IHKzx/bSz1JetOfD4zKXSK9sOm4/a7+0qomJODcTL6JsRXGeV/ZdkAPSIDydfFmWzYCraMlQEosSZEyBYkQ=="],
"@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.150.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-v6IPfcAcSYrBWXV5Tce1DmxDMXLhEYpIIWiRFPJopgCVscZdLaV4MRQOUxvL3usQlw+yrwYOjBFB9IwBx+jUiQ=="],
"@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.150.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-AoR/4jD02HET0KO0yNT4nbTE+XJUxiO9jB1X/ycEjUE3WrIJ3IjV/Vf+gskhWLcqqeXfvNnkDtBR7epllAm88A=="],
"@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.150.0", "", { "os": "linux", "cpu": "arm" }, "sha512-A/hycCFjLUrCmLoL5O/vBp40ohlaXO1Ta3v5hqYZxicYFs129wZmgZJggcW4mYGYbZebQLhup+N8JjeQl8qwyw=="],
"@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.150.0", "", { "os": "linux", "cpu": "arm" }, "sha512-pbqahg1Pkz7J4RKmXm30s/iQu99hv64ayXCu8P4p75HcEH5azOdR4eGqiB6lFGkFR3mMpzaYsSKLkS8oJbjmeQ=="],
"@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.150.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-HV11aRbBQGwqv8bEo+K/6qr88uB4fEe1mhXncMhlVCrj+WpBSraxrUzHaPVmeQRU6x6x8v/3DuCbbo93rpp+fw=="],
"@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.150.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-k6pVkJqALwtEuP4zukVhGRhdIy4+ofChUIUUsAHHyqDZlNsknmel3JjbggrJiWGaes6h/dIcWmEXsKW4SmK9oQ=="],
"@oxc-parser/binding-linux-ppc64-gnu": ["@oxc-parser/binding-linux-ppc64-gnu@0.150.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tEFg39mw/rHO5n8GK2DR4ZMFfsPQZtnQIPbsIpjoQRomJc/2tRfsCSmCT6gNit+NZGoeJG5aH9ATDH5bf0WnoQ=="],
"@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.150.0", "", { "os": "linux", "cpu": "none" }, "sha512-0JO7IFkoek6HqV589Menx3hJySNXi6quRhO4tq2P7kpEHKEXoDATnoPVUpn5B7gDH2KLkdo751N0uIrGJFCTCw=="],
"@oxc-parser/binding-linux-riscv64-musl": ["@oxc-parser/binding-linux-riscv64-musl@0.150.0", "", { "os": "linux", "cpu": "none" }, "sha512-7XPzREnyAS5wHU9aB+49Uaqgoo1gVCklHc3DRlc3fJy2tXD/eAJFN1QFz/crj+Ulup5P1+axNREF+/RGaB/IRA=="],
"@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.150.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-7n7ZxcbRWDFaTdyj8M8p0O9OfzoMKm8O6syBAIgcSutbOALq0Bb9G52Me+XH0anMLZV+NgXc726gqgG2q39vcQ=="],
"@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.150.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Vx0GSA9ZCRTUiczoEQnIIyXkMvtEY8uc6IiwLQtMe5ci2sXPqjrry0Ek5fsPP1k2kCzkML3ow9UOOEgnEDi7Bw=="],
"@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.150.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ii4/9m3viDLssMnfXU7/Pni/3nYplApuGayceX4qsVGaqqQJCx3tHIH9M/HWIeSty5i8LjS21zPYT6lVIkwLxw=="],
"@oxc-parser/binding-openharmony-arm64": ["@oxc-parser/binding-openharmony-arm64@0.150.0", "", { "os": "none", "cpu": "arm64" }, "sha512-ZtoxX5rez36ZWkwtiEhjkCPnlTPoQ2I7lIL0IYZ1yjxMYohbvoOjBmUIZzrf9W/HouONq0omIfTeCWEY+R380w=="],
"@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.150.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-VqeRb5JX/bKYBrff7TUDvJyKY0914UzuCkuXIGxJS4FUmvMNfqkCbc7Sq90iMz9DlmAtlggwaBYQ9eg3h3ltiw=="],
"@oxc-parser/binding-win32-ia32-msvc": ["@oxc-parser/binding-win32-ia32-msvc@0.150.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-EPqJfeZ4Pgg2BJSsCV8GozxV0FDltRzpVtGVa1r2noJu1iu+opOw3gtBWXwIo9odCT/MdFfyHxdy0/CRqs3OqA=="],
"@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.150.0", "", { "os": "win32", "cpu": "x64" }, "sha512-n5YMzbqwPQqozbkrexi0lNZrUz5cnPHalYpFWe6Dau7AmKIWNo+y24IwzDuZEQtEdUHuKhXmMMih/MPjOI+CMw=="],
"@oxc-project/types": ["@oxc-project/types@0.150.0", "", {}, "sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw=="],
"@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.24.2", "", { "os": "android", "cpu": "arm" }, "sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA=="],
"@oxc-resolver/binding-android-arm64": ["@oxc-resolver/binding-android-arm64@11.24.2", "", { "os": "android", "cpu": "arm64" }, "sha512-cl4icWaZFnLdg8m6qtnh5rBMuGbxc/ptStFHLeCNwr+2cZjkjNwQu/jYRS0CHlnPecOJMpuS5M6/BH+0J/YkEg=="],
"@oxc-resolver/binding-darwin-arm64": ["@oxc-resolver/binding-darwin-arm64@11.24.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-At29QEMF6HajbQvgY8K6OXnHD1x9rad74xBEfmCB6ZqCGsdq75aK7tOYcTbOanMy8qdIBrfL3SMr3p/lfSlb9w=="],
"@oxc-resolver/binding-darwin-x64": ["@oxc-resolver/binding-darwin-x64@11.24.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-A5Kqr1EUj4oIL5CF4WRssq/o5P0Y11cwoFouMRmQ7YnC/A8V93nv1nb7aSU8HwcgmXropjLNkVTl4MN87cu28Q=="],
"@oxc-resolver/binding-freebsd-x64": ["@oxc-resolver/binding-freebsd-x64@11.24.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-R5xkRBRRz7ceH/P5Jrc6G7FmdUdgpLYyESFAUDVTNQ9K0sGPxcp4ljiwEwEqsvNcQ4sYbMRrWcHHBCu7ksAJVw=="],
"@oxc-resolver/binding-linux-arm-gnueabihf": ["@oxc-resolver/binding-linux-arm-gnueabihf@11.24.2", "", { "os": "linux", "cpu": "arm" }, "sha512-k/RuYL4L/R58IBn3wT5ma3Wh4k62bp1eYCFRWCmMsasUOqL+H6sW0VGFadEzKWXFFlz+2uIMoeMk9ySSZJHgbg=="],
"@oxc-resolver/binding-linux-arm-musleabihf": ["@oxc-resolver/binding-linux-arm-musleabihf@11.24.2", "", { "os": "linux", "cpu": "arm" }, "sha512-bnHAak3ujYfH5pKk4NieFNbvYvernfoQDgwLddbZ3OtMYrem87/qjlA+u+aKG0oZcqSLGCful/6/CEA+aeAgaA=="],
"@oxc-resolver/binding-linux-arm64-gnu": ["@oxc-resolver/binding-linux-arm64-gnu@11.24.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-vDT3KHgzYp47gmtNOqL2VNhCyl5Zv643eyxm//A68J8DeUGXrvD1pZFiaT4jSfe+RInfnn1R2yVHye4enx6RnA=="],
"@oxc-resolver/binding-linux-arm64-musl": ["@oxc-resolver/binding-linux-arm64-musl@11.24.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-+kMlQvbzfyEYtu5FcjE4p+ttBLpKW4d/AsAsuE69BxV6V4twZJeIQZFfD8gh/wqglY0MkPSezWXQH0jBV13MUw=="],
"@oxc-resolver/binding-linux-ppc64-gnu": ["@oxc-resolver/binding-linux-ppc64-gnu@11.24.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-shjfMhmZ3gq9fv/w7bi3PnZlgOPG+2QAOFf0BJF0EgBSIGZ6PMLN2zbGEblTUYB/NKVDRyYhE2ff3dJ1QqNPkA=="],
"@oxc-resolver/binding-linux-riscv64-gnu": ["@oxc-resolver/binding-linux-riscv64-gnu@11.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-zGelwFR5oRo+b69k8Lrzun86DyUHzfKN6cnjbR9l7Z7NIRznOE/2ZvPa1IUKqAL2PzAXOdwkfVqNvO1H2RlpAw=="],
"@oxc-resolver/binding-linux-riscv64-musl": ["@oxc-resolver/binding-linux-riscv64-musl@11.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-qxZ1SWCXJY0eyhAlP6Lmo9F2Nrtx7EkYj9oCgL8apDPCwXwCEDA2U697bbT81JIc2IrVjxO4KX6WU2N+oN9Z4w=="],
"@oxc-resolver/binding-linux-s390x-gnu": ["@oxc-resolver/binding-linux-s390x-gnu@11.24.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-sGCecF3cx2DFlH4t/z7ApnOnXqN48p5p5mlHDEnHTAukQa2P+qMVE4CwyWE9W+q/m3QJ7kKfGrIjax31f44oFQ=="],
"@oxc-resolver/binding-linux-x64-gnu": ["@oxc-resolver/binding-linux-x64-gnu@11.24.2", "", { "os": "linux", "cpu": "x64" }, "sha512-k/VlMMcSzMlahb3/fENM4rTlsJ0s3fFROA0KXPBmKggqmTSaE383sl8F3KCOXPLmVsYfW6hCitMhXCEtNeZxxg=="],
"@oxc-resolver/binding-linux-x64-musl": ["@oxc-resolver/binding-linux-x64-musl@11.24.2", "", { "os": "linux", "cpu": "x64" }, "sha512-8hbnZyNi97b/8wapYaIF9+t9GmZKBW2vunaOc3h9HGJptH7b7XpvZqOTBSm/MpTjr7H497BlgOaSfLUdhmy2bw=="],
"@oxc-resolver/binding-openharmony-arm64": ["@oxc-resolver/binding-openharmony-arm64@11.24.2", "", { "os": "none", "cpu": "arm64" }, "sha512-MvyGik3a6pVgZ0t/kWlbmFxFLmXQJwgLsY2eYFHLpy0wGwRbfzeIGgDwQ3kXqE30z+kSXennRkCrT7TUvkptNg=="],
"@oxc-resolver/binding-wasm32-wasi": ["@oxc-resolver/binding-wasm32-wasi@11.24.2", "", { "dependencies": { "@emnapi/core": "1.11.2", "@emnapi/runtime": "1.11.2", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-vHcssMPwO08RTvj/c0iOBz90attxyG3wQJ0dTcyEQK43LRpcdLWZlV5feBhv6Isn6ahbQIzHbCgfa81+RiML0Q=="],
"@oxc-resolver/binding-win32-arm64-msvc": ["@oxc-resolver/binding-win32-arm64-msvc@11.24.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-uokJqro2iBqkFvJdKQLP7d8/BUmFwESQFVmIJUQKj1Xn1a/LysJoe1vmeECLF5b3jsV8CAL5sEMJXX6SdK9Nhg=="],
"@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.24.2", "", { "os": "win32", "cpu": "x64" }, "sha512-UqGPmo56KDfLlfXFAFIrNflHT8tFxWGEivWg3Zeyp4Uy2NlKN1FGPr6/BxcLGG3+kZ6Wp14g5Uj+n71boqZfiw=="],
"@pdf-lib/standard-fonts": ["@pdf-lib/standard-fonts@1.0.0", "", { "dependencies": { "pako": "^1.0.6" } }, "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA=="],
@@ -2165,7 +2247,7 @@
"@turbo/windows-arm64": ["@turbo/windows-arm64@2.9.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-fVdCsnmYoKICsycbWuuGp6Jvi51/3G/UluFWuAUCvR8PIW5IJkAk5BM9UF8PSm0Q2IphWHFZjYEgjHsh3B9y/g=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.4", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A=="],
"@types/archiver": ["@types/archiver@8.0.0", "", { "dependencies": { "@types/node": "*", "@types/readdir-glob": "*" } }, "sha512-YpXPbEuv9+eUIPPQWUPahj3cvs9isWRuF+J4z+KbdYVDO3rWorWQFxUVHnwPu2AgKwvgpki5F2VMX0Xx+mX45A=="],
@@ -3135,6 +3217,8 @@
"fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
"fd-package-json": ["fd-package-json@2.0.0", "", { "dependencies": { "walk-up-path": "^4.0.0" } }, "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ=="],
"fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
@@ -3165,6 +3249,8 @@
"form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="],
"formatly": ["formatly@0.7.1", "", { "dependencies": { "fd-package-json": "^2.0.0", "package-manager-detector": "^1.8.0" }, "bin": { "formatly": "bin/index.mjs" } }, "sha512-ESuCEJE0SGgabl9q+zABI8LhJDmF+8nhpuqDzuM48HfJ9QTD5o6PqFJDFZY7w6f8xsh5lixuj5nJsQNvYOWvKQ=="],
"formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="],
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
@@ -3213,7 +3299,7 @@
"get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="],
"get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="],
"get-tsconfig": ["get-tsconfig@4.14.3", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA=="],
"giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="],
@@ -3433,7 +3519,7 @@
"jake": ["jake@10.9.4", "", { "dependencies": { "async": "^3.2.6", "filelist": "^1.0.4", "picocolors": "^1.1.1" }, "bin": { "jake": "bin/cli.js" } }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="],
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
"jmespath": ["jmespath@0.16.0", "", {}, "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw=="],
@@ -3501,6 +3587,8 @@
"kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
"knip": ["knip@6.37.0", "", { "dependencies": { "fdir": "^6.5.0", "formatly": "^0.7.0", "get-tsconfig": "4.14.3", "jiti": "^2.7.0", "oxc-parser": "^0.150.0", "oxc-resolver": "11.24.2", "picomatch": "^4.0.7", "smol-toml": "^1.8.0", "strip-json-comments": "5.0.3", "tinyglobby": "^0.2.17", "unbash": "^4.0.11", "yaml": "^2.9.1", "zod": "^4.4.3" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-NKCYuoGKJ+o2AuoCuV1mDvfMYnSSWfDXqxwmzxeHyd8fftjNRl8TBUg3fbSrtJjhm7Vj6sc1PX3WZnZhECs9EQ=="],
"kysely": ["kysely@0.28.17", "", {}, "sha512-nbD8lB9EB3wNdMhOCdx5Li8DxnLbvKByylRLcJ1h+4SkrowVeECAyZlyiKMThF7xFdRz0jSQ2MoJr+wXux2y0Q=="],
"layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="],
@@ -3917,6 +4005,10 @@
"orderedmap": ["orderedmap@2.1.1", "", {}, "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g=="],
"oxc-parser": ["oxc-parser@0.150.0", "", { "dependencies": { "@oxc-project/types": "^0.150.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.150.0", "@oxc-parser/binding-android-arm64": "0.150.0", "@oxc-parser/binding-darwin-arm64": "0.150.0", "@oxc-parser/binding-darwin-x64": "0.150.0", "@oxc-parser/binding-freebsd-x64": "0.150.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.150.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.150.0", "@oxc-parser/binding-linux-arm64-gnu": "0.150.0", "@oxc-parser/binding-linux-arm64-musl": "0.150.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.150.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.150.0", "@oxc-parser/binding-linux-riscv64-musl": "0.150.0", "@oxc-parser/binding-linux-s390x-gnu": "0.150.0", "@oxc-parser/binding-linux-x64-gnu": "0.150.0", "@oxc-parser/binding-linux-x64-musl": "0.150.0", "@oxc-parser/binding-openharmony-arm64": "0.150.0", "@oxc-parser/binding-win32-arm64-msvc": "0.150.0", "@oxc-parser/binding-win32-ia32-msvc": "0.150.0", "@oxc-parser/binding-win32-x64-msvc": "0.150.0" } }, "sha512-zwajKw1GUa57IOdafIS7/yOObGF2NihwIfZfs5yJTRprMSkiwhWOc/k+n8bwnUPbHr2/XsjfBO8KyTPaW3VpWg=="],
"oxc-resolver": ["oxc-resolver@11.24.2", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.24.2", "@oxc-resolver/binding-android-arm64": "11.24.2", "@oxc-resolver/binding-darwin-arm64": "11.24.2", "@oxc-resolver/binding-darwin-x64": "11.24.2", "@oxc-resolver/binding-freebsd-x64": "11.24.2", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.24.2", "@oxc-resolver/binding-linux-arm-musleabihf": "11.24.2", "@oxc-resolver/binding-linux-arm64-gnu": "11.24.2", "@oxc-resolver/binding-linux-arm64-musl": "11.24.2", "@oxc-resolver/binding-linux-ppc64-gnu": "11.24.2", "@oxc-resolver/binding-linux-riscv64-gnu": "11.24.2", "@oxc-resolver/binding-linux-riscv64-musl": "11.24.2", "@oxc-resolver/binding-linux-s390x-gnu": "11.24.2", "@oxc-resolver/binding-linux-x64-gnu": "11.24.2", "@oxc-resolver/binding-linux-x64-musl": "11.24.2", "@oxc-resolver/binding-openharmony-arm64": "11.24.2", "@oxc-resolver/binding-wasm32-wasi": "11.24.2", "@oxc-resolver/binding-win32-arm64-msvc": "11.24.2", "@oxc-resolver/binding-win32-x64-msvc": "11.24.2" } }, "sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw=="],
"p-cancelable": ["p-cancelable@4.0.1", "", {}, "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg=="],
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
@@ -3925,7 +4017,7 @@
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
"package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="],
"package-manager-detector": ["package-manager-detector@1.8.0", "", {}, "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A=="],
"pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
@@ -4345,6 +4437,8 @@
"smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
"smol-toml": ["smol-toml@1.8.0", "", {}, "sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ=="],
"socket.io": ["socket.io@4.8.3", "", { "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.4.1", "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" } }, "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A=="],
"socket.io-adapter": ["socket.io-adapter@2.5.7", "", { "dependencies": { "debug": "~4.4.1", "ws": "~8.20.1" } }, "sha512-e0LyK91f3cUxTmv95/KzoLg47+zF+s/sbxRGDNsyG4dmIP8ZSX8ax6byOxfJXeNNtS/8AZlfD+uP7gBeR7DLlg=="],
@@ -4427,7 +4521,7 @@
"strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="],
"strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
"strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="],
"stripe": ["stripe@18.5.0", "", { "dependencies": { "qs": "^6.11.0" }, "peerDependencies": { "@types/node": ">=12.x.x" }, "optionalPeers": ["@types/node"] }, "sha512-Hp+wFiEQtCB0LlNgcFh5uVyKznpDjzyUZ+CNVEf+I3fhlYvh7rZruIg+jOwzJRCpy0ZTPMjlzm7J2/M2N6d+DA=="],
@@ -4543,7 +4637,7 @@
"tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="],
"tsx": ["tsx@4.22.4", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg=="],
@@ -4571,6 +4665,8 @@
"uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
"unbash": ["unbash@4.0.11", "", {}, "sha512-FoSOKV7NEofQSkAefMVHam4ZPKYMxjAydxiV72UFEDNV/YofxjGfiZ2A9pZjdL/lRJzTjcu4PABo1JYJX8N5iQ=="],
"uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
"underscore": ["underscore@1.13.8", "", {}, "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ=="],
@@ -4653,6 +4749,8 @@
"walk-sync": ["walk-sync@0.2.7", "", { "dependencies": { "ensure-posix-path": "^1.0.0", "matcher-collection": "^1.0.0" } }, "sha512-OH8GdRMowEFr0XSHQeX5fGweO6zSVHo7bG/0yJQx6LAj9Oukz0C8heI3/FYectT66gY0IPGe89kOvU410/UNpg=="],
"walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="],
"warning": ["warning@4.0.3", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w=="],
"wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="],
@@ -4727,7 +4825,7 @@
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
"yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
"yaml": ["yaml@2.9.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw=="],
"yaml-js": ["yaml-js@0.3.1", "", {}, "sha512-LjoIFHCtSfkGzPsmYmfDsW+TShtQBcY7lwH1yLQ5brJHXRhjteUnVE2ThCbz1madq8JUZIAjFiavfnIFeTO8CQ=="],
@@ -4765,6 +4863,8 @@
"@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@antfu/install-pkg/package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="],
"@antfu/install-pkg/tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="],
"@asamuzakjp/css-color/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
@@ -4773,28 +4873,190 @@
"@aws-crypto/crc32/@aws-sdk/types": ["@aws-sdk/types@3.973.13", "", { "dependencies": { "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-pEHZqRkAlHfnfAU9tK+WpKv/gBNjGJrHMgA3A0iYRGyswBS2t0pfez+lWlwktb3Bqa0ovh7w/QJTFwp3fDxLNg=="],
"@aws-crypto/crc32/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-crypto/sha256-browser/@aws-sdk/types": ["@aws-sdk/types@3.973.13", "", { "dependencies": { "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-pEHZqRkAlHfnfAU9tK+WpKv/gBNjGJrHMgA3A0iYRGyswBS2t0pfez+lWlwktb3Bqa0ovh7w/QJTFwp3fDxLNg=="],
"@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
"@aws-crypto/sha256-browser/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-crypto/sha256-js/@aws-sdk/types": ["@aws-sdk/types@3.973.13", "", { "dependencies": { "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-pEHZqRkAlHfnfAU9tK+WpKv/gBNjGJrHMgA3A0iYRGyswBS2t0pfez+lWlwktb3Bqa0ovh7w/QJTFwp3fDxLNg=="],
"@aws-crypto/sha256-js/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-crypto/supports-web-crypto/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-crypto/util/@aws-sdk/types": ["@aws-sdk/types@3.973.13", "", { "dependencies": { "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-pEHZqRkAlHfnfAU9tK+WpKv/gBNjGJrHMgA3A0iYRGyswBS2t0pfez+lWlwktb3Bqa0ovh7w/QJTFwp3fDxLNg=="],
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
"@aws-crypto/util/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/checksums/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/client-appconfig/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/client-appconfigdata/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/client-athena/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/client-bedrock-runtime/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/client-cloudformation/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/client-cloudtrail/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/client-cloudwatch/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/client-cloudwatch-logs/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/client-codepipeline/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/client-dynamodb/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/client-ec2/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/client-ecs/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/client-iam/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/client-identitystore/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/client-lambda/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/client-organizations/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/client-rds-data/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/client-s3/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/client-secrets-manager/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/client-sesv2/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/client-sqs/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/client-ssm/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/client-sso-admin/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/client-sts/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/client-textract/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.7.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow=="],
"@aws-sdk/core/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/credential-provider-cognito-identity/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/credential-provider-env/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/credential-provider-http/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/credential-provider-ini/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/credential-provider-login/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/credential-provider-node/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/credential-provider-process/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1116.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/nested-clients": "^3.997.44", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-ygIivKqh8aHzNkucOCXHyIBgBpLPfrSI0mCqXF+vLBsPTUKqj0VSqAY0GFPe7lQl4HntjOcQ+KSyS7oUV2C54Q=="],
"@aws-sdk/credential-provider-sso/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/credential-provider-web-identity/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/credential-providers/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/dynamodb-codec/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/endpoint-cache/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/eventstream-handler-node/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/lib-dynamodb/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/lib-storage/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/middleware-endpoint-discovery/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/middleware-eventstream/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/middleware-sdk-ec2/@smithy/signature-v4": ["@smithy/signature-v4@5.7.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow=="],
"@aws-sdk/middleware-sdk-ec2/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/middleware-sdk-s3/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/middleware-sdk-sqs/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/middleware-websocket/@smithy/signature-v4": ["@smithy/signature-v4@5.7.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow=="],
"@aws-sdk/middleware-websocket/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/nested-clients/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/s3-request-presigner/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.7.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow=="],
"@aws-sdk/signature-v4-multi-region/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/token-providers/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/types/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/util-dynamodb/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/util-locate-window/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/xml-builder/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@azure-rest/core-client/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@azure/abort-controller/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@azure/arm-containerinstance/@azure/abort-controller": ["@azure/abort-controller@1.1.0", "", { "dependencies": { "tslib": "^2.2.0" } }, "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw=="],
"@azure/arm-containerinstance/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@azure/communication-common/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@azure/communication-email/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@azure/core-auth/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@azure/core-client/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@azure/core-lro/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@azure/core-paging/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@azure/core-rest-pipeline/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@azure/core-tracing/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@azure/core-util/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@azure/core-xml/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@azure/identity/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@azure/keyvault-common/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@azure/keyvault-keys/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@azure/logger/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@azure/storage-blob/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@azure/storage-common/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@azure/storage-queue/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"@babel/generator/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="],
@@ -4835,6 +5097,10 @@
"@daytona/sdk/socket.io-client": ["socket.io-client@4.8.3", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g=="],
"@daytona/sdk/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@earendil-works/pi-agent-core/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
"@earendil-works/pi-ai/@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.91.1", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw=="],
"@earendil-works/pi-ai/@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.1048.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.11", "@aws-sdk/credential-provider-node": "^3.972.42", "@aws-sdk/eventstream-handler-node": "^3.972.16", "@aws-sdk/middleware-eventstream": "^3.972.12", "@aws-sdk/middleware-websocket": "^3.972.19", "@aws-sdk/token-providers": "3.1048.0", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/fetch-http-handler": "^5.4.2", "@smithy/node-http-handler": "^4.7.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ=="],
@@ -4849,12 +5115,12 @@
"@earendil-works/pi-coding-agent/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
"@earendil-works/pi-coding-agent/jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
"@earendil-works/pi-coding-agent/semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="],
"@earendil-works/pi-coding-agent/undici": ["undici@8.5.0", "", {}, "sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg=="],
"@earendil-works/pi-coding-agent/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
"@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="],
"@electron/asar/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
@@ -4871,12 +5137,22 @@
"@electron/osx-sign/isbinaryfile": ["isbinaryfile@4.0.10", "", {}, "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw=="],
"@emnapi/core/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@emnapi/runtime/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
"@esbuild-kit/esm-loader/get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="],
"@google-cloud/storage/google-auth-library": ["google-auth-library@9.15.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^6.1.1", "gcp-metadata": "^6.1.0", "gtoken": "^7.0.0", "jws": "^4.0.0" } }, "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng=="],
"@grpc/proto-loader/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
"@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
"@inquirer/core/mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="],
"@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
@@ -4909,6 +5185,16 @@
"@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
"@opentelemetry/configuration/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
"@peculiar/asn1-schema/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@peculiar/json-schema/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@peculiar/utils/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@peculiar/webcrypto/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@playwright/browser-chromium/playwright-core": ["playwright-core@1.62.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw=="],
"@playwright/test/playwright": ["playwright@1.61.1", "", { "dependencies": { "playwright-core": "1.61.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ=="],
@@ -4985,27 +5271,53 @@
"@redis/client/cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
"@rolldown/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
"@rolldown/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
"@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.5", "", { "dependencies": { "@tybys/wasm-util": "^0.10.2" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q=="],
"@shuding/opentype.js/fflate": ["fflate@0.7.4", "", {}, "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw=="],
"@sim/emcn/@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.10", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog=="],
"@sim/emcn/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ=="],
"@smithy/core/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@smithy/credential-provider-imds/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@smithy/eventstream-codec/@smithy/core": ["@smithy/core@3.25.0", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-TTD6el7tvKyafkXBf7XO3jLOE+qVxOTrLjp/fEGiV3BMfUHK/LfdYlQO9YgZvzxC7kqA3H/IhJXNqQgnbgjb7A=="],
"@smithy/eventstream-codec/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@smithy/fetch-http-handler/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@smithy/is-array-buffer/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@smithy/middleware-compression/fflate": ["fflate@0.8.3", "", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="],
"@smithy/middleware-compression/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@smithy/node-http-handler/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@smithy/signature-v4/@smithy/core": ["@smithy/core@3.25.0", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-TTD6el7tvKyafkXBf7XO3jLOE+qVxOTrLjp/fEGiV3BMfUHK/LfdYlQO9YgZvzxC7kqA3H/IhJXNqQgnbgjb7A=="],
"@smithy/signature-v4/@smithy/types": ["@smithy/types@4.15.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Z5TAOxygoFvybJV3igo5SloFflSokHx2hu1eFA+DxDTcn+FtKxUSui+rbTRG1pAafMA888Z3MVvCWUuvCrTXjg=="],
"@smithy/signature-v4/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@smithy/types/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@smithy/util-buffer-from/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@smithy/util-utf8/@smithy/core": ["@smithy/core@3.25.0", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-TTD6el7tvKyafkXBf7XO3jLOE+qVxOTrLjp/fEGiV3BMfUHK/LfdYlQO9YgZvzxC7kqA3H/IhJXNqQgnbgjb7A=="],
"@smithy/util-utf8/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@socket.io/redis-adapter/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
"@tailwindcss/node/jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
"@swc/helpers/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tailwindcss/node/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
@@ -5055,6 +5367,8 @@
"@trigger.dev/sdk/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="],
"@tybys/wasm-util/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@types/archiver/@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="],
"@types/buffer-from/@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="],
@@ -5085,6 +5399,8 @@
"@types/ws/@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="],
"@typespec/ts-http-runtime/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@xyflow/react/zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="],
"accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
@@ -5109,14 +5425,14 @@
"app-builder-lib/hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="],
"app-builder-lib/jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
"app-builder-lib/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"app-builder-lib/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="],
"archiver/async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
"aria-hidden/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"artillery/@azure/storage-blob": ["@azure/storage-blob@12.33.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.3", "@azure/core-http-compat": "^2.2.0", "@azure/core-lro": "^2.2.0", "@azure/core-paging": "^1.6.2", "@azure/core-rest-pipeline": "^1.19.1", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/core-xml": "^1.4.5", "@azure/logger": "^1.1.4", "@azure/storage-common": "^12.4.1", "events": "^3.0.0", "tslib": "^2.8.1" } }, "sha512-2SX8oP8PyblUcAFZSg39c8Ls+tFjavM6sBeV+qpw33mRzRhI/5hrFJmJ/x0H9xx5l6ECPvgSP8uPxqTeVbHNIA=="],
"artillery/@playwright/test": ["@playwright/test@1.62.1", "", { "dependencies": { "playwright": "1.62.1" }, "bin": { "playwright": "cli.js" } }, "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ=="],
@@ -5133,6 +5449,8 @@
"artillery/socket.io-client": ["socket.io-client@4.8.3", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g=="],
"asn1js/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"async-retry/retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="],
"axios/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
@@ -5259,6 +5577,8 @@
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"framer-motion/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"fumadocs-mdx/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"fumadocs-mdx/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
@@ -5339,6 +5659,8 @@
"lint-staged/commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="],
"lint-staged/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
"log-update/ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="],
"log-update/cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
@@ -5359,6 +5681,8 @@
"mixpanel/https-proxy-agent": ["https-proxy-agent@5.0.0", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA=="],
"motion/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"neo4j-driver-bolt-connection/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
"node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
@@ -5379,14 +5703,12 @@
"parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
"pdf-lib/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="],
"pino-pretty/pino-abstract-transport": ["pino-abstract-transport@3.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg=="],
"pino-pretty/strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="],
"pkijs/@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="],
"pkijs/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
"playwright/playwright-core": ["playwright-core@1.62.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw=="],
@@ -5403,18 +5725,30 @@
"proxy-addr/ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
"pvtsutils/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"raw-body/iconv-lite": ["iconv-lite@0.7.1", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw=="],
"rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
"react-email/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"react-email/commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="],
"react-email/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
"react-email/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"react-email/tailwindcss": ["tailwindcss@4.3.1", "", {}, "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q=="],
"react-promise-suspense/fast-deep-equal": ["fast-deep-equal@2.0.1", "", {}, "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w=="],
"react-remove-scroll/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"react-remove-scroll-bar/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"react-style-singleton/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
"readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
@@ -5423,8 +5757,12 @@
"rimraf/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
"rolldown/@oxc-project/types": ["@oxc-project/types@0.133.0", "", {}, "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA=="],
"rss-parser/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="],
"rxjs/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
"slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
@@ -5479,8 +5817,16 @@
"unzipper/fs-extra": ["fs-extra@11.3.1", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g=="],
"use-callback-ref/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"use-sidecar/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"vfile-matter/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
"vite/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
"webcrypto-core/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"whatwg-encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"widest-line/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
@@ -5545,10 +5891,14 @@
"@earendil-works/pi-ai/@aws-sdk/client-bedrock-runtime/@smithy/types": ["@smithy/types@4.15.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Z5TAOxygoFvybJV3igo5SloFflSokHx2hu1eFA+DxDTcn+FtKxUSui+rbTRG1pAafMA888Z3MVvCWUuvCrTXjg=="],
"@earendil-works/pi-ai/@aws-sdk/client-bedrock-runtime/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@earendil-works/pi-ai/@smithy/node-http-handler/@smithy/core": ["@smithy/core@3.25.0", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-TTD6el7tvKyafkXBf7XO3jLOE+qVxOTrLjp/fEGiV3BMfUHK/LfdYlQO9YgZvzxC7kqA3H/IhJXNqQgnbgjb7A=="],
"@earendil-works/pi-ai/@smithy/node-http-handler/@smithy/types": ["@smithy/types@4.15.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Z5TAOxygoFvybJV3igo5SloFflSokHx2hu1eFA+DxDTcn+FtKxUSui+rbTRG1pAafMA888Z3MVvCWUuvCrTXjg=="],
"@earendil-works/pi-ai/@smithy/node-http-handler/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@electron/fuses/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
@@ -5605,6 +5955,8 @@
"@grpc/proto-loader/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"@img/sharp-wasm32/@emnapi/runtime/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@inquirer/core/wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
@@ -5643,6 +5995,14 @@
"@radix-ui/react-scroll-area/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="],
"@rolldown/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
"@rolldown/binding-wasm32-wasi/@emnapi/core/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@rolldown/binding-wasm32-wasi/@emnapi/runtime/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
"@sim/emcn/@radix-ui/react-avatar/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@sim/emcn/@radix-ui/react-avatar/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
@@ -5729,6 +6089,8 @@
"app-builder-lib/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
"artillery/@azure/storage-blob/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"artillery/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"artillery/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
@@ -6113,6 +6475,8 @@
"@inquirer/core/wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@sim/emcn/@radix-ui/react-avatar/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@trigger.dev/core/socket.io-client/engine.io-client/xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.0.0", "", {}, "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A=="],
+94
View File
@@ -0,0 +1,94 @@
{
"$schema": "https://unpkg.com/knip@6/schema.json",
// Gate reachability and dependency ownership. Export/type pruning needs review
// of public package contracts and test-only consumers, not a blanket threshold.
"include": ["files", "dependencies", "unlisted", "unresolved"],
"workspaces": {
".": {
"entry": ["scripts/**/*.{ts,tsx}", "vitest.shared.ts"],
"paths": { "@/*": ["./apps/sim/*"] },
// This alias selects the native tsc binary; see check-native-typecheck.ts.
"ignoreDependencies": ["@typescript/native"]
},
"apps/sim": {
"entry": [
// Container bootstrap and Trigger's directory-discovered task graph.
"bootstrap.ts",
"trigger.config.ts",
"background/**/*.ts",
"scripts/**/*.{ts,tsx}",
"**/*.integration.ts",
"vitest.integration.setup.ts",
// Spawned worker and assets bundled/loaded by filename.
"lib/execution/isolated-vm-worker.cjs",
"lib/execution/sandbox/bundles/*.{ts,cjs}",
// Compile-time fixture compatibility assertion, included by tsconfig.
"lib/table/fixture-contract.ts",
// scripts/generate-docs.ts imports this through pathToFileURL.
"app/(landing)/integrations/data/landing-content.ts",
// Mirrored from the sibling worker's contracts:sync, not app-owned.
"lib/mothership/generated/account.ts",
"lib/mothership/generated/local-files.ts"
],
// Required/discoverable barrels whose children have direct live imports.
// Ignore only the barrel file finding, so it cannot keep dead children alive.
"ignoreIssues": {
"sandbox-tasks/index.ts": ["files"],
"components/mcp/index.ts": ["files"],
"triggers/quickbooks/index.ts": ["files"],
"lib/api/contracts/selectors/index.ts": ["files"],
"lib/api/contracts/tools/index.ts": ["files"],
"lib/api/contracts/tools/communication/index.ts": ["files"],
"lib/api/contracts/tools/databases/index.ts": ["files"],
"lib/api/contracts/tools/media/index.ts": ["files"],
"lib/api/contracts/v1/index.ts": ["files"],
"app/(landing)/components/shared/two-block-workflow-preview/index.ts": ["files"],
"app/(landing)/customers/components/customer-story-card/index.ts": ["files"],
"app/(landing)/customers/components/customer-story-media/index.ts": ["files"],
"app/(landing)/customers/components/customer-story-page/index.ts": ["files"]
},
"ignoreDependencies": [
// docker/app.Dockerfile invokes this binary to rebuild isolated-vm.
"node-gyp",
// Imported inside the generated source string in sandbox/bundles/build.ts.
"pptxgenjs",
// Next aliases this marker to its bundled server-only module.
"server-only",
// Knip's email CLI plugin infers this transitive preview implementation.
"@react-email/ui"
],
// Live test spawns this path with cwd=MSHIP_WORKER_ROOT (a sibling repo).
"ignoreUnresolved": ["tools/probes/src/controller-run-read.ts"]
},
"apps/realtime": { "entry": ["src/bootstrap.ts"] },
"apps/desktop": {
// scripts/build.ts and e2e/updater.spec.ts supply these to esbuild by path.
"entry": [
"src/main/index.ts",
"src/preload/*.ts",
"src/preload/browser/index.ts",
"src/renderer/*/index.tsx",
"scripts/*.ts",
"e2e/fixtures/updater.ts"
],
// ensure-pty-prebuilds.ts assembles these package names for universal builds.
"ignoreDependencies": ["@lydell/node-pty-darwin-arm64", "@lydell/node-pty-darwin-x64"]
},
"packages/db": {
"entry": [
"scripts/*.ts",
"script-migrations/*.ts",
"**/*.integration.ts",
"vitest.integration.setup.ts"
]
},
"packages/sim-cli": {
"entry": ["src/index.ts", "src/runtime.ts", "scripts/*.ts"]
},
"packages/ts-sdk": { "entry": ["examples/*.ts"] },
// The contract audit reads this snapshot by filename.
"packages/desktop-bridge": { "entry": ["contract-snapshot.ts"] },
// This shared config is consumed by apps that own the Next dependency.
"packages/tsconfig": { "ignoreUnresolved": ["next"] }
}
}
+11 -8
View File
@@ -14,17 +14,17 @@
"dev:sockets": "cd apps/realtime && bun run dev",
"dev:full": "bunx concurrently -n \"App,Realtime\" -c \"cyan,magenta\" \"cd apps/sim && bun run dev\" \"cd apps/realtime && bun run dev\"",
"dev:full:capped": "bunx concurrently -n \"App,Realtime\" -c \"cyan,magenta\" \"cd apps/sim && bun run dev:capped\" \"cd apps/realtime && bun run dev\"",
"test": "bun run test:setup && bun run test:scripts && turbo run test",
"test": "bun run test:scripts && turbo run test",
"test:setup": "bun run --cwd packages/sim-setup test",
"test:integration": "bun --no-env-file scripts/test-integration.ts",
"test:search-performance": "KNOWLEDGE_SEARCH_PERFORMANCE_TEST=true bun --no-env-file scripts/test-integration.ts search-latency",
"format": "turbo run format",
"format:check": "turbo run format:check",
"lint": "turbo run lint",
"lint:check": "turbo run lint:check",
"format": "biome format --write scripts && turbo run format",
"format:check": "biome format scripts && turbo run format:check",
"lint": "biome check --write --unsafe scripts && turbo run lint",
"lint:check": "biome check scripts && turbo run lint:check",
"lint:helm": "helm lint helm/sim --strict --values helm/sim/ci/default-values.yaml",
"lint:all": "turbo run lint && bun run lint:helm",
"check": "turbo run format:check",
"lint:all": "bun run lint && bun run lint:helm",
"check": "bun run format:check",
"check:egress-boundary": "bun run scripts/check-egress-boundary.ts",
"check:boundaries": "bun run scripts/check-monorepo-boundaries.ts",
"check:api-validation": "bun run scripts/check-api-validation-contracts.ts --check",
@@ -123,7 +123,8 @@
"type-check": "turbo run type-check",
"release": "bun run scripts/create-single-release.ts",
"test:scripts": "vitest run --config scripts/vitest.config.ts",
"check:agent-cli-boundary": "bun run scripts/check-agent-cli-boundary.ts"
"check:agent-cli-boundary": "bun run scripts/check-agent-cli-boundary.ts",
"check:dead-code": "bun run node_modules/knip/bin/knip.js"
},
"overrides": {
"react": "19.2.4",
@@ -174,8 +175,10 @@
"gray-matter": "4.0.3",
"husky": "9.1.7",
"json-schema-to-typescript": "15.0.4",
"knip": "6.37.0",
"lint-staged": "16.0.0",
"opentype.js": "1.3.4",
"pdf-lib": "1.17.1",
"react": "19.2.4",
"remark-gfm": "4.0.1",
"remark-parse": "11.0.0",
+69
View File
@@ -0,0 +1,69 @@
import { spawnSync } from 'node:child_process'
import { copyFileSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import path from 'node:path'
import { describe, expect, it } from 'vitest'
const ROOT = path.resolve(import.meta.dirname, '..')
describe('package boundary command', () => {
it('rejects app module edges regardless of import form, without treating documentation as imports', () => {
const fixture = mkdtempSync(path.join(ROOT, 'node_modules/.boundary-audit-'))
try {
mkdirSync(path.join(fixture, 'scripts'))
mkdirSync(path.join(fixture, 'packages/example/src'), { recursive: true })
mkdirSync(path.join(fixture, 'apps/web'), { recursive: true })
mkdirSync(path.join(fixture, 'apps/docs'))
mkdirSync(path.join(fixture, 'apps/no-package'))
writeFileSync(
path.join(fixture, 'apps/web/package.json'),
JSON.stringify({ name: '@example/web' })
)
writeFileSync(
path.join(fixture, 'apps/docs/package.json'),
JSON.stringify({ name: 'example-docs' })
)
copyFileSync(
path.join(ROOT, 'scripts/check-monorepo-boundaries.ts'),
path.join(fixture, 'scripts/check-monorepo-boundaries.ts')
)
const cases = [
["import { value } from '@/lib/value'", false],
["import '@/lib/value'", false],
["export * from '../../../apps/sim/lib/value'", false],
["import { value } from '../../../apps/sim/lib/value'", false],
["const value = import('@/lib/value')", false],
['const value = import(`@/lib/value`)', false],
["const value = require('../../../apps/sim/lib/value')", false],
["import value = require('../../../apps/sim/lib/value')", false],
["type Value = import('../../../apps/sim/lib/value').Value", false],
[String.raw`import '\x40/lib/value'`, false],
["import { value } from '@example/web'", false],
["import { value } from '@example/web/lib/value'", false],
["import 'example-docs'", false],
["import 'example-docs/lib/value'", false],
["import { value } from './apps/value'", true],
["import { value } from '@sim/utils/errors'", true],
["import { value } from '@example/web-utils'", true],
["import { value } from 'example-docs-utils'", true],
["// import { value } from '@/lib/value'", true],
['const example = "import { value } from \'@/lib/value\'"', true],
] as const
for (const [source, allowed] of cases) {
writeFileSync(path.join(fixture, 'packages/example/src/index.ts'), `${source}\n`)
const result = spawnSync(
'bun',
[path.join(fixture, 'scripts/check-monorepo-boundaries.ts')],
{
cwd: fixture,
encoding: 'utf8',
}
)
expect.soft(result.status, `${source}\n${result.stderr}`).toBe(allowed ? 0 : 1)
if (!allowed) expect.soft(result.stderr).toContain('packages/example/src/index.ts:1')
}
} finally {
rmSync(fixture, { recursive: true, force: true })
}
}, 30_000)
})
+59 -17
View File
@@ -1,15 +1,21 @@
#!/usr/bin/env bun
import { readdir, readFile } from 'node:fs/promises'
import path from 'node:path'
import ts from '@typescript/typescript6'
const ROOT = path.resolve(import.meta.dir, '..')
const PACKAGES_DIR = path.join(ROOT, 'packages')
const FORBIDDEN_PATTERNS: Array<{ pattern: RegExp; description: string }> = [
{ pattern: /from\s+['"]@\/(?!\*)/g, description: "'@/' path alias (apps/sim-only)" },
{ pattern: /from\s+['"]\.\.\/\.\.\/apps\//g, description: 'relative import into apps/' },
{ pattern: /from\s+['"]apps\//g, description: "bare 'apps/' import" },
]
/** Package dependencies point toward shared code, including type-only imports. */
function forbiddenImport(file: string, specifier: string, appNames: ReadonlySet<string>): boolean {
if (specifier.startsWith('@/') || specifier.startsWith('apps/')) return true
for (const name of appNames) {
if (specifier === name || specifier.startsWith(`${name}/`)) return true
}
if (!specifier.startsWith('.')) return false
const target = path.relative(ROOT, path.resolve(path.dirname(file), specifier))
return target === 'apps' || target.startsWith(`apps${path.sep}`)
}
const SKIP_DIRS = new Set(['node_modules', 'dist', '.next', '.turbo', 'coverage'])
@@ -28,6 +34,21 @@ async function walk(dir: string, results: string[] = []): Promise<string[]> {
}
async function main() {
const appNames = new Set<string>()
for (const entry of await readdir(path.join(ROOT, 'apps'), { withFileTypes: true })) {
if (!entry.isDirectory()) continue
const content = await readFile(
path.join(ROOT, 'apps', entry.name, 'package.json'),
'utf8'
).catch((error: NodeJS.ErrnoException) => {
if (error.code === 'ENOENT') return null
throw error
})
if (content === null) continue
const manifest = JSON.parse(content) as { name?: string }
if (typeof manifest.name === 'string') appNames.add(manifest.name)
}
const packagesEntries = await readdir(PACKAGES_DIR, { withFileTypes: true })
const packageDirs = packagesEntries
.filter((entry) => entry.isDirectory())
@@ -39,21 +60,42 @@ async function main() {
const files = await walk(dir)
for (const file of files) {
const content = await readFile(file, 'utf8')
const source = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true)
const lines = content.split('\n')
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
for (const { pattern, description } of FORBIDDEN_PATTERNS) {
pattern.lastIndex = 0
if (pattern.test(line)) {
offenders.push({
file: path.relative(ROOT, file),
line: i + 1,
description,
snippet: line.trim(),
})
}
function visit(node: ts.Node): void {
let module: ts.Node | undefined
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
module = node.moduleSpecifier
} else if (
ts.isImportEqualsDeclaration(node) &&
ts.isExternalModuleReference(node.moduleReference)
) {
module = node.moduleReference.expression
} else if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument)) {
module = node.argument.literal
} else if (
ts.isCallExpression(node) &&
(node.expression.kind === ts.SyntaxKind.ImportKeyword ||
(ts.isIdentifier(node.expression) && node.expression.text === 'require'))
) {
module = node.arguments[0]
}
if (
module &&
ts.isStringLiteralLike(module) &&
forbiddenImport(file, module.text, appNames)
) {
const { line } = source.getLineAndCharacterOfPosition(module.getStart(source))
offenders.push({
file: path.relative(ROOT, file),
line: line + 1,
description: `import into apps/: ${module.text}`,
snippet: lines[line].trim(),
})
}
ts.forEachChild(node, visit)
}
visit(source)
}
}
-2
View File
@@ -1,12 +1,10 @@
#!/usr/bin/env bun
import { spawnSync } from 'node:child_process'
import path from 'node:path'
import { localBin } from './local-bin'
const ROOT = path.resolve(import.meta.dir, '..')
const CHECKS = [
[process.execPath, 'run', 'scripts/generate-openapi.ts', '--check'],
[localBin('vitest'), 'run', '--config', 'scripts/openapi/vitest.config.ts'],
[process.execPath, 'run', 'scripts/check-openapi-specs.ts'],
] as const
@@ -0,0 +1,97 @@
import { spawnSync } from 'node:child_process'
import {
copyFileSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
symlinkSync,
writeFileSync,
} from 'node:fs'
import path from 'node:path'
import { describe, expect, it } from 'vitest'
const ROOT = path.resolve(import.meta.dirname, '..')
describe('script collection command', () => {
it('collects nested scripts and reports an orphan when a config drops its tests', () => {
const fixture = mkdtempSync(path.join(ROOT, 'node_modules/.script-coverage-'))
try {
mkdirSync(path.join(fixture, 'scripts/nested'), { recursive: true })
mkdirSync(path.join(fixture, 'scripts/openapi'))
mkdirSync(path.join(fixture, 'node_modules/.bin'), { recursive: true })
symlinkSync(
path.join(ROOT, 'node_modules/.bin/vitest'),
path.join(fixture, 'node_modules/.bin/vitest')
)
for (const file of [
'scripts/check-script-test-coverage.ts',
'scripts/local-bin.ts',
'scripts/vitest.config.ts',
'vitest.shared.ts',
]) {
copyFileSync(path.join(ROOT, file), path.join(fixture, file))
}
writeFileSync(
path.join(fixture, 'package.json'),
JSON.stringify({
scripts: {
test: 'bun run test:scripts',
'test:scripts': 'vitest run --config scripts/vitest.config.ts',
},
})
)
for (const file of [
'nested/check.test.ts',
'nested/view.test.tsx',
'nested/check.spec.ts',
'openapi/api.test.ts',
]) {
writeFileSync(path.join(fixture, 'scripts', file), '')
}
const run = () =>
spawnSync('bun', [path.join(fixture, 'scripts/check-script-test-coverage.ts')], {
cwd: fixture,
encoding: 'utf8',
})
const valid = run()
expect(valid.status, valid.stderr).toBe(0)
expect(valid.stdout).toContain('4 script tests collected')
const manifestPath = path.join(fixture, 'package.json')
const originalManifest = readFileSync(manifestPath, 'utf8')
for (const command of [
'echo tests-disabled',
'vitest run --config scripts/alternate.config.ts',
'vitest run --config scripts/vitest.config.ts scripts/nested/check.test.ts',
]) {
writeFileSync(
manifestPath,
JSON.stringify({ scripts: { test: 'bun run test:scripts', 'test:scripts': command } })
)
const disabled = run()
expect.soft(disabled.status, command).toBe(1)
expect
.soft(disabled.stderr, command)
.toContain('test:scripts must run the complete scripts')
}
writeFileSync(manifestPath, originalManifest)
const config = path.join(fixture, 'scripts/vitest.config.ts')
writeFileSync(
config,
readFileSync(config, 'utf8').replace(
/include: \[[^\]]+\]/,
"include: ['scripts/*.test.ts']"
)
)
const missing = run()
expect(missing.status, missing.stderr).toBe(1)
expect(missing.stderr).toContain('scripts/nested/check.test.ts')
expect(missing.stderr).toContain('scripts/nested/view.test.tsx')
expect(missing.stderr).toContain('scripts/openapi/api.test.ts')
} finally {
rmSync(fixture, { recursive: true, force: true })
}
}, 30_000)
})
+26 -9
View File
@@ -1,14 +1,14 @@
#!/usr/bin/env bun
/**
* Asserts every `scripts/*.test.ts` file is collected by the scripts Vitest config.
* Asserts every script test is collected by the scripts Vitest config.
*
* The root `test` script once chained a hand-maintained list of `test:*` entries, and a
* hand-maintained list silently drifts from the files on disk: a test added without a matching
* entry never runs, in CI or locally, and nothing reports it. `scripts/check-migrations-safety.test.ts`
* sat unreferenced and green for exactly that reason. `scripts/vitest.config.ts` now collects
* the directory by glob, so drift can only come from a file the glob does not match (a test in a
* subdirectory, a different suffix) or from the `test` script no longer chaining `test:scripts`.
* This guard checks both by asking Vitest which files it would run.
* subdirectory, a different suffix) or from the test commands no longer running that config.
* This guard checks the command contract and asks Vitest which files it would run.
*
* `run-audits.ts` derives its own list from the `check:*` namespace precisely so a new audit is
* picked up by default, so this guard registers itself simply by being named `check:*` — it cannot
@@ -16,9 +16,12 @@
*/
import { readdirSync } from 'node:fs'
import path from 'node:path'
import { localBin } from './local-bin'
const ROOT = path.resolve(import.meta.dir, '..')
const SUB_SCRIPT_PATTERN = /bun run ([\w:-]+)/g
const SCRIPTS_TEST_CONFIG = 'scripts/vitest.config.ts'
const SCRIPTS_TEST_COMMAND = `vitest run --config ${SCRIPTS_TEST_CONFIG}`
const manifest = await Bun.file(path.join(ROOT, 'package.json')).json()
const commands = manifest.scripts as Record<string, string>
@@ -41,8 +44,14 @@ if (!reachableScripts('test').has('test:scripts')) {
process.exit(1)
}
// Keep collection tied to the actual runner; filters or another config can silently skip tests.
if (commands['test:scripts']?.trim() !== SCRIPTS_TEST_COMMAND) {
console.error(`test:scripts must run the complete scripts Vitest config: ${SCRIPTS_TEST_COMMAND}`)
process.exit(1)
}
const listed = Bun.spawnSync(
['bunx', 'vitest', 'list', '--json', '--filesOnly', '--config', 'scripts/vitest.config.ts'],
[localBin('vitest'), 'list', '--json', '--filesOnly', '--config', SCRIPTS_TEST_CONFIG],
{
cwd: ROOT,
}
@@ -57,16 +66,24 @@ const collected = new Set(
)
)
const onDisk = readdirSync(path.join(ROOT, 'scripts'))
.filter((file) => file.endsWith('.test.ts'))
.map((file) => `scripts/${file}`)
.sort()
/** Inventory independently of the include globs so a narrowed config cannot hide its omissions. */
function testFiles(dir: string): string[] {
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
if (['node_modules', '.git', 'dist'].includes(entry.name)) return []
const file = path.join(dir, entry.name)
if (entry.isDirectory()) return testFiles(file)
return /\.(test|spec)\.(?:[cm]?[jt]s|[jt]sx)$/.test(entry.name)
? [path.relative(ROOT, file).split(path.sep).join('/')]
: []
})
}
const onDisk = testFiles(path.join(ROOT, 'scripts')).sort()
const orphaned = onDisk.filter((file) => !collected.has(file))
if (orphaned.length > 0) {
console.error(
`Script tests never run by \`bun run test\`:\n${orphaned.map((file) => ` - ${file}`).join('\n')}\n` +
'Make sure the `scripts/vitest.config.ts` include glob matches them.'
'Make sure the scripts Vitest config collects them.'
)
process.exit(1)
}
+1 -1
View File
@@ -259,7 +259,7 @@ describe('generated OpenAPI documents', () => {
)
}
}
})
}, 30_000)
it('covers the complete public v2 operation surface with canonical errors', () => {
const outputs = DOCUMENTS.map((document) => document.output)
-26
View File
@@ -1,26 +0,0 @@
import path from 'node:path'
import { defineConfig, mergeConfig } from 'vitest/config'
import sharedConfig from '../../vitest.shared'
const ROOT = path.resolve(import.meta.dirname, '../..')
export default mergeConfig(
sharedConfig,
defineConfig({
resolve: {
alias: {
'@': path.join(ROOT, 'apps/sim'),
},
},
test: {
include: ['scripts/openapi/**/*.test.ts'],
/**
* The determinism check serializes all seven published documents twice —
* roughly 2MB of JSON — so it was running against vitest's 5s default
* rather than a budget anyone chose. It already sat just under that on
* staging, and this branch's richer descriptions tip it over.
*/
testTimeout: 30_000,
},
})
)

Some files were not shown because too many files have changed in this diff Show More