@lota-sdk/core 0.4.28 → 0.4.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lota-sdk/core",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.30",
|
|
4
4
|
"files": [
|
|
5
5
|
"src",
|
|
6
6
|
"infrastructure/schema"
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"@ai-sdk/provider": "^3.0.9",
|
|
33
33
|
"@chat-adapter/slack": "^4.26.0",
|
|
34
34
|
"@chat-adapter/state-ioredis": "^4.26.0",
|
|
35
|
-
"@lota-sdk/shared": "0.4.
|
|
35
|
+
"@lota-sdk/shared": "0.4.30",
|
|
36
36
|
"@mendable/firecrawl-js": "^4.20.0",
|
|
37
37
|
"@surrealdb/node": "^3.0.3",
|
|
38
38
|
"ai": "^6.0.170",
|
|
@@ -38,8 +38,8 @@ class AiGatewayStreamAttemptTag extends Context.Service<
|
|
|
38
38
|
|
|
39
39
|
const EXPECTED_GATEWAY_KEY_PREFIX = 'sk-bf-'
|
|
40
40
|
const AI_GATEWAY_VIRTUAL_KEY_HEADER = 'x-bf-vk'
|
|
41
|
-
const AI_GATEWAY_TIMEOUT_MS =
|
|
42
|
-
const AI_GATEWAY_STREAM_IDLE_TIMEOUT_MS =
|
|
41
|
+
const AI_GATEWAY_TIMEOUT_MS = 360_000
|
|
42
|
+
const AI_GATEWAY_STREAM_IDLE_TIMEOUT_MS = 180_000
|
|
43
43
|
const AI_GATEWAY_MAX_RETRIES = 4
|
|
44
44
|
const AI_GATEWAY_MAX_RETRY_DELAY_MS = 15_000
|
|
45
45
|
const OPENAI_RESPONSES_PROVIDER_ID = 'openai.responses'
|
|
@@ -748,13 +748,60 @@ function isOpenRouterModel(modelId: string): boolean {
|
|
|
748
748
|
return modelId.trim().toLowerCase().startsWith('openrouter/')
|
|
749
749
|
}
|
|
750
750
|
|
|
751
|
+
function mergeAbortSignals(signals: Array<AbortSignal | undefined>): { signal?: AbortSignal; cleanup: () => void } {
|
|
752
|
+
const activeSignals = signals.filter((signal): signal is AbortSignal => Boolean(signal))
|
|
753
|
+
if (activeSignals.length === 0) return { cleanup: () => undefined }
|
|
754
|
+
if (activeSignals.length === 1) return { signal: activeSignals[0], cleanup: () => undefined }
|
|
755
|
+
|
|
756
|
+
const controller = new AbortController()
|
|
757
|
+
const listeners: Array<() => void> = []
|
|
758
|
+
const abortFrom = (signal: AbortSignal) => {
|
|
759
|
+
if (!controller.signal.aborted) controller.abort(signal.reason)
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
for (const signal of activeSignals) {
|
|
763
|
+
if (signal.aborted) {
|
|
764
|
+
abortFrom(signal)
|
|
765
|
+
continue
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const listener = () => abortFrom(signal)
|
|
769
|
+
signal.addEventListener('abort', listener, { once: true })
|
|
770
|
+
listeners.push(() => signal.removeEventListener('abort', listener))
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
return {
|
|
774
|
+
signal: controller.signal,
|
|
775
|
+
cleanup: () => {
|
|
776
|
+
for (const cleanup of listeners) cleanup()
|
|
777
|
+
},
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
async function callAbortableProvider<T>(
|
|
782
|
+
params: AiGatewayCallOptions,
|
|
783
|
+
effectSignal: AbortSignal,
|
|
784
|
+
evaluate: (params: AiGatewayCallOptions) => PromiseLike<T>,
|
|
785
|
+
): Promise<T> {
|
|
786
|
+
const { signal, cleanup } = mergeAbortSignals([effectSignal, params.abortSignal])
|
|
787
|
+
try {
|
|
788
|
+
return await evaluate(signal ? { ...params, abortSignal: signal } : params)
|
|
789
|
+
} finally {
|
|
790
|
+
cleanup()
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
751
794
|
function attemptAiGatewayGenerate(
|
|
752
795
|
source: string,
|
|
753
|
-
|
|
796
|
+
params: AiGatewayCallOptions,
|
|
797
|
+
evaluate: (params: AiGatewayCallOptions) => PromiseLike<AiGatewayGenerateResult>,
|
|
754
798
|
): Effect.Effect<AiGatewayAttemptResult<AiGatewayGenerateResult>, AiGenerationError> {
|
|
755
799
|
return withAiGatewayResilience(
|
|
756
800
|
source,
|
|
757
|
-
Effect.tryPromise({
|
|
801
|
+
Effect.tryPromise({
|
|
802
|
+
try: (signal) => callAbortableProvider(params, signal, evaluate),
|
|
803
|
+
catch: (cause) => classifyAiGatewayError(source, cause),
|
|
804
|
+
}),
|
|
758
805
|
).pipe(
|
|
759
806
|
Effect.map((result) => ({ source, result })),
|
|
760
807
|
Effect.withSpan('AiGateway.generateAttempt'),
|
|
@@ -764,11 +811,15 @@ function attemptAiGatewayGenerate(
|
|
|
764
811
|
|
|
765
812
|
function attemptAiGatewayStream(
|
|
766
813
|
source: string,
|
|
767
|
-
|
|
814
|
+
params: AiGatewayCallOptions,
|
|
815
|
+
evaluate: (params: AiGatewayCallOptions) => PromiseLike<AiGatewayStreamResult>,
|
|
768
816
|
): Effect.Effect<AiGatewayAttemptResult<AiGatewayStreamResult>, AiGenerationError> {
|
|
769
817
|
return withAiGatewayResilience(
|
|
770
818
|
source,
|
|
771
|
-
Effect.tryPromise({
|
|
819
|
+
Effect.tryPromise({
|
|
820
|
+
try: (signal) => callAbortableProvider(params, signal, evaluate),
|
|
821
|
+
catch: (cause) => classifyAiGatewayError(source, cause),
|
|
822
|
+
}),
|
|
772
823
|
).pipe(
|
|
773
824
|
Effect.map((result) => ({ source, result })),
|
|
774
825
|
Effect.withSpan('AiGateway.streamAttempt'),
|
|
@@ -778,10 +829,11 @@ function attemptAiGatewayStream(
|
|
|
778
829
|
|
|
779
830
|
function executeGenerateAttemptPlan(
|
|
780
831
|
modelId: string,
|
|
781
|
-
|
|
832
|
+
params: AiGatewayCallOptions,
|
|
833
|
+
doGenerate: (params: AiGatewayCallOptions) => PromiseLike<AiGatewayGenerateResult>,
|
|
782
834
|
): Effect.Effect<AiGatewayAttemptResult<AiGatewayGenerateResult>, AiGenerationError> {
|
|
783
835
|
const primary = Layer.succeed(AiGatewayGenerateAttemptTag, {
|
|
784
|
-
execute: attemptAiGatewayGenerate('ai-gateway.generate', doGenerate),
|
|
836
|
+
execute: attemptAiGatewayGenerate('ai-gateway.generate', params, doGenerate),
|
|
785
837
|
})
|
|
786
838
|
const effect = Effect.gen(function* () {
|
|
787
839
|
const attempt = yield* AiGatewayGenerateAttemptTag
|
|
@@ -797,10 +849,11 @@ function executeGenerateAttemptPlan(
|
|
|
797
849
|
|
|
798
850
|
function executeStreamAttemptPlan(
|
|
799
851
|
modelId: string,
|
|
800
|
-
|
|
852
|
+
params: AiGatewayCallOptions,
|
|
853
|
+
doStream: (params: AiGatewayCallOptions) => PromiseLike<AiGatewayStreamResult>,
|
|
801
854
|
): Effect.Effect<AiGatewayAttemptResult<AiGatewayStreamResult>, AiGenerationError> {
|
|
802
855
|
const primary = Layer.succeed(AiGatewayStreamAttemptTag, {
|
|
803
|
-
execute: attemptAiGatewayStream('ai-gateway.stream', doStream),
|
|
856
|
+
execute: attemptAiGatewayStream('ai-gateway.stream', params, doStream),
|
|
804
857
|
})
|
|
805
858
|
const effect = Effect.gen(function* () {
|
|
806
859
|
const attempt = yield* AiGatewayStreamAttemptTag
|
|
@@ -1046,7 +1099,7 @@ function createAiGatewayLanguageModelMiddleware(
|
|
|
1046
1099
|
const model = resolveProviderModel(resolvedDeps.gateway.provider, modelId, providerId)
|
|
1047
1100
|
return resolvedDeps.runPromise(
|
|
1048
1101
|
withAiGatewayConcurrency(
|
|
1049
|
-
executeGenerateAttemptPlan(modelId, () => model.doGenerate(
|
|
1102
|
+
executeGenerateAttemptPlan(modelId, params, (attemptParams) => model.doGenerate(attemptParams)).pipe(
|
|
1050
1103
|
Effect.map(({ result }) => ({
|
|
1051
1104
|
...result,
|
|
1052
1105
|
content: injectAiGatewayChatReasoningContent(
|
|
@@ -1063,7 +1116,7 @@ function createAiGatewayLanguageModelMiddleware(
|
|
|
1063
1116
|
const model = resolveProviderModel(resolvedDeps.gateway.provider, modelId, providerId)
|
|
1064
1117
|
return resolvedDeps.runPromise(
|
|
1065
1118
|
withAiGatewayStreamConcurrency(
|
|
1066
|
-
executeStreamAttemptPlan(modelId, () => model.doStream(
|
|
1119
|
+
executeStreamAttemptPlan(modelId, params, (attemptParams) => model.doStream(attemptParams)).pipe(
|
|
1067
1120
|
Effect.map((attempt) => ({
|
|
1068
1121
|
...attempt,
|
|
1069
1122
|
result: isReasoningEnabled(params)
|
|
@@ -6,9 +6,32 @@ import { recordIdToString } from './record-id'
|
|
|
6
6
|
import { TABLES } from './tables'
|
|
7
7
|
|
|
8
8
|
export function isUniqueIndexConflict(error: unknown, indexName: string): boolean {
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
9
|
+
const visited = new Set<unknown>()
|
|
10
|
+
const stack: unknown[] = [error]
|
|
11
|
+
|
|
12
|
+
while (stack.length > 0) {
|
|
13
|
+
const current = stack.pop()
|
|
14
|
+
if (current === null || current === undefined || visited.has(current)) continue
|
|
15
|
+
visited.add(current)
|
|
16
|
+
|
|
17
|
+
if (typeof current === 'string') {
|
|
18
|
+
if (current.includes(indexName) && current.includes('already contains')) return true
|
|
19
|
+
continue
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (current instanceof Error) {
|
|
23
|
+
if (current.message.includes(indexName) && current.message.includes('already contains')) return true
|
|
24
|
+
stack.push(current.cause)
|
|
25
|
+
continue
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (typeof current === 'object') {
|
|
29
|
+
const record = current as Record<string, unknown>
|
|
30
|
+
stack.push(record.message, record.cause)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return false
|
|
12
35
|
}
|
|
13
36
|
|
|
14
37
|
const coerceDate = (value: unknown): Date => {
|
|
@@ -66,11 +66,13 @@ export function createDocumentProcessorQueueRuntime<TJob extends DocumentProcess
|
|
|
66
66
|
queueName?: string
|
|
67
67
|
workerName?: string
|
|
68
68
|
concurrency?: number
|
|
69
|
+
attempts?: number
|
|
69
70
|
lockDuration?: number
|
|
70
71
|
}): { enqueue: (job: TJob) => Promise<void>; startWorker: (options?: { registerSignals?: boolean }) => WorkerHandle } {
|
|
71
72
|
const queueName = params.queueName ?? DEFAULT_DOCUMENT_PROCESSOR_QUEUE
|
|
72
73
|
const workerName = params.workerName ?? DEFAULT_WORKER_NAME
|
|
73
74
|
const concurrency = params.concurrency ?? 10
|
|
75
|
+
const attempts = params.attempts ?? 3
|
|
74
76
|
const lockDuration = params.lockDuration ?? 300_000
|
|
75
77
|
const queueRuntime = createQueueFactory<TJob>({
|
|
76
78
|
name: queueName,
|
|
@@ -80,7 +82,7 @@ export function createDocumentProcessorQueueRuntime<TJob extends DocumentProcess
|
|
|
80
82
|
lockDuration,
|
|
81
83
|
logger: params.logger,
|
|
82
84
|
connectionProvider: params.getConnectionForBullMQ,
|
|
83
|
-
defaultJobOptions: { ...DEFAULT_JOB_RETENTION, attempts
|
|
85
|
+
defaultJobOptions: { ...DEFAULT_JOB_RETENTION, attempts, backoff: { type: 'exponential', delay: 1000 } },
|
|
84
86
|
processorPath: params.getWorkerPath(),
|
|
85
87
|
queueJobService: params.queueJobService,
|
|
86
88
|
})
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { setTimeout as delay } from 'node:timers/promises'
|
|
1
2
|
import { fileURLToPath } from 'node:url'
|
|
2
3
|
|
|
3
4
|
import type { Job, Worker } from 'bullmq'
|
|
@@ -30,6 +31,32 @@ export interface WorkerHandle {
|
|
|
30
31
|
shutdown: () => Promise<void>
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
async function closeWorkerWithTimeout(
|
|
35
|
+
worker: Worker,
|
|
36
|
+
name: string,
|
|
37
|
+
logger: typeof chatLogger,
|
|
38
|
+
timeoutMs = DEFAULT_SHUTDOWN_TIMEOUT_MS,
|
|
39
|
+
): Promise<void> {
|
|
40
|
+
const gracefulClose = worker.close(false)
|
|
41
|
+
const gracefulOutcome = gracefulClose.then(
|
|
42
|
+
() => ({ status: 'closed' as const }),
|
|
43
|
+
(error: unknown) => ({ status: 'failed' as const, error }),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
const outcome = await Promise.race([gracefulOutcome, delay(timeoutMs).then(() => ({ status: 'timed_out' as const }))])
|
|
47
|
+
|
|
48
|
+
if (outcome.status === 'closed') return
|
|
49
|
+
|
|
50
|
+
if (outcome.status === 'timed_out') {
|
|
51
|
+
logger.warn`${name} worker did not close within ${timeoutMs}ms; force-closing`
|
|
52
|
+
} else {
|
|
53
|
+
logger.warn`${name} worker graceful close failed; force-closing: ${outcome.error}`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await worker.close(true)
|
|
57
|
+
if (outcome.status === 'failed') throw outcome.error
|
|
58
|
+
}
|
|
59
|
+
|
|
33
60
|
interface TracedWorkerJobLike {
|
|
34
61
|
id?: unknown
|
|
35
62
|
name: string
|
|
@@ -87,7 +114,7 @@ export const createWorkerShutdown = (worker: Worker, name: string, logger: typeo
|
|
|
87
114
|
return Effect.runPromise(
|
|
88
115
|
Effect.asVoid(
|
|
89
116
|
Effect.tryPromise({
|
|
90
|
-
try: () => worker
|
|
117
|
+
try: () => closeWorkerWithTimeout(worker, name, logger),
|
|
91
118
|
catch: (cause) => new QueueWorkerError({ phase: 'close', cause }),
|
|
92
119
|
}),
|
|
93
120
|
),
|