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:
Theodore Li
2026-08-10 15:15:18 -04:00
committed by GitHub
parent bc8826af56
commit 56910c002e
42 changed files with 1999 additions and 247 deletions
+97
View File
@@ -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))
+18
View File
@@ -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',
+33
View File
@@ -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),
+9
View File
@@ -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')