@lota-sdk/core 0.4.38 → 0.4.40

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.38",
3
+ "version": "0.4.40",
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.38",
35
+ "@lota-sdk/shared": "0.4.40",
36
36
  "@mendable/firecrawl-js": "^4.20.0",
37
37
  "@surrealdb/node": "^3.0.3",
38
38
  "ai": "^6.0.170",
@@ -1,13 +1,14 @@
1
1
  import { devToolsMiddleware } from '@ai-sdk/devtools'
2
2
  import { createOpenAI } from '@ai-sdk/openai'
3
3
  import type { JSONSchema7 } from '@ai-sdk/provider'
4
- import { wrapEmbeddingModel, wrapLanguageModel } from 'ai'
4
+ import { wrapLanguageModel } from 'ai'
5
5
  import type { LanguageModelMiddleware } from 'ai'
6
6
  import { Cause, Clock, Context, Duration, Effect, Fiber, Layer, Semaphore } from 'effect'
7
7
 
8
8
  import { DEFAULT_AI_GATEWAY_URL } from '../config/constants'
9
9
  import { ERROR_TAGS, AiGenerationError, ConfigurationError } from '../effect/errors'
10
10
  import { RuntimeConfigServiceTag } from '../effect/services'
11
+ import { openRouterEmbeddingModel } from '../embeddings/openrouter'
11
12
  import { isRecord, readString } from '../utils/string'
12
13
  import { buildAiGatewayCacheHeaders } from './cache-headers'
13
14
 
@@ -15,7 +16,6 @@ type AiGatewayChatResponse = { body?: unknown }
15
16
  type AiGatewayTransformParamsOptions = Parameters<NonNullable<LanguageModelMiddleware['transformParams']>>[0]
16
17
  type WrapStreamOptions = Parameters<NonNullable<LanguageModelMiddleware['wrapStream']>>[0]
17
18
  type AiGatewayLanguageModel = Parameters<typeof wrapLanguageModel>[0]['model']
18
- type AiGatewayEmbeddingModel = Parameters<typeof wrapEmbeddingModel>[0]['model']
19
19
  type AiGatewayCallOptions = WrapStreamOptions['params']
20
20
  type AiGatewayFunctionTool = Extract<NonNullable<AiGatewayCallOptions['tools']>[number], { type: 'function' }>
21
21
  type AiGatewayGenerateResult = Awaited<ReturnType<WrapStreamOptions['doGenerate']>>
@@ -44,8 +44,6 @@ 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'
46
46
  const OPENAI_CHAT_PROVIDER_ID = 'openai.chat'
