fix(config): handle 'local-agent' provider in getLLMConfig (#118)

* fix(config): handle 'local-agent' provider in getLLMConfig

Routing a mission through a connected local CLI agent (Claude Code / Codex /
Hermes) sets the provider to `local-agent`, but getLLMConfig's switch had no
case for it, so it fell through to `default` and threw
`Unknown provider: local-agent`. This aborted every keyless mission with a
REFUSED, even though `local-agent` is a registered provider wired to
LocalAgentAdapter everywhere else.

Add a `case 'local-agent'` mirroring `codex`: keyless (no apiKey/baseUrl), with
the connected agent id (codex|claude|hermes) carried in `model` and a sensible
default. Verified getLLMConfig('local-agent','claude') now resolves to
{provider:'local-agent', model:'claude'} instead of throwing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(config): regression coverage for local-agent provider (#118)

Addresses review feedback: the fix had no test, so removing the
`case 'local-agent'` would leave CI green while re-breaking keyless missions.

Adds focused assertions in src/__tests__/local-agent-provider.test.ts:
- local-agent + model `claude` resolves to provider/model with no apiKey/baseUrl
- any supported agent id (codex|claude|hermes) passed as model is preserved
- omitting the model falls back to the default agent id
- getLLMConfig('local-agent') no longer throws
- AVAILABLE_MODELS['local-agent'] surfaces the connected-agent ids

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: hoptoad-off <mvzmysun@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-07-27 15:25:02 -04:00
1 parent 32b81708ea
commit e52c2fb2b7
2 files changed
+50

No files matched your search

@@ -0,0 +1,44 @@
import { describe, expect, it } from 'vitest';
import { config, AVAILABLE_MODELS } from '../config/index.js';
// Regression guard for #118: getLLMConfig had no `case 'local-agent'`, so it fell
// through to `default` and threw `Unknown provider: local-agent`, aborting every
// keyless (connected-agent) mission. These assertions fail if that case is removed.
describe("local-agent provider wiring (#118)", () => {
it('resolves a keyless config for local-agent with the agent id carried in model', () => {
const cfg = config.getLLMConfig('local-agent', 'claude');
expect(cfg.provider).toBe('local-agent');
expect(cfg.model).toBe('claude');
// Keyless: the connected CLI agent uses its own login — no API key or base URL.
expect(cfg.apiKey).toBeUndefined();
expect(cfg.baseUrl).toBeUndefined();
});
it('preserves any supported agent id passed as the model', () => {
for (const agent of ['codex', 'claude', 'hermes']) {
const cfg = config.getLLMConfig('local-agent', agent);
expect(cfg.provider).toBe('local-agent');
expect(cfg.model).toBe(agent);
expect(cfg.apiKey).toBeUndefined();
expect(cfg.baseUrl).toBeUndefined();
}
});
it('falls back to the default agent id when no model is given', () => {
const cfg = config.getLLMConfig('local-agent');
expect(cfg.provider).toBe('local-agent');
expect(cfg.model).toBe('claude');
expect(cfg.apiKey).toBeUndefined();
expect(cfg.baseUrl).toBeUndefined();
});
it('does not throw "Unknown provider" for local-agent', () => {
expect(() => config.getLLMConfig('local-agent')).not.toThrow();
});
it('surfaces the connected-agent ids as available local-agent models', () => {
expect(AVAILABLE_MODELS['local-agent']?.map(m => m.id)).toEqual(
expect.arrayContaining(['codex', 'claude', 'hermes']),
);
});
});
+6
View File
@@ -867,6 +867,12 @@ class ConfigManager {
case 'codex': case 'codex':
actualModel = model || this.config.get('codex').defaultModel; actualModel = model || this.config.get('codex').defaultModel;
break; break;
case 'local-agent':
// Keyless backbone: the mission is routed through a connected local CLI agent
// (Claude Code / Codex / Hermes), each using its own login — no API key or base
// URL. The chosen agent id (codex|claude|hermes) travels in the `model` field.
actualModel = model || 'claude';
break;
case 'mock': case 'mock':
actualModel = 'mock-model'; actualModel = 'mock-model';
break; break;