codeep 2.1.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -56,6 +56,7 @@ custom slash commands, lifecycle hooks, checkpoints, `/cost`,
56
56
  - **MiniMax** — MiniMax M2.7, M2.5, M2.1, M2 — Coding Plan & pay-per-use API (international & China)
57
57
  - **Ollama** — Run any model locally or on a remote server, no API key required. Models are fetched dynamically from your Ollama instance.
58
58
  - **OpenRouter** — One key, 100+ models from Anthropic, OpenAI, Google, Meta, Mistral, DeepSeek, Qwen, xAI and more. Per-call cost reported directly by OpenRouter (matches their dashboard exactly). Use `openrouter/auto` to let OpenRouter pick the best model. Tune routing with `/openrouter prefer|ignore|fallbacks|privacy`.
59
+ - **Custom (OpenAI-compatible)** — Point Codeep at any self-hosted or proxied OpenAI-compatible endpoint (vLLM, LiteLLM, LM Studio, text-generation-webui). Set the base URL in `/settings` → **Custom Base URL** (config key `customBaseUrl`, e.g. `http://host:8000/v1`), then pick your model with `/model` (fetched from the server's `/models`). No API key required unless your endpoint enforces one. The `openai` provider also honors the `OPENAI_BASE_URL` env var for proxies that serve `gpt-*` model names.
59
60
  - Switch between providers with `/provider`
60
61
  - Configure different API keys per provider
61
62
  - Both OpenAI-compatible and Anthropic API protocols supported
package/dist/api/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as http from 'node:http';
2
2
  import * as https from 'node:https';
3
- import { config, getApiKey } from '../config/index.js';
3
+ import { config, getApiKey, resolveBaseUrl } from '../config/index.js';
4
4
  import { withRetry, isNetworkError } from '../utils/retry.js';
5
5
  import { getProvider, getProviderBaseUrl, getProviderAuthHeader, usesMaxCompletionTokens, requiresDefaultTemperature } from '../config/providers.js';
6
6
  import { logApiRequest, logApiResponse } from '../utils/logger.js';
@@ -336,14 +336,11 @@ async function chatOpenAI(message, history, model, apiKey, onChunk, abortSignal)
336
336
  const timeout = config.get('apiTimeout');
337
337
  const temperature = config.get('temperature');
338
338
  const maxTokens = config.get('maxTokens');
339
- // Get provider-specific URL and auth
339
+ // Get provider-specific URL and auth. resolveBaseUrl applies user
340
+ // overrides: Ollama (ollamaUrl), Custom (customBaseUrl), and OpenAI
341
+ // (OPENAI_BASE_URL env) — so self-hosted / OpenAI-compatible endpoints work.
340
342
  const providerId = config.get('provider');
341
- let baseUrl = getProviderBaseUrl(providerId, 'openai');
342
- // For Ollama, use the configured URL (can't use lazy require in ESM providers.ts)
343
- if (providerId === 'ollama') {
344
- const ollamaUrl = (config.get('ollamaUrl') || 'http://localhost:11434').replace(/\/$/, '');
345
- baseUrl = `${ollamaUrl}/v1`;
346
- }
343
+ let baseUrl = resolveBaseUrl(providerId, 'openai');
347
344
  const authHeader = getProviderAuthHeader(providerId, 'openai');
348
345
  const useCompletionTokens = usesMaxCompletionTokens(providerId);
349
346
  const omitTemperature = requiresDefaultTemperature(providerId);
@@ -747,11 +744,7 @@ export async function validateApiKey(apiKey, providerId) {
747
744
  }
748
745
  // Determine which protocol to use for validation
749
746
  const protocol = providerConfig.defaultProtocol;
750
- let baseUrl = getProviderBaseUrl(provider, protocol);
751
- if (provider === 'ollama' && protocol === 'openai') {
752
- const ollamaUrl = (config.get('ollamaUrl') || 'http://localhost:11434').replace(/\/$/, '');
753
- baseUrl = `${ollamaUrl}/v1`;
754
- }
747
+ const baseUrl = resolveBaseUrl(provider, protocol);
755
748
  const authHeader = getProviderAuthHeader(provider, protocol);
756
749
  const model = providerConfig.defaultModel;
757
750
  if (!baseUrl) {
@@ -35,6 +35,7 @@ interface ConfigSchema {
35
35
  rateLimitCommands: number;
36
36
  agentMode: AgentMode;
37
37
  ollamaUrl: string;
38
+ customBaseUrl: string;
38
39
  agentConfirmation: 'always' | 'dangerous' | 'never';
39
40
  agentConfirmDeleteFile: boolean;
40
41
  agentConfirmExecuteCommand: boolean;
@@ -134,6 +135,26 @@ export declare function fetchOllamaModels(baseUrl?: string): Promise<{
134
135
  name: string;
135
136
  description: string;
136
137
  }[] | null>;
138
+ /**
139
+ * Fetch the model list from an OpenAI-compatible server's `/models`
140
+ * endpoint (vLLM, LiteLLM, LM Studio, etc.). `baseUrl` is the full base
141
+ * (e.g. http://host:8000/v1). Returns null on error.
142
+ */
143
+ export declare function fetchOpenAiCompatibleModels(baseUrl: string, apiKey?: string): Promise<{
144
+ id: string;
145
+ name: string;
146
+ description: string;
147
+ }[] | null>;
148
+ /**
149
+ * Resolve the effective OpenAI-protocol base URL for a provider, honoring
150
+ * user overrides the static provider table can't express:
151
+ * - ollama → configured `ollamaUrl` + /v1
152
+ * - custom → configured `customBaseUrl` (full base, e.g. http://host:8000/v1)
153
+ * - openai → the OPENAI_BASE_URL env var, if set (OpenAI-SDK convention)
154
+ * Falls back to the provider's hardcoded base URL. Only the `openai`
155
+ * protocol takes overrides; the anthropic protocol uses the static table.
156
+ */
157
+ export declare function resolveBaseUrl(providerId: string, protocol: 'openai' | 'anthropic'): string | null;
137
158
  export { PROVIDERS } from './providers';
138
159
  export declare function getCurrentSessionId(): string;
139
160
  export declare function startNewSession(): string;
@@ -2,7 +2,7 @@ import Conf from 'conf';
2
2
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, unlinkSync, statSync } from 'fs';
3
3
  import { join, dirname } from 'path';
4
4
  import { randomUUID } from 'crypto';
5
- import { PROVIDERS, getProvider } from './providers.js';
5
+ import { PROVIDERS, getProvider, getProviderBaseUrl } from './providers.js';
6
6
  import { logSession } from '../utils/logger.js';
7
7
  // We'll initialize GLOBAL_SESSIONS_DIR after config is created (to use config.path)
8
8
  /**
@@ -144,6 +144,7 @@ function createConfig() {
144
144
  model: 'glm-5.1',
145
145
  agentMode: 'on',
146
146
  ollamaUrl: 'http://localhost:11434',
147
+ customBaseUrl: '',
147
148
  agentConfirmation: 'dangerous',
148
149
  agentConfirmDeleteFile: true,
149
150
  agentConfirmExecuteCommand: true,
@@ -531,6 +532,59 @@ export async function fetchOllamaModels(baseUrl) {
531
532
  return null;
532
533
  }
533
534
  }
535
+ /**
536
+ * Fetch the model list from an OpenAI-compatible server's `/models`
537
+ * endpoint (vLLM, LiteLLM, LM Studio, etc.). `baseUrl` is the full base
538
+ * (e.g. http://host:8000/v1). Returns null on error.
539
+ */
540
+ export async function fetchOpenAiCompatibleModels(baseUrl, apiKey) {
541
+ const base = (baseUrl || '').trim().replace(/\/+$/, '');
542
+ if (!base)
543
+ return null;
544
+ try {
545
+ const headers = {};
546
+ if (apiKey)
547
+ headers['Authorization'] = `Bearer ${apiKey}`;
548
+ const res = await fetch(`${base}/models`, { headers, signal: AbortSignal.timeout(5000) });
549
+ if (!res.ok)
550
+ return null;
551
+ const data = await res.json();
552
+ if (!Array.isArray(data.data))
553
+ return null;
554
+ return data.data.map(m => ({ id: m.id, name: m.id, description: '' }));
555
+ }
556
+ catch {
557
+ return null;
558
+ }
559
+ }
560
+ /**
561
+ * Resolve the effective OpenAI-protocol base URL for a provider, honoring
562
+ * user overrides the static provider table can't express:
563
+ * - ollama → configured `ollamaUrl` + /v1
564
+ * - custom → configured `customBaseUrl` (full base, e.g. http://host:8000/v1)
565
+ * - openai → the OPENAI_BASE_URL env var, if set (OpenAI-SDK convention)
566
+ * Falls back to the provider's hardcoded base URL. Only the `openai`
567
+ * protocol takes overrides; the anthropic protocol uses the static table.
568
+ */
569
+ export function resolveBaseUrl(providerId, protocol) {
570
+ const fallback = getProviderBaseUrl(providerId, protocol);
571
+ if (protocol !== 'openai')
572
+ return fallback;
573
+ if (providerId === 'ollama') {
574
+ const u = (config.get('ollamaUrl') || 'http://localhost:11434').replace(/\/+$/, '');
575
+ return `${u}/v1`;
576
+ }
577
+ if (providerId === 'custom') {
578
+ const u = (config.get('customBaseUrl') || '').trim().replace(/\/+$/, '');
579
+ return u || fallback;
580
+ }
581
+ if (providerId === 'openai') {
582
+ const env = (process.env.OPENAI_BASE_URL || '').trim().replace(/\/+$/, '');
583
+ if (env)
584
+ return env;
585
+ }
586
+ return fallback;
587
+ }
534
588
  // Re-export PROVIDERS for convenience
535
589
  export { PROVIDERS } from './providers.js';
536
590
  // Generate unique session ID
@@ -331,6 +331,24 @@ export const PROVIDERS = {
331
331
  groupLabel: 'Ollama (local)',
332
332
  hint: 'Runs locally — no API key or account needed.',
333
333
  },
334
+ 'custom': {
335
+ name: 'Custom (OpenAI-compatible)',
336
+ description: 'Any OpenAI-compatible endpoint — vLLM, LiteLLM, LM Studio',
337
+ protocols: {
338
+ openai: {
339
+ baseUrl: 'http://localhost:8000/v1',
340
+ authHeader: 'Bearer',
341
+ supportsNativeTools: true,
342
+ },
343
+ },
344
+ models: [],
345
+ defaultModel: '',
346
+ defaultProtocol: 'openai',
347
+ noApiKey: true, // key optional — sent as Bearer only if you set one
348
+ dynamicModels: true,
349
+ groupLabel: 'Custom',
350
+ hint: 'Point at any OpenAI-compatible server. Set the URL in /settings (Custom Base URL) or the OPENAI_BASE_URL env var, then pick your model with /model.',
351
+ },
334
352
  };
335
353
  export function getProvider(id) {
336
354
  return PROVIDERS[id] || null;
@@ -354,6 +372,7 @@ const DISPLAY_ORDER = [
354
372
  'minimax',
355
373
  'minimax-api',
356
374
  'ollama',
375
+ 'custom',
357
376
  'z.ai-cn',
358
377
  'z.ai-cn-api',
359
378
  'minimax-cn',
@@ -120,6 +120,23 @@ export async function handleCommand(command, args, ctx) {
120
120
  });
121
121
  break;
122
122
  }
123
+ if (providerId === 'custom') {
124
+ const base = config.get('customBaseUrl') || 'http://localhost:8000/v1';
125
+ ctx.app.notify(`Fetching models from ${base}…`);
126
+ const { fetchOpenAiCompatibleModels, getApiKey: _getKey } = await import('../config/index.js');
127
+ const models = await fetchOpenAiCompatibleModels(base, _getKey('custom') || undefined);
128
+ if (!models || models.length === 0) {
129
+ ctx.app.notify(`Could not list models from ${base}. Set the base URL in /settings, or set the model directly (config key "model").`);
130
+ break;
131
+ }
132
+ const modelItems = models.map(m => ({ key: m.id, label: m.name, description: '' }));
133
+ const currentModel = config.get('model');
134
+ ctx.app.showSelect(`Select Model (${models.length})`, modelItems, currentModel, (item) => {
135
+ config.set('model', item.key);
136
+ ctx.app.notify(`Model: ${item.key}`);
137
+ });
138
+ break;
139
+ }
123
140
  const { fetchOllamaModels } = await import('../config/index.js');
124
141
  ctx.app.notify('Fetching models from Ollama...');
125
142
  const ollamaModels = await fetchOllamaModels();
@@ -131,6 +131,12 @@ export const SETTINGS = [
131
131
  getValue: () => config.get('ollamaUrl') || 'http://localhost:11434',
132
132
  type: 'text',
133
133
  },
134
+ {
135
+ key: 'customBaseUrl',
136
+ label: 'Custom Base URL',
137
+ getValue: () => config.get('customBaseUrl') || '',
138
+ type: 'text',
139
+ },
134
140
  {
135
141
  key: 'agentApiTimeout',
136
142
  label: 'Agent API Timeout (ms)',
@@ -252,6 +252,14 @@ async function showLoginFlow() {
252
252
  else if (event.key === 'enter') {
253
253
  selectedProvider = providers[selectedProviderIndex];
254
254
  setProvider(selectedProvider.id);
255
+ // Providers that don't need a key (Ollama, Custom OpenAI-compatible)
256
+ // skip the API-key prompt entirely. Configure their endpoint in
257
+ // /settings (Ollama URL / Custom Base URL) once inside the app.
258
+ if (selectedProvider.noApiKey) {
259
+ cleanup();
260
+ resolve('ollama'); // non-null sentinel so the caller proceeds
261
+ return;
262
+ }
255
263
  currentStep = 'apikey';
256
264
  loginScreen = new LoginScreen(screen, input, {
257
265
  providerName: selectedProvider.name,
@@ -13,10 +13,10 @@
13
13
  */
14
14
  import { existsSync, readFileSync, writeFileSync } from 'fs';
15
15
  import { join } from 'path';
16
- import { config, getApiKey } from '../config/index.js';
16
+ import { config, getApiKey, resolveBaseUrl } from '../config/index.js';
17
17
  import { loadProjectIntelligence, generateContextFromIntelligence } from './projectIntelligence.js';
18
18
  import { syncProgress, generateProjectId } from './codeepCloud.js';
19
- import { getProviderBaseUrl, getProviderAuthHeader, supportsNativeTools, getEffectiveMaxTokens, usesMaxCompletionTokens, requiresDefaultTemperature, isNoApiKeyProvider } from '../config/providers.js';
19
+ import { getProviderAuthHeader, supportsNativeTools, getEffectiveMaxTokens, usesMaxCompletionTokens, requiresDefaultTemperature, isNoApiKeyProvider } from '../config/providers.js';
20
20
  import { recordTokenUsage, extractOpenAIUsage, extractAnthropicUsage } from './tokenTracker.js';
21
21
  import { parseOpenAIToolCalls, parseAnthropicToolCalls, parseToolCalls } from './toolParsing.js';
22
22
  import { formatToolDefinitions, getOpenAITools, getAnthropicTools } from './tools.js';
@@ -239,11 +239,7 @@ additionalTools) {
239
239
  const model = config.get('model');
240
240
  const providerId = config.get('provider');
241
241
  const apiKey = getApiKey() || (isNoApiKeyProvider(providerId) ? 'ollama' : null);
242
- let baseUrl = getProviderBaseUrl(providerId, protocol);
243
- if (providerId === 'ollama' && protocol === 'openai') {
244
- const ollamaUrl = (config.get('ollamaUrl') || 'http://localhost:11434').replace(/\/$/, '');
245
- baseUrl = `${ollamaUrl}/v1`;
246
- }
242
+ let baseUrl = resolveBaseUrl(providerId, protocol);
247
243
  const authHeader = getProviderAuthHeader(providerId, protocol);
248
244
  if (!baseUrl)
249
245
  throw new Error(`Provider ${providerId} does not support ${protocol} protocol`);
@@ -413,11 +409,7 @@ export async function agentChatFallback(messages, systemPrompt, onChunk, abortSi
413
409
  const model = config.get('model');
414
410
  const providerId = config.get('provider');
415
411
  const apiKey = getApiKey() || (isNoApiKeyProvider(providerId) ? 'ollama' : null);
416
- let baseUrl = getProviderBaseUrl(providerId, protocol);
417
- if (providerId === 'ollama' && protocol === 'openai') {
418
- const ollamaUrl = (config.get('ollamaUrl') || 'http://localhost:11434').replace(/\/$/, '');
419
- baseUrl = `${ollamaUrl}/v1`;
420
- }
412
+ let baseUrl = resolveBaseUrl(providerId, protocol);
421
413
  const authHeader = getProviderAuthHeader(providerId, protocol);
422
414
  if (!baseUrl)
423
415
  throw new Error(`Provider ${providerId} does not support ${protocol} protocol`);
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Task Planning - breaks down complex tasks into subtasks
3
3
  */
4
- import { config, getApiKey } from '../config/index.js';
5
- import { getProviderBaseUrl, getProviderAuthHeader, requiresDefaultTemperature } from '../config/providers.js';
4
+ import { config, getApiKey, resolveBaseUrl } from '../config/index.js';
5
+ import { getProviderAuthHeader, requiresDefaultTemperature } from '../config/providers.js';
6
6
  /**
7
7
  * Ask AI to break down a complex task into subtasks
8
8
  */
@@ -43,7 +43,7 @@ Break this down into subtasks. Each task = one file or one logical unit. Respond
43
43
  const protocol = config.get('protocol');
44
44
  const provider = config.get('provider');
45
45
  const model = config.get('model');
46
- const baseUrl = getProviderBaseUrl(provider, protocol);
46
+ const baseUrl = resolveBaseUrl(provider, protocol);
47
47
  const authHeaderType = getProviderAuthHeader(provider, protocol);
48
48
  const messages = [
49
49
  { role: 'user', content: systemPrompt }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeep",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "description": "AI-powered coding assistant built for the terminal. Multiple LLM providers, project-aware context, and a seamless development workflow.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",