47
- const OPENAI_EMBEDDING_PROVIDER_ID = 'openai.embedding'
48
- const OPENAI_EMBEDDING_MAX_PER_CALL = 2_048
49
47
  const RETRYABLE_NETWORK_ERROR_CODES = new Set([
50
48
  'ECONNABORTED',
51
49
  'ECONNREFUSED',
@@ -1243,23 +1241,6 @@ function createAiGatewayLanguageModelPlaceholder(modelId: string, providerId: st
1243
1241
  }
1244
1242
  }
1245
1243
 
1246
- function createAiGatewayEmbeddingModelPlaceholder(modelId: string): AiGatewayEmbeddingModel {
1247
- return {
1248
- specificationVersion: 'v3',
1249
- provider: OPENAI_EMBEDDING_PROVIDER_ID,
1250
- modelId,
1251
- maxEmbeddingsPerCall: OPENAI_EMBEDDING_MAX_PER_CALL,
1252
- supportsParallelCalls: true,
1253
- doEmbed: () =>
1254
- Promise.reject(
1255
- new Error(
1256
- `[ai-gateway] AiGateway embedding model ${modelId}.doEmbed was invoked without the gateway middleware; ` +
1257
- 'this call path should be fully handled by aiGatewayEmbeddingModel middleware.',
1258
- ),
1259
- ),
1260
- }
1261
- }
1262
-
1263
1244
  export function aiGatewayModel(modelId: string, deps?: AiGatewayDeps) {
1264
1245
  if (isOpenRouterModel(modelId)) {
1265
1246
  return aiGatewayChatModel(modelId, deps)
@@ -1286,28 +1267,8 @@ export function aiGatewayChatModel(modelId: string, deps?: AiGatewayDeps) {
1286
1267
  )
1287
1268
  }
1288
1269
 
1289
- export function aiGatewayEmbeddingModel(modelId: string, deps?: AiGatewayDeps) {
1290
- return wrapEmbeddingModel({
1291
- model: createAiGatewayEmbeddingModelPlaceholder(modelId),
1292
- middleware: {
1293
- specificationVersion: 'v3',
1294
- wrapEmbed: ({ params }) => {
1295
- const resolvedDeps = resolveAiGatewayDeps(deps)
1296
- const embeddingModel = resolvedDeps.gateway.provider.embeddingModel(modelId)
1297
- return resolvedDeps.runPromise(
1298
- withAiGatewayConcurrency(
1299
- withAiGatewayResilience(
1300
- 'ai-gateway.embed',
1301
- Effect.tryPromise({
1302
- try: () => embeddingModel.doEmbed(params),
1303
- catch: (cause) => classifyAiGatewayError('ai-gateway.embed', cause),
1304
- }),
1305
- ).pipe(Effect.withSpan('AiGateway.embed'), Effect.annotateSpans({ modelId })),
1306
- ).pipe(Effect.provideService(AiGatewayTag, resolvedDeps.gateway)),
1307
- )
1308
- },
1309
- },
1310
- })
1270
+ export function aiGatewayEmbeddingModel(modelId: string, _deps?: AiGatewayDeps) {
1271
+ return openRouterEmbeddingModel(modelId)
1311
1272
  }
1312
1273
 
1313
1274
  /**
@@ -1319,7 +1280,7 @@ export function aiGatewayEmbeddingModel(modelId: string, deps?: AiGatewayDeps) {
1319
1280
  export type AiGatewayModels = {
1320
1281
  model(modelId: string): ReturnType<typeof aiGatewayModel>
1321
1282
  chatModel(modelId: string): ReturnType<typeof aiGatewayChatModel>
1322
- embeddingModel(modelId: string): ReturnType<typeof aiGatewayEmbeddingModel>
1283
+ embeddingModel(modelId: string): ReturnType<typeof openRouterEmbeddingModel>
1323
1284
  openRouterResponseHealingModel(modelId: string): ReturnType<typeof aiGatewayOpenRouterResponseHealingModel>
1324
1285
  }
1325
1286
 
@@ -1327,7 +1288,7 @@ export function createAiGatewayModels(deps: AiGatewayDeps): AiGatewayModels {
1327
1288
  return {
1328
1289
  model: (modelId: string) => aiGatewayModel(modelId, deps),
1329
1290
  chatModel: (modelId: string) => aiGatewayChatModel(modelId, deps),
1330
- embeddingModel: (modelId: string) => aiGatewayEmbeddingModel(modelId, deps),
1291
+ embeddingModel: (modelId: string) => openRouterEmbeddingModel(modelId),
1331
1292
  openRouterResponseHealingModel: (modelId: string) => aiGatewayOpenRouterResponseHealingModel(modelId, deps),
1332
1293
  }
1333
1294
  }
@@ -0,0 +1,46 @@
1
+ import { createOpenAI } from '@ai-sdk/openai'
2
+
3
+ import { ConfigurationError } from '../effect/errors'
4
+
5
+ const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1'
6
+ const OPENROUTER_MODEL_PREFIX = 'openrouter/' as const
7
+ const OPENAI_TEXT_EMBEDDING_3_SMALL_MODEL_ID = 'openai/text-embedding-3-small'
8
+ const OPENROUTER_API_KEY_ENV = 'OPENROUTER_API_KEY'
9
+
10
+ type OpenRouterProvider = ReturnType<typeof createOpenAI>
11
+
12
+ let cachedProvider: { apiKey: string; provider: OpenRouterProvider } | null = null
13
+
14
+ function normalizeOpenRouterEmbeddingModelId(modelId: string): string {
15
+ const normalized = modelId.trim()
16
+ if (normalized === 'text-embedding-3-small') return OPENAI_TEXT_EMBEDDING_3_SMALL_MODEL_ID
17
+ return normalized.startsWith(OPENROUTER_MODEL_PREFIX) ? normalized.slice(OPENROUTER_MODEL_PREFIX.length) : normalized
18
+ }
19
+
20
+ function readOpenRouterApiKey(env: Record<string, string | undefined> = process.env): string {
21
+ const apiKey = env[OPENROUTER_API_KEY_ENV]?.trim()
22
+ if (!apiKey) {
23
+ throw new ConfigurationError({
24
+ message: `[embeddings-provider] ${OPENROUTER_API_KEY_ENV} is required for direct OpenRouter embeddings.`,
25
+ key: OPENROUTER_API_KEY_ENV,
26
+ })
27
+ }
28
+ return apiKey
29
+ }
30
+
31
+ function getOpenRouterProvider(apiKey: string): OpenRouterProvider {
32
+ if (cachedProvider?.apiKey === apiKey) return cachedProvider.provider
33
+
34
+ const provider = createOpenAI({ apiKey, baseURL: OPENROUTER_BASE_URL })
35
+ cachedProvider = { apiKey, provider }
36
+ return provider
37
+ }
38
+
39
+ export function openRouterEmbeddingModel(modelId: string) {
40
+ const normalizedModelId = normalizeOpenRouterEmbeddingModelId(modelId)
41
+ if (!normalizedModelId) {
42
+ throw new ConfigurationError({ message: '[embeddings-provider] Model id is required.', key: 'embeddingModelId' })
43
+ }
44
+
45
+ return getOpenRouterProvider(readOpenRouterApiKey()).embeddingModel(normalizedModelId)
46
+ }
@@ -1,10 +1,11 @@
1
1
  import { embed, embedMany } from 'ai'
2
2
  import { Schema, Effect } from 'effect'
3
3
 
4
- import { aiGatewayEmbeddingModel } from '../ai-gateway/ai-gateway'
5
4
  import { ERROR_TAGS, ConfigurationError } from '../effect/errors'
5
+ import { openRouterEmbeddingModel } from './openrouter'
6
6
 
7
7
  const SUPPORTED_EMBEDDING_PREFIXES = ['openai/', 'openrouter/'] as const
8
+ const SUPPORTED_BARE_EMBEDDING_MODEL_IDS = ['text-embedding-3-small'] as const
8
9
 
9
10
  type SharedEmbeddingCache = {
10
11
  get(model: string, text: string): Promise<number[] | null>
@@ -30,14 +31,18 @@ function resolveEmbeddingModel(modelId: string) {
30
31
  throw new ConfigurationError({ message: '[embeddings-provider] Model id is required.', key: 'embeddingModelId' })
31
32
  }
32
33
 
34
+ if (SUPPORTED_BARE_EMBEDDING_MODEL_IDS.includes(normalized as (typeof SUPPORTED_BARE_EMBEDDING_MODEL_IDS)[number])) {
35
+ return openRouterEmbeddingModel(`openai/${normalized}`)
36
+ }
37
+
33
38
  if (!SUPPORTED_EMBEDDING_PREFIXES.some((prefix) => normalized.startsWith(prefix))) {
34
39
  throw new ConfigurationError({
35
- message: `[embeddings-provider] Unsupported model id "${modelId}". Use one of: ${SUPPORTED_EMBEDDING_PREFIXES.join(', ')}*.`,
40
+ message: `[embeddings-provider] Unsupported model id "${modelId}". Use one of: ${SUPPORTED_EMBEDDING_PREFIXES.join(', ')}* or ${SUPPORTED_BARE_EMBEDDING_MODEL_IDS.join(', ')}.`,
36
41
  key: 'embeddingModelId',
37
42
  })
38
43
  }
39
44
 
40
- return aiGatewayEmbeddingModel(normalized)
45
+ return openRouterEmbeddingModel(normalized)
41
46
  }
42
47
 
43
48
  function normalizeEmbedding(embedding: readonly number[]): number[] {
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ export * from './ai-gateway'
4
4
  export * from './config'
5
5
  export * from './db'
6
6
  export * from './document'
7
+ export * from './embeddings/openrouter'
7
8
  export * from './queues'
8
9
  export * from './redis'
9
10
  export * from './runtime'
@@ -291,6 +291,7 @@ export const LOTA_RUNTIME_ENV_KEYS = Object.freeze([
291
291
  'REDIS_URL',
292
292
  'AI_GATEWAY_URL',
293
293
  'AI_GATEWAY_KEY',
294
+ 'OPENROUTER_API_KEY',
294
295
  'AI_EMBEDDING_MODEL',
295
296
  'AI_GATEWAY_MAX_CONCURRENCY',
296
297
  'S3_ENDPOINT',