Skip to content

Commit 2ea36e2

Browse files
author
Your Name
committed
feat(kbot): integrate specialist preferred provider routing for local hermes
1 parent 9a0d13a commit 2ea36e2

3 files changed

Lines changed: 79 additions & 2 deletions

File tree

packages/kbot/src/agent.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
estimateCost, isLocalProvider, warmOllamaModelCache,
1616
routeModelForTask, anthropicThinkingConfig, resolveModelAlias,
1717
anthropicRefusalFallback, dataRetentionNotice, modelRequiresDataRetention,
18+
isProviderConfigured, isProviderReachable, getProviderKey,
1819
type ByokProvider,
1920
} from './auth.js'
2021
import {
@@ -33,6 +34,7 @@ import {
3334
} from './tool-pipeline.js'
3435
import { formatContextForPrompt, type ProjectContext } from './context.js'
3536
import { getMatrixSystemPrompt, listAgents, createAgent, type MatrixAgent } from './matrix.js'
37+
import { getPreferredProvider } from './agents/specialists.js'
3638
import {
3739
buildLearningContext, buildFullLearningContext, findPattern, recordPattern, recordPatternFailure,
3840
cacheSolution, updateProfile, classifyTask, extractKeywords,
@@ -1173,6 +1175,17 @@ export async function runAgent(
11731175
}
11741176
}
11751177

1178+
// Step 1.75: Specialist preferred provider routing (NousResearch/Hermes Agent Integration, Phase 2)
1179+
if (options.agent) {
1180+
const preferred = getPreferredProvider(options.agent)
1181+
if (preferred && isProviderConfigured(preferred) && await isProviderReachable(preferred)) {
1182+
byokProvider = preferred
1183+
apiKey = getProviderKey(preferred)
1184+
isLocal = isLocalProvider(preferred)
1185+
ui.onInfo(`[routing] specialist ${options.agent} routed to preferred provider: ${preferred}`)
1186+
}
1187+
}
1188+
11761189
const tier = options.tier || 'free'
11771190

11781191
// Ensure lazy tools are loaded before building the tool list for the API.
@@ -1196,7 +1209,7 @@ export async function runAgent(
11961209
}
11971210

11981211
// OpenAI-compatible APIs cap at 128 tools per request
1199-
const providerConfig = getProvider(getByokProvider())
1212+
const providerConfig = getProvider(byokProvider)
12001213
if (providerConfig.apiStyle === 'openai' && tools.length > 128) {
12011214
// Prioritize core tools, then fill remaining slots
12021215
const core = tools.filter(t => CORE_TOOLS.has(t.name))

packages/kbot/src/auth.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// kbot Auth Tests
21
import { describe, it, expect, afterEach } from 'vitest'
32
import {
43
detectProvider,
@@ -16,6 +15,9 @@ import {
1615
anthropicRefusalFallback,
1716
modelRequiresDataRetention,
1817
dataRetentionNotice,
18+
isProviderConfigured,
19+
isProviderReachable,
20+
getProviderKey,
1921
PROVIDERS,
2022
} from './auth.js'
2123

@@ -322,4 +324,21 @@ describe('Anthropic model capabilities', () => {
322324
expect(models).not.toContain('claude-mythos-1')
323325
})
324326
})
327+
328+
describe('Provider Configuration and Reachability', () => {
329+
it('detects keyless providers as configured', () => {
330+
expect(isProviderConfigured('ollama')).toBe(true)
331+
expect(isProviderConfigured('hermes')).toBe(true)
332+
})
333+
334+
it('returns local API keys correctly', () => {
335+
expect(getProviderKey('ollama')).toBe('local')
336+
expect(getProviderKey('hermes')).toBe('local')
337+
})
338+
339+
it('identifies non-local cloud providers as reachable by default', async () => {
340+
const reachable = await isProviderReachable('openai')
341+
expect(reachable).toBe(true)
342+
})
343+
})
325344
})

packages/kbot/src/auth.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,51 @@ export function isKeylessProvider(provider: ByokProvider): boolean {
497497
return provider === 'ollama' || provider === 'lmstudio' || provider === 'jan' || provider === 'embedded' || provider === 'llada' || provider === 'hermes'
498498
}
499499

500+
/** Check if a provider is configured (either keyless, or has an API key/token available) */
501+
export function isProviderConfigured(provider: ByokProvider): boolean {
502+
if (isKeylessProvider(provider)) return true
503+
const config = loadConfig()
504+
if (config?.byok_provider === provider && config?.byok_key) return true
505+
const envKey = ENV_KEYS.find(k => k.provider === provider)?.env
506+
if (envKey && process.env[envKey]) return true
507+
return false
508+
}
509+
510+
/** Check if a provider is reachable (local servers only; cloud is assumed true) */
511+
export async function isProviderReachable(provider: ByokProvider): Promise<boolean> {
512+
if (!isLocalProvider(provider)) return true
513+
const p = PROVIDERS[provider]
514+
try {
515+
const url = new URL(p.apiUrl)
516+
const res = await fetch(`${url.protocol}//${url.host}`, {
517+
method: 'GET',
518+
signal: AbortSignal.timeout(500),
519+
})
520+
return true
521+
} catch {
522+
return false
523+
}
524+
}
525+
526+
/** Get the API key/token for a specific provider */
527+
export function getProviderKey(provider: ByokProvider): string | null {
528+
if (isKeylessProvider(provider)) return 'local'
529+
const config = loadConfig()
530+
if (config?.byok_provider === provider && config?.byok_key) {
531+
return config.byok_key
532+
}
533+
const envKey = ENV_KEYS.find(k => k.provider === provider)?.env
534+
if (envKey) {
535+
const val = process.env[envKey]
536+
if (val) return val
537+
}
538+
// Fallback: if the global key matches the requested provider style, reuse it
539+
if (config?.byok_key && detectProvider(config.byok_key) === provider) {
540+
return config.byok_key
541+
}
542+
return null
543+
}
544+
500545
/** Check if BYOK mode is enabled (via env var or config) */
501546
export function isByokEnabled(): boolean {
502547
// Local providers always work without keys

0 commit comments

Comments
 (0)