Merge PR #2600: fix(ai): add chat touchpoint to litellm proxy recipe

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

# Conflicts:
#	src/core/ai/recipes/litellm-proxy.ts
This commit is contained in:
Sinabina
2026-07-13 11:27:14 -07:00
2 changed files with 64 additions and 1 deletions
+10 -1
View File
@@ -20,6 +20,15 @@ export const litellmProxy: Recipe = {
setup_url: 'https://docs.litellm.ai/docs/proxy/quick_start',
},
touchpoints: {
chat: {
// Models depend on the proxy's config; the openai-compat tier accepts
// user-provided IDs and lets the proxied backend validate them.
models: [],
supports_tools: true,
supports_subagent_loop: false,
supports_prompt_cache: false,
price_last_verified: '2026-04-20',
},
embedding: {
// Models depend on the proxy's config; declare empties so wizard prompts user.
models: [],
@@ -42,5 +51,5 @@ export const litellmProxy: Recipe = {
supports_multimodal: true,
},
},
setup_hint: 'Run LiteLLM (https://docs.litellm.ai) in front of any provider; set LITELLM_BASE_URL (include the /v1 suffix if your proxy serves the OpenAI route there, e.g. http://localhost:4000/v1) + pass --embedding-model litellm:<model> and --embedding-dimensions <N>.',
setup_hint: 'Run LiteLLM (https://docs.litellm.ai) in front of any provider; set LITELLM_BASE_URL (include the /v1 suffix if your proxy serves the OpenAI route there, e.g. http://localhost:4000/v1), optionally set LITELLM_API_KEY, and use litellm:<model> for chat. For embeddings, also pass --embedding-model litellm:<model> and --embedding-dimensions <N>.',
};
+54
View File
@@ -0,0 +1,54 @@
import { describe, expect, test } from 'bun:test';
import { defaultResolveAuth } from '../../src/core/ai/gateway.ts';
import { assertTouchpoint } from '../../src/core/ai/model-resolver.ts';
import { getRecipe } from '../../src/core/ai/recipes/index.ts';
describe('recipe: litellm proxy', () => {
test('registered with expected openai-compatible shape', () => {
const r = getRecipe('litellm');
expect(r).toBeDefined();
expect(r!.id).toBe('litellm');
expect(r!.tier).toBe('openai-compat');
expect(r!.implementation).toBe('openai-compatible');
expect(r!.base_url_default).toBe('http://localhost:4000');
expect(r!.auth_env?.required ?? []).toEqual([]);
expect(r!.auth_env?.optional ?? []).toContain('LITELLM_BASE_URL');
expect(r!.auth_env?.optional ?? []).toContain('LITELLM_API_KEY');
});
test('chat touchpoint accepts arbitrary proxied model IDs', () => {
const r = getRecipe('litellm')!;
expect(r.touchpoints.chat).toBeDefined();
expect(r.touchpoints.chat!.models).toEqual([]);
expect(r.touchpoints.chat!.supports_tools).toBe(true);
expect(r.touchpoints.chat!.supports_subagent_loop).toBe(false);
expect(() => assertTouchpoint(r, 'chat', 'gpt-4o')).not.toThrow();
expect(() => assertTouchpoint(r, 'chat', 'deepseek-v4-pro')).not.toThrow();
});
test('embedding touchpoint still uses user-provided models and dimensions', () => {
const r = getRecipe('litellm')!;
expect(r.touchpoints.embedding).toBeDefined();
expect(r.touchpoints.embedding!.models).toEqual([]);
expect(r.touchpoints.embedding!.user_provided_models).toBe(true);
expect(r.touchpoints.embedding!.default_dims).toBe(0);
expect(r.touchpoints.embedding!.no_batch_cap).toBe(true);
});
test('default auth honors LITELLM_API_KEY and ignores URL-only config', () => {
const r = getRecipe('litellm')!;
const noAuth = defaultResolveAuth(r, {}, 'chat');
expect(noAuth.headerName).toBe('Authorization');
expect(noAuth.token).toBe('Bearer unauthenticated');
const urlOnly = defaultResolveAuth(r, { LITELLM_BASE_URL: 'http://proxy.example' }, 'chat');
expect(urlOnly.token).toBe('Bearer unauthenticated');
const withKey = defaultResolveAuth(r, {
LITELLM_BASE_URL: 'http://proxy.example',
LITELLM_API_KEY: 'sk-litellm-fake',
}, 'chat');
expect(withKey.token).toBe('Bearer sk-litellm-fake');
});
});