Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Open
19 changes: 19 additions & 0 deletions 19 .agents/types/agent-definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,25 @@ export interface AgentDefinition {
audio?: number | string
request?: number | string
}
/**
* Override the upstream LLM endpoint with an OpenAI-compatible base URL.
* When set, this agent's LLM calls bypass the Codebuff backend / OpenRouter
* and go directly to `${baseUrl}/chat/completions`.
*
* Use for local models (Ollama, LM Studio) or self-hosted OpenAI-compatible
* providers. The other providerOptions keys (order, allow_fallbacks, etc.)
* are OpenRouter-specific and ignored when `baseUrl` is set.
*
* Falls back to env var CODEBUFF_BASE_URL when unset.
* Example: "http://localhost:11434/v1"
*/
baseUrl?: string
/**
* API key for the endpoint set in `baseUrl`. Ignored if `baseUrl` is unset.
* Falls back to env var CODEBUFF_PROVIDER_API_KEY. Most local runtimes
* (Ollama, LM Studio) ignore the value entirely.
*/
apiKey?: string
}

// ============================================================================
Expand Down
19 changes: 19 additions & 0 deletions 19 agents/types/agent-definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,25 @@ export interface AgentDefinition {
audio?: number | string
request?: number | string
}
/**
* Override the upstream LLM endpoint with an OpenAI-compatible base URL.
* When set, this agent's LLM calls bypass the Codebuff backend / OpenRouter
* and go directly to `${baseUrl}/chat/completions`.
*
* Use for local models (Ollama, LM Studio) or self-hosted OpenAI-compatible
* providers. The other providerOptions keys (order, allow_fallbacks, etc.)
* are OpenRouter-specific and ignored when `baseUrl` is set.
*
* Falls back to env var CODEBUFF_BASE_URL when unset.
* Example: "http://localhost:11434/v1"
*/
baseUrl?: string
/**
* API key for the endpoint set in `baseUrl`. Ignored if `baseUrl` is unset.
* Falls back to env var CODEBUFF_PROVIDER_API_KEY. Most local runtimes
* (Ollama, LM Studio) ignore the value entirely.
*/
apiKey?: string
}

// ============================================================================
Expand Down
344 changes: 344 additions & 0 deletions 344 cli/src/commands/__tests__/local-provider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,344 @@
import { describe, expect, test, beforeEach, afterEach } from 'bun:test'

import {
applyLocalAction,
DEFAULT_LOCAL_BASE_URL,
getActiveLocalBaseUrl,
getActiveLocalModel,
parseLocalArgs,
} from '../local-provider'

describe('parseLocalArgs — basic shapes', () => {
test('empty args → status', () => {
expect(parseLocalArgs('').kind).toBe('status')
expect(parseLocalArgs(' ').kind).toBe('status')
expect(parseLocalArgs('\t\n').kind).toBe('status')
})

test('"status" → status', () => {
expect(parseLocalArgs('status').kind).toBe('status')
expect(parseLocalArgs(' status ').kind).toBe('status')
expect(parseLocalArgs('STATUS').kind).toBe('status')
})

test('"list" / "models" → list', () => {
expect(parseLocalArgs('list').kind).toBe('list')
expect(parseLocalArgs('models').kind).toBe('list')
})

test('"off" → disable', () => {
expect(parseLocalArgs('off').kind).toBe('disable')
expect(parseLocalArgs('disable').kind).toBe('disable')
})

test('"off" with stray args → invalid', () => {
const r = parseLocalArgs('off http://oops')
expect(r.kind).toBe('invalid')
})

test('unknown subcommand → invalid', () => {
const r = parseLocalArgs('foobar')
expect(r.kind).toBe('invalid')
if (r.kind === 'invalid') expect(r.reason).toContain('Unknown')
})
})

