mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
feat(embeddings): add OpenRouter support (#6396)
* feat(knowledge): add OpenRouter embedding fallback * fix(knowledge): preserve successful embedding batches * feat(embeddings): add OpenRouter provider * fix(knowledge): bill only platform embedding tokens * test(embeddings): include OpenRouter provider * feat(embeddings): load OpenRouter model catalog * fix(embeddings): preserve legacy provider default * fix(embeddings): batch OpenRouter requests * fix(embeddings): reset stale OpenRouter model
This commit is contained in:
@@ -9,8 +9,8 @@ const QUERY_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/queries')
|
||||
const SELECTOR_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/selectors')
|
||||
|
||||
const BASELINE = {
|
||||
totalRoutes: 1009,
|
||||
zodRoutes: 1009,
|
||||
totalRoutes: 1010,
|
||||
zodRoutes: 1010,
|
||||
nonZodRoutes: 0,
|
||||
} as const
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
type EnvCapabilityValues,
|
||||
getCapabilityFields,
|
||||
hasEnvCapabilityValue,
|
||||
KNOWLEDGE_EMBEDDINGS_CAPABILITY,
|
||||
OAUTH_CLIENT_CAPABILITIES,
|
||||
type OAuthClientCapabilityField,
|
||||
type OAuthClientCapabilityId,
|
||||
@@ -730,6 +731,101 @@ export const KNOWLEDGE_SETUP = defineCapabilitySetup(OCR_CAPABILITY, {
|
||||
optionOrder: ['local', 'mistral', 'azure-mistral'],
|
||||
})
|
||||
|
||||
export const KNOWLEDGE_EMBEDDINGS_SETUP = defineCapabilitySetup(KNOWLEDGE_EMBEDDINGS_CAPABILITY, {
|
||||
label: 'Knowledge embeddings',
|
||||
message: 'Knowledge embedding provider?',
|
||||
actions: {},
|
||||
providers: {
|
||||
'azure-openai': {
|
||||
hint: 'preferred when configured; falls back to other configured providers',
|
||||
prompts: [
|
||||
{
|
||||
type: 'field',
|
||||
key: 'AZURE_OPENAI_ENDPOINT',
|
||||
input: 'text',
|
||||
required: true,
|
||||
validate: true,
|
||||
},
|
||||
{
|
||||
type: 'field',
|
||||
key: 'AZURE_OPENAI_API_VERSION',
|
||||
input: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
type: 'field',
|
||||
key: 'AZURE_OPENAI_API_KEY',
|
||||
input: 'secret',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
type: 'field',
|
||||
key: 'KB_OPENAI_MODEL_NAME',
|
||||
input: 'text',
|
||||
hint: 'optional Azure deployment name; defaults to the embedding model id',
|
||||
},
|
||||
],
|
||||
},
|
||||
openai: {
|
||||
hint: 'direct OpenAI with optional key rotation',
|
||||
prompts: [
|
||||
{
|
||||
type: 'choice',
|
||||
id: 'openai-credentials',
|
||||
message: 'OpenAI credentials?',
|
||||
options: [
|
||||
{
|
||||
id: 'single',
|
||||
label: 'Single API key',
|
||||
currentWhen: { kind: 'present', key: 'OPENAI_API_KEY' },
|
||||
prompts: [{ type: 'field', key: 'OPENAI_API_KEY', input: 'secret', required: true }],
|
||||
},
|
||||
{
|
||||
id: 'rotating',
|
||||
label: 'Rotating key pool',
|
||||
currentWhen: {
|
||||
kind: 'any',
|
||||
conditions: [
|
||||
{ kind: 'present', key: 'OPENAI_API_KEY_1' },
|
||||
{ kind: 'present', key: 'OPENAI_API_KEY_2' },
|
||||
{ kind: 'present', key: 'OPENAI_API_KEY_3' },
|
||||
],
|
||||
},
|
||||
prompts: [
|
||||
{ type: 'field', key: 'OPENAI_API_KEY_1', input: 'secret', required: true },
|
||||
{
|
||||
type: 'field',
|
||||
key: 'OPENAI_API_KEY_2',
|
||||
input: 'secret',
|
||||
hint: 'optional rotating key',
|
||||
},
|
||||
{
|
||||
type: 'field',
|
||||
key: 'OPENAI_API_KEY_3',
|
||||
input: 'secret',
|
||||
hint: 'optional rotating key',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
openrouter: {
|
||||
hint: 'fallback for OpenAI knowledge embedding models',
|
||||
prompts: [
|
||||
{
|
||||
type: 'field',
|
||||
key: 'OPENROUTER_API_KEY',
|
||||
input: 'secret',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
optionOrder: ['azure-openai', 'openai', 'openrouter'],
|
||||
})
|
||||
|
||||
export const CAPABILITY_SETUPS = [
|
||||
EMAIL_SETUP,
|
||||
STORAGE_SETUP,
|
||||
@@ -737,6 +833,7 @@ export const CAPABILITY_SETUPS = [
|
||||
JOBS_SETUP,
|
||||
CACHE_SETUP,
|
||||
KNOWLEDGE_SETUP,
|
||||
KNOWLEDGE_EMBEDDINGS_SETUP,
|
||||
] as const
|
||||
|
||||
const configuredCapabilityIds = new Set(CAPABILITY_SETUPS.map((setup) => setup.definition.id))
|
||||
|
||||
@@ -14,6 +14,10 @@ describe('env capability status', () => {
|
||||
expect(status.features.jobs).toMatchObject({ state: 'default', providerId: 'database' })
|
||||
expect(status.features.cache).toMatchObject({ state: 'default', providerId: 'database' })
|
||||
expect(status.features.knowledge).toMatchObject({ state: 'default', providerId: 'local' })
|
||||
expect(status.features['knowledge-embeddings']).toMatchObject({
|
||||
state: 'missing',
|
||||
providerIds: [],
|
||||
})
|
||||
expect(status.features.llm).toMatchObject({
|
||||
state: 'missing',
|
||||
configuredPoolCount: 0,
|
||||
@@ -60,6 +64,20 @@ describe('env capability status', () => {
|
||||
expect(JSON.stringify(status)).not.toContain('secret-private-key')
|
||||
})
|
||||
|
||||
it('reports configured knowledge embedding transports without exposing keys', () => {
|
||||
const status = buildEnvCapabilityStatus({
|
||||
OPENAI_API_KEY: 'secret-openai-key',
|
||||
OPENROUTER_API_KEY: 'secret-openrouter-key',
|
||||
})
|
||||
|
||||
expect(status.features['knowledge-embeddings']).toMatchObject({
|
||||
state: 'configured',
|
||||
providerIds: ['openai', 'openrouter'],
|
||||
})
|
||||
expect(JSON.stringify(status)).not.toContain('secret-openai-key')
|
||||
expect(JSON.stringify(status)).not.toContain('secret-openrouter-key')
|
||||
})
|
||||
|
||||
it('reports selected Daytona and cloud storage providers', () => {
|
||||
const status = buildEnvCapabilityStatus({
|
||||
SANDBOX_PROVIDER: 'daytona',
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
inspectCapability,
|
||||
inspectOAuthClientCapability,
|
||||
isTruthyEnvCapabilityValue,
|
||||
KNOWLEDGE_EMBEDDINGS_CAPABILITY,
|
||||
LLM_KEY_POOLS,
|
||||
OAUTH_CLIENT_CAPABILITIES,
|
||||
type OAuthClientCapabilityId,
|
||||
@@ -43,6 +44,8 @@ interface FeatureStatusBase<TId extends SetupStatusFeatureId> {
|
||||
}
|
||||
|
||||
type EmailProviderId = (typeof EMAIL_CAPABILITY.providers)[number]['id']
|
||||
type KnowledgeEmbeddingsProviderId =
|
||||
(typeof KNOWLEDGE_EMBEDDINGS_CAPABILITY.providers)[number]['id']
|
||||
type StorageProviderId =
|
||||
| (typeof STORAGE_CAPABILITY)['defaultProvider']['id']
|
||||
| (typeof STORAGE_CAPABILITY.providers)[number]['id']
|
||||
@@ -77,6 +80,13 @@ export interface KnowledgeCapabilityStatus extends FeatureStatusBase<'knowledge'
|
||||
providerId: 'local' | 'mistral' | 'azure-mistral' | null
|
||||
}
|
||||
|
||||
export interface KnowledgeEmbeddingsCapabilityStatus
|
||||
extends FeatureStatusBase<'knowledge-embeddings'> {
|
||||
strategy: 'fallback'
|
||||
providerIds: readonly KnowledgeEmbeddingsProviderId[]
|
||||
providers: readonly ProviderInspection<KnowledgeEmbeddingsProviderId>[]
|
||||
}
|
||||
|
||||
export interface LlmKeyPoolStatus {
|
||||
id: LlmKeyPoolId
|
||||
state: 'configured' | 'missing'
|
||||
@@ -99,6 +109,7 @@ interface FeatureStatusById {
|
||||
jobs: JobsCapabilityStatus
|
||||
cache: CacheCapabilityStatus
|
||||
knowledge: KnowledgeCapabilityStatus
|
||||
'knowledge-embeddings': KnowledgeEmbeddingsCapabilityStatus
|
||||
llm: LlmCapabilityStatus
|
||||
}
|
||||
|
||||
@@ -391,6 +402,26 @@ function inspectKnowledge(values: EnvCapabilityValues): KnowledgeCapabilityStatu
|
||||
}
|
||||
}
|
||||
|
||||
function inspectKnowledgeEmbeddings(
|
||||
values: EnvCapabilityValues
|
||||
): KnowledgeEmbeddingsCapabilityStatus {
|
||||
const inspection = inspectCapability(KNOWLEDGE_EMBEDDINGS_CAPABILITY, values)
|
||||
const brokenState = brokenProviderState(inspection.providers)
|
||||
const state = inspection.configured ? 'configured' : (brokenState ?? 'missing')
|
||||
const configurationError =
|
||||
inspection.error ??
|
||||
getCapabilityConfigurationError(KNOWLEDGE_EMBEDDINGS_CAPABILITY, inspection.providers)
|
||||
|
||||
return {
|
||||
...featureMetadata('knowledge-embeddings'),
|
||||
strategy: 'fallback',
|
||||
state,
|
||||
providerIds: inspection.providerIds,
|
||||
providers: inspection.providers,
|
||||
...(configurationError ? { issue: issue(brokenState ?? 'invalid', configurationError) } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
function inspectLlm(values: EnvCapabilityValues): LlmCapabilityStatus {
|
||||
const pools = {} as Record<LlmKeyPoolId, LlmKeyPoolStatus>
|
||||
let configuredPoolCount = 0
|
||||
@@ -463,6 +494,7 @@ const FEATURE_STATUS_BUILDERS = {
|
||||
jobs: inspectJobs,
|
||||
cache: inspectCache,
|
||||
knowledge: inspectKnowledge,
|
||||
'knowledge-embeddings': inspectKnowledgeEmbeddings,
|
||||
llm: inspectLlm,
|
||||
} satisfies Record<SetupStatusFeatureId, (values: EnvCapabilityValues) => unknown>
|
||||
|
||||
@@ -476,6 +508,7 @@ export function buildEnvCapabilityStatus(values: EnvCapabilityValues): EnvCapabi
|
||||
jobs: FEATURE_STATUS_BUILDERS.jobs(values),
|
||||
cache: FEATURE_STATUS_BUILDERS.cache(values),
|
||||
knowledge: FEATURE_STATUS_BUILDERS.knowledge(values),
|
||||
'knowledge-embeddings': FEATURE_STATUS_BUILDERS['knowledge-embeddings'](values),
|
||||
llm: FEATURE_STATUS_BUILDERS.llm(values),
|
||||
},
|
||||
oauthClients: inspectOAuthClients(values),
|
||||
|
||||
@@ -104,6 +104,15 @@ function featureDetail(feature: FeatureStatus): string {
|
||||
if (feature.providerId === 'local') return 'Local parser (default)'
|
||||
if (feature.providerId === 'azure-mistral') return 'Azure Mistral OCR'
|
||||
return feature.providerId === 'mistral' ? 'Mistral OCR' : 'Not configured'
|
||||
case 'knowledge-embeddings':
|
||||
return feature.providerIds.length > 0
|
||||
? feature.providerIds
|
||||
.map(
|
||||
(id) =>
|
||||
feature.providers.find((provider) => provider.id === id)?.label ?? titleCase(id)
|
||||
)
|
||||
.join(' → ')
|
||||
: 'Not configured'
|
||||
case 'llm': {
|
||||
const configured = Object.values(feature.pools)
|
||||
.filter((pool) => pool.state === 'configured')
|
||||
|
||||
Reference in New Issue
Block a user