@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lota-sdk/core",
3
- "version": "0.4.46",
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.46",
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({ baseURL, apiKey, headers: { [AI_GATEWAY_VIRTUAL_KEY_HEADER]: apiKey } })
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 ? normalizeAiGatewayChatProviderOptions(params, modelId) : params,
1247
+ providerId === OPENAI_CHAT_PROVIDER_ID
1248
+ ? normalizeAiGatewayChatProviderOptions(addAzureOpenAiPromptCacheRetention(params, modelId), modelId)
1249
+ : addAzureOpenAiPromptCacheRetention(params, modelId),
1110
1250
  ),
1111
1251
  type,
1112
1252
  ),
@@ -9,6 +9,7 @@ export {
9
9
  aiGatewayModel,
10
10
  aiGatewayOpenRouterResponseHealingModel,
11
11
  createAiGatewayModels,
12
+ addAzureOpenAiPromptCacheRetention,
12
13
  extractAiGatewayChatReasoningDeltaText,
13
14
  extractAiGatewayChatReasoningText,
14
15
  injectAiGatewayChatReasoningContent,