describe('parseLocalArgs — enable shapes', () => {
test('"on" → enable with default URL, no model', () => {
const r = parseLocalArgs('on')
expect(r.kind).toBe('enable')
if (r.kind === 'enable') {
expect(r.baseUrl).toBe(DEFAULT_LOCAL_BASE_URL)
expect(r.model).toBeUndefined()
}
})

test('"on <url>" → enable with URL only', () => {
const r = parseLocalArgs('on http://localhost:1234/v1')
expect(r.kind).toBe('enable')
if (r.kind === 'enable') {
expect(r.baseUrl).toBe('http://localhost:1234/v1')
expect(r.model).toBeUndefined()
}
})

test('"on <model>" (model only, no URL) → enable with default URL + model', () => {
const r = parseLocalArgs('on llama3.1:8b')
expect(r.kind).toBe('enable')
if (r.kind === 'enable') {
expect(r.baseUrl).toBe(DEFAULT_LOCAL_BASE_URL)
expect(r.model).toBe('llama3.1:8b')
}
})

test('"on <url> <model>" → both set', () => {
const r = parseLocalArgs('on http://localhost:1234/v1 llama3.1:8b')
expect(r.kind).toBe('enable')
if (r.kind === 'enable') {
expect(r.baseUrl).toBe('http://localhost:1234/v1')
expect(r.model).toBe('llama3.1:8b')
}
})

test('"enable <url>" and "set <model>" aliases work', () => {
const a = parseLocalArgs('enable http://x:1/v1')
expect(a.kind).toBe('enable')
const b = parseLocalArgs('set gemma4:e2b')
expect(b.kind).toBe('enable')
if (b.kind === 'enable') expect(b.model).toBe('gemma4:e2b')
})

test('bare URL → enable', () => {
const r = parseLocalArgs('http://localhost:11434/v1')
expect(r.kind).toBe('enable')
if (r.kind === 'enable') expect(r.baseUrl).toBe('http://localhost:11434/v1')
})

test('bare model tag → enable with default URL + model', () => {
const r = parseLocalArgs('llama3.1:8b')
expect(r.kind).toBe('enable')
if (r.kind === 'enable') {
expect(r.baseUrl).toBe(DEFAULT_LOCAL_BASE_URL)
expect(r.model).toBe('llama3.1:8b')
}
})

test('non-http URL → invalid', () => {
const r = parseLocalArgs('on ftp://localhost')
expect(r.kind).toBe('invalid')
})

test('malformed URL → invalid', () => {
const r = parseLocalArgs('on http://')
expect(r.kind).toBe('invalid')
})

test('https URL accepted', () => {
const r = parseLocalArgs('on https://my-vm.example.com:8080/v1 llama3.1:8b')
expect(r.kind).toBe('enable')
if (r.kind === 'enable')
expect(r.baseUrl).toBe('https://my-vm.example.com:8080/v1')
})
})

describe('parseLocalArgs — model subcommand', () => {
test('"model <name>" → set-model', () => {
const r = parseLocalArgs('model llama3.1:8b')
expect(r.kind).toBe('set-model')
if (r.kind === 'set-model') expect(r.model).toBe('llama3.1:8b')
})

test('"model clear" / "model off" / "model none" → clear-model', () => {
expect(parseLocalArgs('model clear').kind).toBe('clear-model')
expect(parseLocalArgs('model off').kind).toBe('clear-model')
expect(parseLocalArgs('model none').kind).toBe('clear-model')
})

test('"model" without name → invalid', () => {
const r = parseLocalArgs('model')
expect(r.kind).toBe('invalid')
})

test('"model <flag>" → invalid', () => {
const r = parseLocalArgs('model --x')
expect(r.kind).toBe('invalid')
})
})

