mirror of
https://github.com/simstudioai/sim.git
synced 2026-10-13 15:48:31 +08:00
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:
@@ -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:
|
||||
|
||||
-11
@@ -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,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. */
|
||||
|
||||
@@ -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()
|
||||
@@ -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,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 }
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 } : {}),
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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'],
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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(),
|
||||
}))
|
||||
|
||||
@@ -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()
|
||||
},
|
||||
}
|
||||
),
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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'))
|
||||
|
||||
@@ -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'],
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}))
|
||||
|
||||
@@ -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()
|
||||
},
|
||||
}
|
||||
),
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
},
|
||||
}
|
||||
),
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
},
|
||||
}
|
||||
),
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
},
|
||||
}
|
||||
),
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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 } })
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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}')
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
},
|
||||
}
|
||||
),
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
},
|
||||
}
|
||||
),
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user