@lota-sdk/core 0.4.46 → 0.4.48
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 +2 -2
- package/src/ai-gateway/ai-gateway.ts +142 -2
- package/src/ai-gateway/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lota-sdk/core",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.48",
|
|
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.48",
|
|
36
36
|
"@mendable/firecrawl-js": "^4.20.0",
|
|
37
37
|
"@surrealdb/node": "^3.0.3",
|
|
38
38
|
"ai": "^6.0.170",
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
|
|
1
3
|
import { devToolsMiddleware } from '@ai-sdk/devtools'
|
|
2
4
|
import { createOpenAI } from '@ai-sdk/openai'
|
|
3
5
|
import type { JSONSchema7 } from '@ai-sdk/provider'
|
|
@@ -37,6 +39,17 @@ class AiGatewayStreamAttemptTag extends Context.Service<
|
|
|
37
39
|
|
|
38
40
|
const EXPECTED_GATEWAY_KEY_PREFIX = 'sk-bf-'
|
|
39
41
|
const AI_GATEWAY_VIRTUAL_KEY_HEADER = 'x-bf-vk'
|
|
42
|
+
const AI_GATEWAY_PASSTHROUGH_EXTRA_PARAMS_HEADER = 'x-bf-passthrough-extra-params'
|
|
43
|
+
const AI_GATEWAY_SESSION_ID_HEADER = 'x-bf-session-id'
|
|
44
|
+
const AI_GATEWAY_SESSION_TTL_HEADER = 'x-bf-session-ttl'
|
|
45
|
+
const AZURE_OPENAI_PROMPT_CACHE_RETENTION = '24h'
|
|
46
|
+
const AZURE_OPENAI_PROMPT_CACHE_SESSION_TTL = '24h'
|
|
47
|
+
const AZURE_OPENAI_PROMPT_CACHE_KEY_PREFIX = 'azpc'
|
|
48
|
+
const AZURE_OPENAI_PROMPT_CACHE_HASH_LENGTH = 48
|
|
49
|
+
const AZURE_OPENAI_PROMPT_CACHE_MAX_STRING_LENGTH = 120_000
|
|
50
|
+
const AZURE_OPENAI_PROMPT_CACHE_MAX_ARRAY_ITEMS = 80
|
|
51
|
+
const AZURE_OPENAI_PROMPT_CACHE_MAX_OBJECT_KEYS = 80
|
|
52
|
+
const AZURE_OPENAI_PROMPT_CACHE_MAX_DEPTH = 8
|
|
40
53
|
const AI_GATEWAY_TIMEOUT_MS = 360_000
|
|
41
54
|
const AI_GATEWAY_STREAM_IDLE_TIMEOUT_MS = 180_000
|
|
42
55
|
const AI_GATEWAY_MAX_RETRIES = 4
|
|
@@ -512,7 +525,11 @@ export function makeAiGatewayService(
|
|
|
512
525
|
})
|
|
513
526
|
}
|
|
514
527
|
const baseURL = yield* normalizeAiGatewayUrlEffect(config.aiGateway.url)
|
|
515
|
-
const provider = createOpenAI({
|
|
528
|
+
const provider = createOpenAI({
|
|
529
|
+
baseURL,
|
|
530
|
+
apiKey,
|
|
531
|
+
headers: { [AI_GATEWAY_VIRTUAL_KEY_HEADER]: apiKey, [AI_GATEWAY_PASSTHROUGH_EXTRA_PARAMS_HEADER]: 'true' },
|
|
532
|
+
})
|
|
516
533
|
|
|
517
534
|
return AiGatewayTag.of({ semaphore, provider })
|
|
518
535
|
})
|
|
@@ -728,6 +745,127 @@ function isOpenRouterModel(modelId: string): boolean {
|
|
|
728
745
|
return modelId.trim().toLowerCase().startsWith('openrouter/')
|
|
729
746
|
}
|
|
730
747
|
|
|
748
|
+
function isAzureOpenAiPromptCacheModel(modelId: string): boolean {
|
|
749
|
+
const normalized = modelId.trim().toLowerCase()
|
|
750
|
+
if (!normalized) return false
|
|
751
|
+
|
|
752
|
+
const [providerPrefix, ...modelParts] = normalized.split('/')
|
|
753
|
+
if (modelParts.length > 0) {
|
|
754
|
+
if (providerPrefix !== 'azure') return false
|
|
755
|
+
return isAzureOpenAiPromptCacheModel(modelParts.join('/'))
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
return (
|
|
759
|
+
normalized === 'main-gpt-model' ||
|
|
760
|
+
normalized === 'mini-gpt-model' ||
|
|
761
|
+
normalized === 'nano-gpt-model' ||
|
|
762
|
+
normalized === 'gpt-5.5' ||
|
|
763
|
+
normalized.startsWith('gpt-5.5-') ||
|
|
764
|
+
normalized === 'gpt-5.4' ||
|
|
765
|
+
normalized.startsWith('gpt-5.4-')
|
|
766
|
+
)
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function hashAzureOpenAiPromptCacheValue(value: string): string {
|
|
770
|
+
return createHash('sha256').update(value).digest('hex').slice(0, AZURE_OPENAI_PROMPT_CACHE_HASH_LENGTH)
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function stablePromptCacheStringify(value: unknown): string {
|
|
774
|
+
return JSON.stringify(normalizePromptCacheValue(value, 0))
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function normalizePromptCacheValue(value: unknown, depth: number): unknown {
|
|
778
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'number') return value
|
|
779
|
+
if (typeof value === 'string') {
|
|
780
|
+
return value.length > AZURE_OPENAI_PROMPT_CACHE_MAX_STRING_LENGTH
|
|
781
|
+
? value.slice(0, AZURE_OPENAI_PROMPT_CACHE_MAX_STRING_LENGTH)
|
|
782
|
+
: value
|
|
783
|
+
}
|
|
784
|
+
if (typeof value === 'bigint') return value.toString()
|
|
785
|
+
if (value instanceof Uint8Array) {
|
|
786
|
+
return {
|
|
787
|
+
type: 'uint8array',
|
|
788
|
+
length: value.byteLength,
|
|
789
|
+
sha256: createHash('sha256').update(value).digest('hex').slice(0, AZURE_OPENAI_PROMPT_CACHE_HASH_LENGTH),
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
if (Array.isArray(value)) {
|
|
793
|
+
return value
|
|
794
|
+
.slice(0, AZURE_OPENAI_PROMPT_CACHE_MAX_ARRAY_ITEMS)
|
|
795
|
+
.map((item) => normalizePromptCacheValue(item, depth + 1))
|
|
796
|
+
}
|
|
797
|
+
if (!isRecord(value) || depth >= AZURE_OPENAI_PROMPT_CACHE_MAX_DEPTH) return null
|
|
798
|
+
|
|
799
|
+
return Object.fromEntries(
|
|
800
|
+
Object.entries(value)
|
|
801
|
+
.filter(([, item]) => item !== undefined && typeof item !== 'function' && typeof item !== 'symbol')
|
|
802
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
803
|
+
.slice(0, AZURE_OPENAI_PROMPT_CACHE_MAX_OBJECT_KEYS)
|
|
804
|
+
.map(([key, item]) => [key, normalizePromptCacheValue(item, depth + 1)]),
|
|
805
|
+
)
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function readExplicitOpenAiPromptCacheKey(openaiOptions: Record<string, unknown>): string | null {
|
|
809
|
+
const promptCacheKey = openaiOptions.promptCacheKey
|
|
810
|
+
return typeof promptCacheKey === 'string' && promptCacheKey.trim().length > 0 ? promptCacheKey.trim() : null
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function buildAzureOpenAiPromptCacheKey(params: AiGatewayCallOptions, modelId: string): string {
|
|
814
|
+
const payload = {
|
|
815
|
+
version: 1,
|
|
816
|
+
model: modelId.trim().toLowerCase(),
|
|
817
|
+
prompt: params.prompt,
|
|
818
|
+
responseFormat: params.responseFormat,
|
|
819
|
+
tools: params.tools,
|
|
820
|
+
}
|
|
821
|
+
return `${AZURE_OPENAI_PROMPT_CACHE_KEY_PREFIX}_${hashAzureOpenAiPromptCacheValue(stablePromptCacheStringify(payload))}`
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function buildAzureOpenAiPromptCacheSessionId(promptCacheKey: string): string {
|
|
825
|
+
if (promptCacheKey.startsWith(`${AZURE_OPENAI_PROMPT_CACHE_KEY_PREFIX}_`)) return promptCacheKey
|
|
826
|
+
return `${AZURE_OPENAI_PROMPT_CACHE_KEY_PREFIX}_${hashAzureOpenAiPromptCacheValue(promptCacheKey)}`
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function withHeaderIfMissing(
|
|
830
|
+
headers: Record<string, string | undefined>,
|
|
831
|
+
name: string,
|
|
832
|
+
value: string,
|
|
833
|
+
): Record<string, string | undefined> {
|
|
834
|
+
for (const key of Object.keys(headers)) {
|
|
835
|
+
if (key.toLowerCase() === name.toLowerCase()) return headers
|
|
836
|
+
}
|
|
837
|
+
return { ...headers, [name]: value }
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
export function addAzureOpenAiPromptCacheRetention(
|
|
841
|
+
params: AiGatewayCallOptions,
|
|
842
|
+
modelId?: string,
|
|
843
|
+
): AiGatewayCallOptions {
|
|
844
|
+
if (!modelId || !isAzureOpenAiPromptCacheModel(modelId)) {
|
|
845
|
+
return params
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const providerOptions = isRecord(params.providerOptions) ? { ...params.providerOptions } : {}
|
|
849
|
+
const openaiOptions = isRecord(providerOptions.openai) ? { ...providerOptions.openai } : {}
|
|
850
|
+
const promptCacheKey =
|
|
851
|
+
readExplicitOpenAiPromptCacheKey(openaiOptions) ?? buildAzureOpenAiPromptCacheKey(params, modelId)
|
|
852
|
+
const sessionId = buildAzureOpenAiPromptCacheSessionId(promptCacheKey)
|
|
853
|
+
const headersWithSession = withHeaderIfMissing(
|
|
854
|
+
withHeaderIfMissing({ ...params.headers }, AI_GATEWAY_SESSION_ID_HEADER, sessionId),
|
|
855
|
+
AI_GATEWAY_SESSION_TTL_HEADER,
|
|
856
|
+
AZURE_OPENAI_PROMPT_CACHE_SESSION_TTL,
|
|
857
|
+
)
|
|
858
|
+
|
|
859
|
+
return {
|
|
860
|
+
...params,
|
|
861
|
+
headers: headersWithSession,
|
|
862
|
+
providerOptions: {
|
|
863
|
+
...providerOptions,
|
|
864
|
+
openai: { ...openaiOptions, promptCacheKey, promptCacheRetention: AZURE_OPENAI_PROMPT_CACHE_RETENTION },
|
|
865
|
+
} as AiGatewayCallOptions['providerOptions'],
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
731
869
|
function mergeAbortSignals(signals: Array<AbortSignal | undefined>): { signal?: AbortSignal; cleanup: () => void } {
|
|
732
870
|
const activeSignals = signals.filter((signal): signal is AbortSignal => Boolean(signal))
|
|
733
871
|
if (activeSignals.length === 0) return { cleanup: () => undefined }
|
|
@@ -1106,7 +1244,9 @@ function createAiGatewayLanguageModelMiddleware(
|
|
|
1106
1244
|
Promise.resolve(
|
|
1107
1245
|
addAiGatewayReasoningRawChunks(
|
|
1108
1246
|
normalizeAiGatewayJsonSchemas(
|
|
1109
|
-
providerId === OPENAI_CHAT_PROVIDER_ID
|
|
1247
|
+
providerId === OPENAI_CHAT_PROVIDER_ID
|
|
1248
|
+
? normalizeAiGatewayChatProviderOptions(addAzureOpenAiPromptCacheRetention(params, modelId), modelId)
|
|
1249
|
+
: addAzureOpenAiPromptCacheRetention(params, modelId),
|
|
1110
1250
|
),
|
|
1111
1251
|
type,
|
|
1112
1252
|
),
|
package/src/ai-gateway/index.ts
CHANGED