describe('applyLocalAction (side effects on process.env)', () => {
let originalBaseUrl: string | undefined
let originalApiKey: string | undefined
let originalModel: string | undefined

beforeEach(() => {
originalBaseUrl = process.env.CODEBUFF_BASE_URL
originalApiKey = process.env.CODEBUFF_PROVIDER_API_KEY
originalModel = process.env.CODEBUFF_PROVIDER_MODEL
delete process.env.CODEBUFF_BASE_URL
delete process.env.CODEBUFF_PROVIDER_API_KEY
delete process.env.CODEBUFF_PROVIDER_MODEL
})

afterEach(() => {
if (originalBaseUrl === undefined) delete process.env.CODEBUFF_BASE_URL
else process.env.CODEBUFF_BASE_URL = originalBaseUrl
if (originalApiKey === undefined)
delete process.env.CODEBUFF_PROVIDER_API_KEY
else process.env.CODEBUFF_PROVIDER_API_KEY = originalApiKey
if (originalModel === undefined) delete process.env.CODEBUFF_PROVIDER_MODEL
else process.env.CODEBUFF_PROVIDER_MODEL = originalModel
})

test('enable without model sets baseUrl, clears any previous model override', async () => {
process.env.CODEBUFF_PROVIDER_MODEL = 'stale-model'
const msg = await applyLocalAction({
kind: 'enable',
baseUrl: 'http://localhost:11434/v1',
})
expect(process.env.CODEBUFF_BASE_URL).toBe('http://localhost:11434/v1')
expect(process.env.CODEBUFF_PROVIDER_MODEL).toBeUndefined()
expect(msg).toContain('ON')
expect(msg).toContain('No model override')
expect(msg).toContain('llama3.1:8b')
})

test('enable with model sets both env vars', async () => {
const msg = await applyLocalAction({
kind: 'enable',
baseUrl: 'http://localhost:11434/v1',
model: 'llama3.1:8b',
})
expect(process.env.CODEBUFF_BASE_URL).toBe('http://localhost:11434/v1')
expect(process.env.CODEBUFF_PROVIDER_MODEL).toBe('llama3.1:8b')
expect(msg).toContain('Model override: llama3.1:8b')
})

test('set-model when local is OFF → error', async () => {
const msg = await applyLocalAction({
kind: 'set-model',
model: 'llama3.1:8b',
})
expect(process.env.CODEBUFF_PROVIDER_MODEL).toBeUndefined()
expect(msg).toContain('OFF')
})

test('set-model when local is ON → updates model', async () => {
process.env.CODEBUFF_BASE_URL = 'http://localhost:11434/v1'
const msg = await applyLocalAction({
kind: 'set-model',
model: 'llama3.1:8b',
})
expect(process.env.CODEBUFF_PROVIDER_MODEL).toBe('llama3.1:8b')
expect(msg).toContain('Model override: llama3.1:8b')
})

test('clear-model removes only the model, keeps baseUrl', async () => {
process.env.CODEBUFF_BASE_URL = 'http://localhost:11434/v1'
process.env.CODEBUFF_PROVIDER_MODEL = 'llama3.1:8b'
const msg = await applyLocalAction({ kind: 'clear-model' })
expect(process.env.CODEBUFF_BASE_URL).toBe('http://localhost:11434/v1')
expect(process.env.CODEBUFF_PROVIDER_MODEL).toBeUndefined()
expect(msg).toContain('cleared')
})

test('clear-model when none set is friendly', async () => {
const msg = await applyLocalAction({ kind: 'clear-model' })
expect(msg).toContain('No model override')
})

test('disable clears baseUrl, apiKey, and model', async () => {
process.env.CODEBUFF_BASE_URL = 'http://localhost:11434/v1'
process.env.CODEBUFF_PROVIDER_API_KEY = 'ollama'
process.env.CODEBUFF_PROVIDER_MODEL = 'llama3.1:8b'
const msg = await applyLocalAction({ kind: 'disable' })
expect(process.env.CODEBUFF_BASE_URL).toBeUndefined()
expect(process.env.CODEBUFF_PROVIDER_API_KEY).toBeUndefined()
expect(process.env.CODEBUFF_PROVIDER_MODEL).toBeUndefined()
expect(msg).toContain('OFF')
expect(msg).toContain('llama3.1:8b')
})

test('disable when already off → idempotent', async () => {
const msg = await applyLocalAction({ kind: 'disable' })
expect(msg).toContain('already OFF')
})

test('status when off mentions /local list and shows usage', async () => {
const msg = await applyLocalAction({ kind: 'status' })
expect(msg).toContain('OFF')
expect(msg).toContain('/local list')
})

test('status when on with model shows both URL and model', async () => {
process.env.CODEBUFF_BASE_URL = 'http://localhost:1234/v1'
process.env.CODEBUFF_PROVIDER_MODEL = 'llama3.1:8b'
const msg = await applyLocalAction({ kind: 'status' })
expect(msg).toContain('ON')
expect(msg).toContain('http://localhost:1234/v1')
expect(msg).toContain('llama3.1:8b')
})

test('status when on without model warns about no model override', async () => {
process.env.CODEBUFF_BASE_URL = 'http://localhost:11434/v1'
const msg = await applyLocalAction({ kind: 'status' })
expect(msg).toContain('ON')
expect(msg).toContain('(none')
})

test('invalid returns reason prefixed', async () => {
const msg = await applyLocalAction({
kind: 'invalid',
reason: 'something wrong',
})
expect(msg).toContain('something wrong')
})

test('list when off returns error', async () => {
const msg = await applyLocalAction({ kind: 'list' })
expect(msg).toContain('OFF')
})
})

describe('parseLocalArgs + applyLocalAction end-to-end', () => {
let originalBaseUrl: string | undefined
let originalModel: string | undefined

beforeEach(() => {
originalBaseUrl = process.env.CODEBUFF_BASE_URL
originalModel = process.env.CODEBUFF_PROVIDER_MODEL
delete process.env.CODEBUFF_BASE_URL
delete process.env.CODEBUFF_PROVIDER_MODEL
})

afterEach(() => {
if (originalBaseUrl === undefined) delete process.env.CODEBUFF_BASE_URL
else process.env.CODEBUFF_BASE_URL = originalBaseUrl
if (originalModel === undefined) delete process.env.CODEBUFF_PROVIDER_MODEL
else process.env.CODEBUFF_PROVIDER_MODEL = originalModel
})

test('user types `/local on llama3.1:8b` → URL default + model set', async () => {
await applyLocalAction(parseLocalArgs('on llama3.1:8b'))
expect(process.env.CODEBUFF_BASE_URL).toBe(DEFAULT_LOCAL_BASE_URL)
expect(process.env.CODEBUFF_PROVIDER_MODEL).toBe('llama3.1:8b')
})

test('user types `/local llama3.1:8b` (no `on`) → same effect', async () => {
await applyLocalAction(parseLocalArgs('llama3.1:8b'))
expect(process.env.CODEBUFF_BASE_URL).toBe(DEFAULT_LOCAL_BASE_URL)
expect(process.env.CODEBUFF_PROVIDER_MODEL).toBe('llama3.1:8b')
})

test('user types `/local on http://x/v1 llama3.1:8b` → both set', async () => {
await applyLocalAction(parseLocalArgs('on http://x.example.com:9999/v1 llama3.1:8b'))
expect(process.env.CODEBUFF_BASE_URL).toBe('http://x.example.com:9999/v1')
expect(process.env.CODEBUFF_PROVIDER_MODEL).toBe('llama3.1:8b')
})

test('user types `/local model llama3.1:8b` after `/local on` → model added', async () => {
await applyLocalAction(parseLocalArgs('on'))
expect(process.env.CODEBUFF_PROVIDER_MODEL).toBeUndefined()
await applyLocalAction(parseLocalArgs('model llama3.1:8b'))
expect(process.env.CODEBUFF_PROVIDER_MODEL).toBe('llama3.1:8b')
})

test('user types `/local off` → both cleared', async () => {
await applyLocalAction(parseLocalArgs('on llama3.1:8b'))
await applyLocalAction(parseLocalArgs('off'))
expect(process.env.CODEBUFF_BASE_URL).toBeUndefined()
expect(process.env.CODEBUFF_PROVIDER_MODEL).toBeUndefined()
})

test('mutations are visible via getter functions', async () => {
await applyLocalAction(parseLocalArgs('on llama3.1:8b'))
expect(getActiveLocalBaseUrl()).toBe(DEFAULT_LOCAL_BASE_URL)
expect(getActiveLocalModel()).toBe('llama3.1:8b')
})

test('re-enabling without model clears previous model override', async () => {
await applyLocalAction(parseLocalArgs('on llama3.1:8b'))
await applyLocalAction(parseLocalArgs('on'))
expect(getActiveLocalBaseUrl()).toBe(DEFAULT_LOCAL_BASE_URL)
expect(getActiveLocalModel()).toBeUndefined()
})
})
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.