codeep 2.3.1 → 2.4.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 +8 -6
- package/dist/api/index.js +24 -0
- package/dist/api/ollamaNative.d.ts +117 -0
- package/dist/api/ollamaNative.js +228 -0
- package/dist/config/index.d.ts +13 -0
- package/dist/config/index.js +3 -0
- package/dist/config/providers.js +11 -10
- package/dist/renderer/commands.js +50 -0
- package/dist/renderer/components/Help.js +2 -0
- package/dist/renderer/components/Settings.js +22 -0
- package/dist/utils/agentChat.js +32 -0
- package/dist/utils/ollamaCatalog.d.ts +30 -0
- package/dist/utils/ollamaCatalog.js +37 -0
- package/dist/utils/tokenTracker.js +6 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,12 +48,12 @@ custom slash commands, lifecycle hooks, checkpoints, `/cost`,
|
|
|
48
48
|
## Features
|
|
49
49
|
|
|
50
50
|
### Multi-Provider Support
|
|
51
|
-
- **Z.AI (ZhipuAI)** — GLM
|
|
52
|
-
- **OpenAI** — GPT
|
|
53
|
-
- **Anthropic** — Claude
|
|
54
|
-
- **DeepSeek** — DeepSeek
|
|
55
|
-
- **Google AI** — Gemini
|
|
56
|
-
- **MiniMax** — MiniMax
|
|
51
|
+
- **Z.AI (ZhipuAI)** — GLM models (Coding Plan & pay-per-use API, international & China)
|
|
52
|
+
- **OpenAI** — GPT models (flagship, Mini, Nano)
|
|
53
|
+
- **Anthropic** — Claude models (Opus, Sonnet, Haiku)
|
|
54
|
+
- **DeepSeek** — DeepSeek models (Pro, Flash)
|
|
55
|
+
- **Google AI** — Gemini models (Pro, Flash)
|
|
56
|
+
- **MiniMax** — MiniMax models (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
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.
|
|
@@ -1016,6 +1016,8 @@ In `dangerous` mode, configure which tools require confirmation via `/settings`:
|
|
|
1016
1016
|
| `/memory remove <n>` | Remove note by index |
|
|
1017
1017
|
| `/memory clear` | Clear all notes |
|
|
1018
1018
|
| `/model pull <name>` | Pull an Ollama model (local Ollama only) |
|
|
1019
|
+
| `/model browse` | Browse a curated catalog of local coding models and pull one (Ollama) |
|
|
1020
|
+
| `/model rm <name>` | Remove a locally-installed Ollama model (local only) |
|
|
1019
1021
|
|
|
1020
1022
|
### Skills
|
|
1021
1023
|
|
package/dist/api/index.js
CHANGED
|
@@ -394,6 +394,30 @@ async function chatOpenAI(message, history, model, apiKey, onChunk, abortSignal)
|
|
|
394
394
|
...(openRouterProvider ? { provider: openRouterProvider } : {}),
|
|
395
395
|
});
|
|
396
396
|
try {
|
|
397
|
+
// Opt-in native Ollama transport (plain chat). When `ollamaNativeApi` is on,
|
|
398
|
+
// route through /api/chat so num_ctx + keep_alive apply and the real context
|
|
399
|
+
// window is used. OFF by default → falls through to the /v1 shim below
|
|
400
|
+
// unchanged. Plain chat sends no tools, so the native call is simple here.
|
|
401
|
+
if (providerId === 'ollama' && config.get('ollamaNativeApi') === true) {
|
|
402
|
+
const { streamOllamaNativeChat, getOllamaContextLength } = await import('./ollamaNative.js');
|
|
403
|
+
const ollamaUrl = config.get('ollamaUrl') || 'http://localhost:11434';
|
|
404
|
+
const cfgCtx = Number(config.get('ollamaNumCtx')) || 0;
|
|
405
|
+
const numCtx = cfgCtx > 0 ? cfgCtx : ((await getOllamaContextLength(model, ollamaUrl)) ?? undefined);
|
|
406
|
+
const result = await streamOllamaNativeChat({
|
|
407
|
+
baseUrl: ollamaUrl,
|
|
408
|
+
model,
|
|
409
|
+
messages,
|
|
410
|
+
numCtx,
|
|
411
|
+
keepAlive: config.get('ollamaKeepAlive') || undefined,
|
|
412
|
+
temperature: omitTemperature ? undefined : temperature,
|
|
413
|
+
timeoutMs: timeout,
|
|
414
|
+
onChunk: stream ? onChunk : undefined,
|
|
415
|
+
});
|
|
416
|
+
if (result.promptTokens != null && result.completionTokens != null) {
|
|
417
|
+
recordTokenUsage({ promptTokens: result.promptTokens, completionTokens: result.completionTokens, totalTokens: result.promptTokens + result.completionTokens }, model, providerId);
|
|
418
|
+
}
|
|
419
|
+
return stripThinkTags(result.text);
|
|
420
|
+
}
|
|
397
421
|
// Use node:http for Ollama — bypasses undici connection pooling (AggregateError in Node v24)
|
|
398
422
|
if (providerId === 'ollama') {
|
|
399
423
|
const nodeStream = await httpRequest(`${baseUrl}/chat/completions`, {
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native Ollama `/api/chat` transport.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists: Codeep normally talks to Ollama through its OpenAI-compatible
|
|
5
|
+
* `/v1/chat/completions` shim, which silently IGNORES Ollama-native options like
|
|
6
|
+
* `num_ctx` (context window) and `keep_alive` (model residency). The shim also
|
|
7
|
+
* defaults to a small context (~2-4k) regardless of the model's real window, so
|
|
8
|
+
* long sessions get silently truncated server-side.
|
|
9
|
+
*
|
|
10
|
+
* The native `/api/chat` endpoint accepts those options and streams
|
|
11
|
+
* newline-delimited JSON (NOT SSE). Each line is a full JSON object:
|
|
12
|
+
* {"message":{"role":"assistant","content":"…"},"done":false}
|
|
13
|
+
* …
|
|
14
|
+
* {"message":{"content":""},"done":true,"prompt_eval_count":N,"eval_count":M}
|
|
15
|
+
*
|
|
16
|
+
* This module keeps the line-parsing as PURE functions so they can be unit
|
|
17
|
+
* tested without a live server. The networking wrapper lives at the bottom.
|
|
18
|
+
*/
|
|
19
|
+
/** A tool call as Ollama's native /api/chat returns it: `function.arguments`
|
|
20
|
+
* is a JSON OBJECT (unlike OpenAI's JSON-string form). */
|
|
21
|
+
export interface OllamaToolCall {
|
|
22
|
+
name: string;
|
|
23
|
+
arguments: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
export interface OllamaStreamDelta {
|
|
26
|
+
/** Text chunk from this line (may be empty). */
|
|
27
|
+
content: string;
|
|
28
|
+
/** True on the terminating line. */
|
|
29
|
+
done: boolean;
|
|
30
|
+
/** Tool calls present on this line (Ollama emits them on one message). */
|
|
31
|
+
toolCalls?: OllamaToolCall[];
|
|
32
|
+
/** Prompt (input) tokens — only present on the final line. */
|
|
33
|
+
promptTokens?: number;
|
|
34
|
+
/** Completion (output) tokens — only present on the final line. */
|
|
35
|
+
completionTokens?: number;
|
|
36
|
+
}
|
|
37
|
+
/** Extract + normalize `message.tool_calls` from a parsed message. Returns
|
|
38
|
+
* undefined when none. Tolerates missing/malformed entries (never throws). */
|
|
39
|
+
export declare function extractOllamaToolCalls(msg: unknown): OllamaToolCall[] | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Parse a single newline-delimited JSON line from `/api/chat`. Returns null for
|
|
42
|
+
* blank lines or unparseable keepalives (never throws). Tolerates both the
|
|
43
|
+
* streaming shape (`message.content`) and the non-stream shape.
|
|
44
|
+
*/
|
|
45
|
+
export declare function parseOllamaChatLine(line: string): OllamaStreamDelta | null;
|
|
46
|
+
export interface OllamaAccumulator {
|
|
47
|
+
text: string;
|
|
48
|
+
toolCalls: OllamaToolCall[];
|
|
49
|
+
promptTokens?: number;
|
|
50
|
+
completionTokens?: number;
|
|
51
|
+
done: boolean;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Fold a parsed delta into a running accumulator. Pure — no I/O. The caller
|
|
55
|
+
* feeds each line's parse result here and reads `text` / token counts at the end.
|
|
56
|
+
*/
|
|
57
|
+
export declare function foldOllamaDelta(acc: OllamaAccumulator, delta: OllamaStreamDelta | null): OllamaAccumulator;
|
|
58
|
+
/**
|
|
59
|
+
* Split a buffer into complete lines + a trailing remainder. Pure helper so the
|
|
60
|
+
* stream reader can carry partial lines across chunk boundaries correctly.
|
|
61
|
+
* Returns { lines, rest } where `rest` is the unfinished tail (no newline yet).
|
|
62
|
+
*/
|
|
63
|
+
export declare function splitOllamaLines(buffer: string): {
|
|
64
|
+
lines: string[];
|
|
65
|
+
rest: string;
|
|
66
|
+
};
|
|
67
|
+
export declare const initialOllamaAccumulator: () => OllamaAccumulator;
|
|
68
|
+
/**
|
|
69
|
+
* Pull the real context_length out of an `/api/show` response's `model_info`.
|
|
70
|
+
* The key is architecture-prefixed (e.g. `llama.context_length`,
|
|
71
|
+
* `qwen2.context_length`), so we scan for any key ending in `.context_length`
|
|
72
|
+
* (or the bare `context_length`). Pure + tolerant — returns null when absent.
|
|
73
|
+
*/
|
|
74
|
+
export declare function extractContextLength(showResponse: unknown): number | null;
|
|
75
|
+
/**
|
|
76
|
+
* Fetch a model's real maximum context window via `/api/show`. Cached per model;
|
|
77
|
+
* returns null on any error (caller falls back to its default). Never throws.
|
|
78
|
+
*/
|
|
79
|
+
export declare function getOllamaContextLength(model: string, ollamaBaseUrl: string): Promise<number | null>;
|
|
80
|
+
/** Test seam — reset the per-model context cache. */
|
|
81
|
+
export declare function _clearOllamaContextCache(): void;
|
|
82
|
+
export interface OllamaChatOptions {
|
|
83
|
+
/** Ollama base URL (without /v1), e.g. http://localhost:11434 */
|
|
84
|
+
baseUrl: string;
|
|
85
|
+
model: string;
|
|
86
|
+
/** Messages in OpenAI shape {role, content} — Ollama /api/chat accepts these. */
|
|
87
|
+
messages: {
|
|
88
|
+
role: string;
|
|
89
|
+
content: string;
|
|
90
|
+
}[];
|
|
91
|
+
/** num_ctx — the context window to allocate. 0/undefined = let Ollama decide. */
|
|
92
|
+
numCtx?: number;
|
|
93
|
+
/** keep_alive — how long to keep the model resident, e.g. "30m". */
|
|
94
|
+
keepAlive?: string;
|
|
95
|
+
temperature?: number;
|
|
96
|
+
timeoutMs?: number;
|
|
97
|
+
onChunk?: (text: string) => void;
|
|
98
|
+
/** Tool definitions in OpenAI function format. Ollama's /api/chat accepts the
|
|
99
|
+
* same `{type:'function',function:{...}}` shape and returns `tool_calls`. */
|
|
100
|
+
tools?: unknown[];
|
|
101
|
+
/** Pass-through for non-string message parts (tool results carry tool_call_id
|
|
102
|
+
* etc.). When set, used verbatim instead of `messages`. */
|
|
103
|
+
rawMessages?: unknown[];
|
|
104
|
+
}
|
|
105
|
+
export interface OllamaChatResult {
|
|
106
|
+
text: string;
|
|
107
|
+
toolCalls: OllamaToolCall[];
|
|
108
|
+
promptTokens?: number;
|
|
109
|
+
completionTokens?: number;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Stream a chat completion from Ollama's native `/api/chat`. Uses node:http to
|
|
113
|
+
* sidestep undici's connection pooling (which throws AggregateError against
|
|
114
|
+
* localhost Ollama on Node 24). Parses the newline-JSON stream via the pure
|
|
115
|
+
* helpers above. Resolves with the full text + token usage.
|
|
116
|
+
*/
|
|
117
|
+
export declare function streamOllamaNativeChat(opts: OllamaChatOptions): Promise<OllamaChatResult>;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native Ollama `/api/chat` transport.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists: Codeep normally talks to Ollama through its OpenAI-compatible
|
|
5
|
+
* `/v1/chat/completions` shim, which silently IGNORES Ollama-native options like
|
|
6
|
+
* `num_ctx` (context window) and `keep_alive` (model residency). The shim also
|
|
7
|
+
* defaults to a small context (~2-4k) regardless of the model's real window, so
|
|
8
|
+
* long sessions get silently truncated server-side.
|
|
9
|
+
*
|
|
10
|
+
* The native `/api/chat` endpoint accepts those options and streams
|
|
11
|
+
* newline-delimited JSON (NOT SSE). Each line is a full JSON object:
|
|
12
|
+
* {"message":{"role":"assistant","content":"…"},"done":false}
|
|
13
|
+
* …
|
|
14
|
+
* {"message":{"content":""},"done":true,"prompt_eval_count":N,"eval_count":M}
|
|
15
|
+
*
|
|
16
|
+
* This module keeps the line-parsing as PURE functions so they can be unit
|
|
17
|
+
* tested without a live server. The networking wrapper lives at the bottom.
|
|
18
|
+
*/
|
|
19
|
+
import http from 'http';
|
|
20
|
+
import https from 'https';
|
|
21
|
+
/** Extract + normalize `message.tool_calls` from a parsed message. Returns
|
|
22
|
+
* undefined when none. Tolerates missing/malformed entries (never throws). */
|
|
23
|
+
export function extractOllamaToolCalls(msg) {
|
|
24
|
+
const raw = msg?.tool_calls;
|
|
25
|
+
if (!Array.isArray(raw) || raw.length === 0)
|
|
26
|
+
return undefined;
|
|
27
|
+
const out = [];
|
|
28
|
+
for (const tc of raw) {
|
|
29
|
+
const fn = tc?.function;
|
|
30
|
+
const name = typeof fn?.name === 'string' ? fn.name : '';
|
|
31
|
+
if (!name)
|
|
32
|
+
continue;
|
|
33
|
+
let args = {};
|
|
34
|
+
const a = fn?.arguments;
|
|
35
|
+
if (a && typeof a === 'object')
|
|
36
|
+
args = a;
|
|
37
|
+
else if (typeof a === 'string') {
|
|
38
|
+
try {
|
|
39
|
+
args = JSON.parse(a);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
args = {};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
out.push({ name, arguments: args });
|
|
46
|
+
}
|
|
47
|
+
return out.length > 0 ? out : undefined;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Parse a single newline-delimited JSON line from `/api/chat`. Returns null for
|
|
51
|
+
* blank lines or unparseable keepalives (never throws). Tolerates both the
|
|
52
|
+
* streaming shape (`message.content`) and the non-stream shape.
|
|
53
|
+
*/
|
|
54
|
+
export function parseOllamaChatLine(line) {
|
|
55
|
+
const trimmed = line.trim();
|
|
56
|
+
if (!trimmed)
|
|
57
|
+
return null;
|
|
58
|
+
let json;
|
|
59
|
+
try {
|
|
60
|
+
json = JSON.parse(trimmed);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const msg = json.message;
|
|
66
|
+
const content = typeof msg?.content === 'string' ? msg.content : '';
|
|
67
|
+
const done = json.done === true;
|
|
68
|
+
const out = { content, done };
|
|
69
|
+
const toolCalls = extractOllamaToolCalls(msg);
|
|
70
|
+
if (toolCalls)
|
|
71
|
+
out.toolCalls = toolCalls;
|
|
72
|
+
if (typeof json.prompt_eval_count === 'number')
|
|
73
|
+
out.promptTokens = json.prompt_eval_count;
|
|
74
|
+
if (typeof json.eval_count === 'number')
|
|
75
|
+
out.completionTokens = json.eval_count;
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Fold a parsed delta into a running accumulator. Pure — no I/O. The caller
|
|
80
|
+
* feeds each line's parse result here and reads `text` / token counts at the end.
|
|
81
|
+
*/
|
|
82
|
+
export function foldOllamaDelta(acc, delta) {
|
|
83
|
+
if (!delta)
|
|
84
|
+
return acc;
|
|
85
|
+
return {
|
|
86
|
+
text: acc.text + delta.content,
|
|
87
|
+
toolCalls: delta.toolCalls ? [...acc.toolCalls, ...delta.toolCalls] : acc.toolCalls,
|
|
88
|
+
promptTokens: delta.promptTokens ?? acc.promptTokens,
|
|
89
|
+
completionTokens: delta.completionTokens ?? acc.completionTokens,
|
|
90
|
+
done: acc.done || delta.done,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Split a buffer into complete lines + a trailing remainder. Pure helper so the
|
|
95
|
+
* stream reader can carry partial lines across chunk boundaries correctly.
|
|
96
|
+
* Returns { lines, rest } where `rest` is the unfinished tail (no newline yet).
|
|
97
|
+
*/
|
|
98
|
+
export function splitOllamaLines(buffer) {
|
|
99
|
+
const parts = buffer.split('\n');
|
|
100
|
+
const rest = parts.pop() ?? '';
|
|
101
|
+
return { lines: parts, rest };
|
|
102
|
+
}
|
|
103
|
+
export const initialOllamaAccumulator = () => ({ text: '', toolCalls: [], done: false });
|
|
104
|
+
/**
|
|
105
|
+
* Pull the real context_length out of an `/api/show` response's `model_info`.
|
|
106
|
+
* The key is architecture-prefixed (e.g. `llama.context_length`,
|
|
107
|
+
* `qwen2.context_length`), so we scan for any key ending in `.context_length`
|
|
108
|
+
* (or the bare `context_length`). Pure + tolerant — returns null when absent.
|
|
109
|
+
*/
|
|
110
|
+
export function extractContextLength(showResponse) {
|
|
111
|
+
const info = showResponse?.model_info;
|
|
112
|
+
if (!info || typeof info !== 'object')
|
|
113
|
+
return null;
|
|
114
|
+
for (const [key, value] of Object.entries(info)) {
|
|
115
|
+
if ((key === 'context_length' || key.endsWith('.context_length')) && typeof value === 'number' && value > 0) {
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
// Per-model context-length cache so we hit /api/show at most once per model.
|
|
122
|
+
const contextLengthCache = new Map();
|
|
123
|
+
/**
|
|
124
|
+
* Fetch a model's real maximum context window via `/api/show`. Cached per model;
|
|
125
|
+
* returns null on any error (caller falls back to its default). Never throws.
|
|
126
|
+
*/
|
|
127
|
+
export async function getOllamaContextLength(model, ollamaBaseUrl) {
|
|
128
|
+
if (contextLengthCache.has(model))
|
|
129
|
+
return contextLengthCache.get(model) ?? null;
|
|
130
|
+
const base = ollamaBaseUrl.replace(/\/+$/, '').replace(/\/v1$/, '');
|
|
131
|
+
try {
|
|
132
|
+
const res = await fetch(`${base}/api/show`, {
|
|
133
|
+
method: 'POST',
|
|
134
|
+
headers: { 'Content-Type': 'application/json' },
|
|
135
|
+
body: JSON.stringify({ name: model }),
|
|
136
|
+
signal: AbortSignal.timeout(5000),
|
|
137
|
+
});
|
|
138
|
+
if (!res.ok) {
|
|
139
|
+
contextLengthCache.set(model, null);
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
const data = await res.json();
|
|
143
|
+
const ctx = extractContextLength(data);
|
|
144
|
+
contextLengthCache.set(model, ctx);
|
|
145
|
+
return ctx;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
contextLengthCache.set(model, null);
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** Test seam — reset the per-model context cache. */
|
|
153
|
+
export function _clearOllamaContextCache() {
|
|
154
|
+
contextLengthCache.clear();
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Stream a chat completion from Ollama's native `/api/chat`. Uses node:http to
|
|
158
|
+
* sidestep undici's connection pooling (which throws AggregateError against
|
|
159
|
+
* localhost Ollama on Node 24). Parses the newline-JSON stream via the pure
|
|
160
|
+
* helpers above. Resolves with the full text + token usage.
|
|
161
|
+
*/
|
|
162
|
+
export function streamOllamaNativeChat(opts) {
|
|
163
|
+
const base = opts.baseUrl.replace(/\/+$/, '').replace(/\/v1$/, '');
|
|
164
|
+
const url = `${base}/api/chat`;
|
|
165
|
+
const options = {};
|
|
166
|
+
if (opts.numCtx && opts.numCtx > 0)
|
|
167
|
+
options.num_ctx = opts.numCtx;
|
|
168
|
+
if (typeof opts.temperature === 'number')
|
|
169
|
+
options.temperature = opts.temperature;
|
|
170
|
+
// When tools are present, Ollama only emits tool_calls in non-streaming mode
|
|
171
|
+
// reliably; streaming tool_calls support varies by version. We stream when
|
|
172
|
+
// there are no tools (plain chat) and fall to non-stream when tools are sent.
|
|
173
|
+
const useStream = !opts.tools || opts.tools.length === 0;
|
|
174
|
+
const body = JSON.stringify({
|
|
175
|
+
model: opts.model,
|
|
176
|
+
messages: opts.rawMessages ?? opts.messages,
|
|
177
|
+
stream: useStream,
|
|
178
|
+
...(opts.tools && opts.tools.length ? { tools: opts.tools } : {}),
|
|
179
|
+
...(Object.keys(options).length ? { options } : {}),
|
|
180
|
+
...(opts.keepAlive ? { keep_alive: opts.keepAlive } : {}),
|
|
181
|
+
});
|
|
182
|
+
return new Promise((resolve, reject) => {
|
|
183
|
+
const u = new URL(url);
|
|
184
|
+
const lib = u.protocol === 'https:' ? https : http;
|
|
185
|
+
const req = lib.request({
|
|
186
|
+
hostname: u.hostname,
|
|
187
|
+
port: u.port,
|
|
188
|
+
path: u.pathname + u.search,
|
|
189
|
+
method: 'POST',
|
|
190
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
|
|
191
|
+
timeout: opts.timeoutMs ?? 120_000,
|
|
192
|
+
}, (res) => {
|
|
193
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
194
|
+
let errBody = '';
|
|
195
|
+
res.on('data', (c) => { errBody += c.toString(); });
|
|
196
|
+
res.on('end', () => reject(new Error(`Ollama /api/chat ${res.statusCode}: ${errBody.slice(0, 300)}`)));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
let buffer = '';
|
|
200
|
+
let acc = initialOllamaAccumulator();
|
|
201
|
+
const decoder = new TextDecoder();
|
|
202
|
+
res.on('data', (chunk) => {
|
|
203
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
204
|
+
const { lines, rest } = splitOllamaLines(buffer);
|
|
205
|
+
buffer = rest;
|
|
206
|
+
for (const line of lines) {
|
|
207
|
+
const delta = parseOllamaChatLine(line);
|
|
208
|
+
if (delta?.content)
|
|
209
|
+
opts.onChunk?.(delta.content);
|
|
210
|
+
acc = foldOllamaDelta(acc, delta);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
res.on('end', () => {
|
|
214
|
+
// Flush any trailing line (final object often arrives without a newline).
|
|
215
|
+
const delta = parseOllamaChatLine(buffer);
|
|
216
|
+
if (delta?.content)
|
|
217
|
+
opts.onChunk?.(delta.content);
|
|
218
|
+
acc = foldOllamaDelta(acc, delta);
|
|
219
|
+
resolve({ text: acc.text, toolCalls: acc.toolCalls, promptTokens: acc.promptTokens, completionTokens: acc.completionTokens });
|
|
220
|
+
});
|
|
221
|
+
res.on('error', reject);
|
|
222
|
+
});
|
|
223
|
+
req.on('error', reject);
|
|
224
|
+
req.on('timeout', () => { req.destroy(new Error('Ollama request timed out')); });
|
|
225
|
+
req.write(body);
|
|
226
|
+
req.end();
|
|
227
|
+
});
|
|
228
|
+
}
|
package/dist/config/index.d.ts
CHANGED
|
@@ -54,6 +54,19 @@ interface ConfigSchema {
|
|
|
54
54
|
rateLimitCommands: number;
|
|
55
55
|
agentMode: AgentMode;
|
|
56
56
|
ollamaUrl: string;
|
|
57
|
+
/** Route Ollama through its NATIVE /api/chat endpoint instead of the
|
|
58
|
+
* OpenAI-compatible /v1 shim. The native endpoint honors num_ctx + keep_alive
|
|
59
|
+
* and exposes the model's real context window. OFF by default — opt-in until
|
|
60
|
+
* verified against a live Ollama; when off, the existing /v1 path is used
|
|
61
|
+
* unchanged so current behavior is preserved. */
|
|
62
|
+
ollamaNativeApi: boolean;
|
|
63
|
+
/** How long Ollama keeps the model loaded in memory between requests (e.g.
|
|
64
|
+
* "30m", "1h", "-1" for forever). Avoids reload latency every turn. Sent on
|
|
65
|
+
* native /api/chat requests (only when ollamaNativeApi is on). Default "30m". */
|
|
66
|
+
ollamaKeepAlive: string;
|
|
67
|
+
/** Override num_ctx (context window) for Ollama. 0 = auto-detect the model's
|
|
68
|
+
* real max via /api/show. Set a specific number to cap VRAM use. Default 0. */
|
|
69
|
+
ollamaNumCtx: number;
|
|
57
70
|
customBaseUrl: string;
|
|
58
71
|
agentConfirmation: 'always' | 'dangerous' | 'never';
|
|
59
72
|
agentConfirmDeleteFile: boolean;
|
package/dist/config/index.js
CHANGED
|
@@ -144,6 +144,9 @@ function createConfig() {
|
|
|
144
144
|
model: 'glm-5.1',
|
|
145
145
|
agentMode: 'on',
|
|
146
146
|
ollamaUrl: 'http://localhost:11434',
|
|
147
|
+
ollamaNativeApi: false,
|
|
148
|
+
ollamaKeepAlive: '30m',
|
|
149
|
+
ollamaNumCtx: 0,
|
|
147
150
|
customBaseUrl: '',
|
|
148
151
|
agentConfirmation: 'dangerous',
|
|
149
152
|
agentConfirmDeleteFile: true,
|
package/dist/config/providers.js
CHANGED
|
@@ -126,9 +126,9 @@ export const PROVIDERS = {
|
|
|
126
126
|
},
|
|
127
127
|
},
|
|
128
128
|
models: [
|
|
129
|
-
{ id: 'MiniMax-
|
|
129
|
+
{ id: 'MiniMax-M3', name: 'MiniMax M3', description: 'Latest MiniMax model' },
|
|
130
130
|
],
|
|
131
|
-
defaultModel: 'MiniMax-
|
|
131
|
+
defaultModel: 'MiniMax-M3',
|
|
132
132
|
defaultProtocol: 'anthropic',
|
|
133
133
|
envKey: 'MINIMAX_API_KEY',
|
|
134
134
|
subscribeUrl: 'https://platform.minimax.io/subscribe/coding-plan?code=2lWvoWUhrp&source=link',
|
|
@@ -146,9 +146,9 @@ export const PROVIDERS = {
|
|
|
146
146
|
},
|
|
147
147
|
},
|
|
148
148
|
models: [
|
|
149
|
-
{ id: 'MiniMax-
|
|
149
|
+
{ id: 'MiniMax-M3', name: 'MiniMax M3', description: 'Latest MiniMax model' },
|
|
150
150
|
],
|
|
151
|
-
defaultModel: 'MiniMax-
|
|
151
|
+
defaultModel: 'MiniMax-M3',
|
|
152
152
|
defaultProtocol: 'openai',
|
|
153
153
|
envKey: 'MINIMAX_API_KEY',
|
|
154
154
|
subscribeUrl: 'https://platform.minimax.io',
|
|
@@ -171,9 +171,9 @@ export const PROVIDERS = {
|
|
|
171
171
|
},
|
|
172
172
|
},
|
|
173
173
|
models: [
|
|
174
|
-
{ id: 'MiniMax-
|
|
174
|
+
{ id: 'MiniMax-M3', name: 'MiniMax M3', description: 'Latest MiniMax model' },
|
|
175
175
|
],
|
|
176
|
-
defaultModel: 'MiniMax-
|
|
176
|
+
defaultModel: 'MiniMax-M3',
|
|
177
177
|
defaultProtocol: 'anthropic',
|
|
178
178
|
envKey: 'MINIMAX_CN_API_KEY',
|
|
179
179
|
subscribeUrl: 'https://platform.minimaxi.com',
|
|
@@ -243,12 +243,13 @@ export const PROVIDERS = {
|
|
|
243
243
|
},
|
|
244
244
|
},
|
|
245
245
|
models: [
|
|
246
|
-
{ id: 'claude-opus-4-
|
|
247
|
-
{ id: 'claude-opus-4-
|
|
246
|
+
{ id: 'claude-opus-4-8', name: 'Claude Opus 4.8', description: 'Most capable Claude model' },
|
|
247
|
+
{ id: 'claude-opus-4-7', name: 'Claude Opus 4.7', description: 'Previous generation Opus' },
|
|
248
|
+
{ id: 'claude-opus-4-6', name: 'Claude Opus 4.6', description: 'Older generation Opus' },
|
|
248
249
|
{ id: 'claude-sonnet-4-6', name: 'Claude Sonnet', description: 'Best balance of speed and intelligence' },
|
|
249
250
|
{ id: 'claude-haiku-4-5-20251001', name: 'Claude Haiku', description: 'Fastest and most affordable' },
|
|
250
251
|
],
|
|
251
|
-
defaultModel: 'claude-opus-4-
|
|
252
|
+
defaultModel: 'claude-opus-4-8',
|
|
252
253
|
defaultProtocol: 'anthropic',
|
|
253
254
|
envKey: 'ANTHROPIC_API_KEY',
|
|
254
255
|
groupLabel: 'Anthropic',
|
|
@@ -266,7 +267,7 @@ export const PROVIDERS = {
|
|
|
266
267
|
},
|
|
267
268
|
models: [
|
|
268
269
|
{ id: 'gemini-3.1-pro-preview', name: 'Gemini 3.1 Pro', description: 'Most capable Gemini model' },
|
|
269
|
-
{ id: 'gemini-3-flash
|
|
270
|
+
{ id: 'gemini-3.5-flash', name: 'Gemini 3.5 Flash', description: 'Pro-level intelligence at Flash speed' },
|
|
270
271
|
],
|
|
271
272
|
defaultModel: 'gemini-3.1-pro-preview',
|
|
272
273
|
defaultProtocol: 'openai',
|
|
@@ -93,6 +93,56 @@ export async function handleCommand(command, args, ctx) {
|
|
|
93
93
|
});
|
|
94
94
|
break;
|
|
95
95
|
}
|
|
96
|
+
// /model browse — curated catalog of recommended local coding models.
|
|
97
|
+
if (args[0] === 'browse') {
|
|
98
|
+
const ollamaUrl = (config.get('ollamaUrl') || 'http://localhost:11434').toLowerCase();
|
|
99
|
+
const isLocal = ollamaUrl.includes('localhost') || ollamaUrl.includes('127.0.0.1') || ollamaUrl.includes('[::1]');
|
|
100
|
+
const { OLLAMA_CODING_MODELS, catalogAgentHint } = await import('../utils/ollamaCatalog.js');
|
|
101
|
+
const items = OLLAMA_CODING_MODELS.map(m => ({
|
|
102
|
+
key: m.pull,
|
|
103
|
+
label: m.name,
|
|
104
|
+
description: `${m.pull} · ${m.vram} · ${catalogAgentHint(m.params)} — ${m.description}`,
|
|
105
|
+
}));
|
|
106
|
+
ctx.app.showSelect('Recommended coding models (pull)', items, '', (item) => {
|
|
107
|
+
if (!isLocal) {
|
|
108
|
+
ctx.app.notify(`Ollama is on a remote server. SSH in and run: ollama pull ${item.key}`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
ctx.app.notify(`Pulling ${item.key}... (this may take a while)`);
|
|
112
|
+
import('child_process').then(({ execFile }) => {
|
|
113
|
+
execFile('ollama', ['pull', item.key], { timeout: 1_200_000 }, (err) => {
|
|
114
|
+
if (err)
|
|
115
|
+
ctx.app.notify(`Pull failed: ${err.message}`);
|
|
116
|
+
else
|
|
117
|
+
ctx.app.notify(`✓ ${item.key} ready — use /model to select it`);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
// /model rm <name> — delete a locally-installed Ollama model.
|
|
124
|
+
if (args[0] === 'rm' || args[0] === 'remove' || args[0] === 'delete') {
|
|
125
|
+
const modelName = args[1];
|
|
126
|
+
if (!modelName) {
|
|
127
|
+
ctx.app.notify('Usage: /model rm <model-name>');
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
const ollamaUrl = (config.get('ollamaUrl') || 'http://localhost:11434').toLowerCase();
|
|
131
|
+
const isLocal = ollamaUrl.includes('localhost') || ollamaUrl.includes('127.0.0.1') || ollamaUrl.includes('[::1]');
|
|
132
|
+
if (!isLocal) {
|
|
133
|
+
ctx.app.notify(`Ollama is on a remote server. SSH in and run: ollama rm ${modelName}`);
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
ctx.app.notify(`Removing ${modelName}…`);
|
|
137
|
+
const { execFile } = await import('child_process');
|
|
138
|
+
execFile('ollama', ['rm', modelName], { timeout: 60_000 }, (err) => {
|
|
139
|
+
if (err)
|
|
140
|
+
ctx.app.notify(`Remove failed: ${err.message}`);
|
|
141
|
+
else
|
|
142
|
+
ctx.app.notify(`✓ Removed ${modelName}`);
|
|
143
|
+
});
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
96
146
|
const providerId = config.get('provider');
|
|
97
147
|
const { isDynamicModelsProvider, isNoApiKeyProvider: _noKey } = await import('../config/providers.js');
|
|
98
148
|
if (isDynamicModelsProvider(providerId)) {
|
|
@@ -171,6 +171,8 @@ export const helpCategories = [
|
|
|
171
171
|
{ key: '/settings > Ollama URL', description: 'Set URL (default: http://localhost:11434)' },
|
|
172
172
|
{ key: '/model', description: 'Pick installed Ollama model dynamically' },
|
|
173
173
|
{ key: '/model pull <model>', description: 'Pull an Ollama model (local Ollama only)' },
|
|
174
|
+
{ key: '/model browse', description: 'Browse recommended local coding models and pull one' },
|
|
175
|
+
{ key: '/model rm <model>', description: 'Remove a locally-installed Ollama model (local only)' },
|
|
174
176
|
{ key: 'OLLAMA_HOST=0.0.0.0', description: 'Required env var for remote Ollama access' },
|
|
175
177
|
],
|
|
176
178
|
},
|
|
@@ -131,6 +131,28 @@ export const SETTINGS = [
|
|
|
131
131
|
getValue: () => config.get('ollamaUrl') || 'http://localhost:11434',
|
|
132
132
|
type: 'text',
|
|
133
133
|
},
|
|
134
|
+
{
|
|
135
|
+
key: 'ollamaNativeApi',
|
|
136
|
+
label: 'Ollama Native API (beta)',
|
|
137
|
+
getValue: () => config.get('ollamaNativeApi'),
|
|
138
|
+
type: 'select',
|
|
139
|
+
options: [
|
|
140
|
+
{ value: false, label: 'Off (OpenAI-compatible /v1)' },
|
|
141
|
+
{ value: true, label: 'On — beta: native /api/chat (num_ctx + keep_alive)' },
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
key: 'ollamaKeepAlive',
|
|
146
|
+
label: 'Ollama Keep-Alive',
|
|
147
|
+
getValue: () => String(config.get('ollamaKeepAlive') ?? '30m'),
|
|
148
|
+
type: 'text',
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
key: 'ollamaNumCtx',
|
|
152
|
+
label: 'Ollama Context (num_ctx, 0=auto)',
|
|
153
|
+
getValue: () => String(config.get('ollamaNumCtx') ?? 0),
|
|
154
|
+
type: 'text',
|
|
155
|
+
},
|
|
134
156
|
{
|
|
135
157
|
key: 'customBaseUrl',
|
|
136
158
|
label: 'Custom Base URL',
|
package/dist/utils/agentChat.js
CHANGED
|
@@ -373,6 +373,38 @@ additionalTools) {
|
|
|
373
373
|
if (prefs)
|
|
374
374
|
openRouterExtras.provider = prefs;
|
|
375
375
|
}
|
|
376
|
+
// Opt-in native Ollama transport for AGENT mode. When `ollamaNativeApi`
|
|
377
|
+
// is on, route through /api/chat (honors num_ctx + keep_alive + the real
|
|
378
|
+
// context window) and parse native tool_calls. OFF by default → the /v1
|
|
379
|
+
// path below runs unchanged. Returns the agent response directly.
|
|
380
|
+
if (providerId === 'ollama' && config.get('ollamaNativeApi') === true) {
|
|
381
|
+
const { streamOllamaNativeChat, getOllamaContextLength } = await import('../api/ollamaNative.js');
|
|
382
|
+
const ollamaUrl = config.get('ollamaUrl') || 'http://localhost:11434';
|
|
383
|
+
const cfgCtx = Number(config.get('ollamaNumCtx')) || 0;
|
|
384
|
+
const numCtx = cfgCtx > 0 ? cfgCtx : ((await getOllamaContextLength(model, ollamaUrl)) ?? undefined);
|
|
385
|
+
const res = await streamOllamaNativeChat({
|
|
386
|
+
baseUrl: ollamaUrl,
|
|
387
|
+
model,
|
|
388
|
+
messages: [],
|
|
389
|
+
rawMessages: [{ role: 'system', content: systemPrompt }, ...messages],
|
|
390
|
+
tools: getOpenAITools(additionalTools),
|
|
391
|
+
numCtx,
|
|
392
|
+
keepAlive: config.get('ollamaKeepAlive') || undefined,
|
|
393
|
+
temperature: requiresDefaultTemperature(providerId) ? undefined : Number(config.get('temperature')),
|
|
394
|
+
timeoutMs,
|
|
395
|
+
onChunk: useStreaming ? onChunk : undefined,
|
|
396
|
+
});
|
|
397
|
+
if (res.promptTokens != null && res.completionTokens != null) {
|
|
398
|
+
recordTokenUsage({ promptTokens: res.promptTokens, completionTokens: res.completionTokens, totalTokens: res.promptTokens + res.completionTokens }, model, providerId);
|
|
399
|
+
}
|
|
400
|
+
const toolCalls = res.toolCalls.map((tc) => ({ tool: tc.name, parameters: tc.arguments }));
|
|
401
|
+
if (toolCalls.length === 0 && res.text) {
|
|
402
|
+
const textCalls = parseToolCalls(res.text);
|
|
403
|
+
if (textCalls.length > 0)
|
|
404
|
+
return { content: res.text, toolCalls: textCalls, usedNativeTools: false };
|
|
405
|
+
}
|
|
406
|
+
return { content: res.text, toolCalls, usedNativeTools: true };
|
|
407
|
+
}
|
|
376
408
|
body = {
|
|
377
409
|
model, messages: [{ role: 'system', content: systemPrompt }, ...messages],
|
|
378
410
|
tools: getOpenAITools(additionalTools), tool_choice: 'auto', stream: useStreaming,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Curated catalog of recommended local coding models for Ollama, surfaced via
|
|
3
|
+
* `/model browse`. Mirrors the MCP catalog pattern: a hand-picked shortlist so
|
|
4
|
+
* users don't have to hunt ollama.com for good coding models.
|
|
5
|
+
*
|
|
6
|
+
* Each entry's `pull` is the exact `ollama pull` tag. `params` is the parameter
|
|
7
|
+
* count (drives the agent-mode hint — <7B models struggle with tool-calling
|
|
8
|
+
* loops). `vram` is a rough "comfortable" requirement for the default quant.
|
|
9
|
+
*
|
|
10
|
+
* Curated, not exhaustive — users can still `/model pull <anything>`.
|
|
11
|
+
*/
|
|
12
|
+
export interface OllamaCatalogEntry {
|
|
13
|
+
/** Exact `ollama pull` tag, e.g. "qwen2.5-coder:7b". */
|
|
14
|
+
pull: string;
|
|
15
|
+
/** Display name. */
|
|
16
|
+
name: string;
|
|
17
|
+
/** Parameter count in billions (for the agent-mode hint). */
|
|
18
|
+
params: number;
|
|
19
|
+
/** Rough comfortable RAM/VRAM for the default quantization. */
|
|
20
|
+
vram: string;
|
|
21
|
+
/** One-line description. */
|
|
22
|
+
description: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Recommended coding models. Ordered roughly best-for-agent first within size
|
|
26
|
+
* tiers. All are real Ollama library tags as of this release.
|
|
27
|
+
*/
|
|
28
|
+
export declare const OLLAMA_CODING_MODELS: OllamaCatalogEntry[];
|
|
29
|
+
/** Agent-mode suitability hint for a catalog entry (≥7B → agent-capable). */
|
|
30
|
+
export declare function catalogAgentHint(params: number): string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Curated catalog of recommended local coding models for Ollama, surfaced via
|
|
3
|
+
* `/model browse`. Mirrors the MCP catalog pattern: a hand-picked shortlist so
|
|
4
|
+
* users don't have to hunt ollama.com for good coding models.
|
|
5
|
+
*
|
|
6
|
+
* Each entry's `pull` is the exact `ollama pull` tag. `params` is the parameter
|
|
7
|
+
* count (drives the agent-mode hint — <7B models struggle with tool-calling
|
|
8
|
+
* loops). `vram` is a rough "comfortable" requirement for the default quant.
|
|
9
|
+
*
|
|
10
|
+
* Curated, not exhaustive — users can still `/model pull <anything>`.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Recommended coding models. Ordered roughly best-for-agent first within size
|
|
14
|
+
* tiers. All are real Ollama library tags as of this release.
|
|
15
|
+
*/
|
|
16
|
+
export const OLLAMA_CODING_MODELS = [
|
|
17
|
+
// ── Coding-tuned, agent-capable ──────────────────────────────────────────
|
|
18
|
+
{ pull: 'qwen2.5-coder:32b', name: 'Qwen2.5 Coder 32B', params: 32, vram: '~20 GB', description: 'Top open coding model — strong at tool use and multi-file edits' },
|
|
19
|
+
{ pull: 'qwen2.5-coder:14b', name: 'Qwen2.5 Coder 14B', params: 14, vram: '~9 GB', description: 'Great balance of quality and speed for agent work' },
|
|
20
|
+
{ pull: 'qwen2.5-coder:7b', name: 'Qwen2.5 Coder 7B', params: 7, vram: '~5 GB', description: 'Smallest Qwen Coder that still handles agent mode' },
|
|
21
|
+
{ pull: 'deepseek-coder-v2:16b', name: 'DeepSeek Coder V2 16B', params: 16, vram: '~10 GB', description: 'MoE coding model — fast for its quality' },
|
|
22
|
+
// ── General-purpose, agent-capable ───────────────────────────────────────
|
|
23
|
+
{ pull: 'llama3.1:8b', name: 'Llama 3.1 8B', params: 8, vram: '~5 GB', description: 'Solid all-rounder; reliable tool-calling for its size' },
|
|
24
|
+
{ pull: 'llama3.1:70b', name: 'Llama 3.1 70B', params: 70, vram: '~40 GB', description: 'Large general model — strongest reasoning if you have the VRAM' },
|
|
25
|
+
{ pull: 'mistral-nemo:12b', name: 'Mistral Nemo 12B', params: 12, vram: '~8 GB', description: '128K context general model, good instruction following' },
|
|
26
|
+
{ pull: 'gemma2:9b', name: 'Gemma 2 9B', params: 9, vram: '~6 GB', description: 'Google open model — capable general assistant' },
|
|
27
|
+
// ── Reasoning ────────────────────────────────────────────────────────────
|
|
28
|
+
{ pull: 'deepseek-r1:14b', name: 'DeepSeek R1 14B', params: 14, vram: '~9 GB', description: 'Reasoning model — thinks before answering; good for hard bugs' },
|
|
29
|
+
{ pull: 'deepseek-r1:8b', name: 'DeepSeek R1 8B', params: 8, vram: '~5 GB', description: 'Smaller reasoning model for modest hardware' },
|
|
30
|
+
// ── Small / fast (chat-first; weaker at agent loops) ─────────────────────
|
|
31
|
+
{ pull: 'qwen2.5-coder:3b', name: 'Qwen2.5 Coder 3B', params: 3, vram: '~3 GB', description: 'Fast inline completions / chat; light for full agent tasks' },
|
|
32
|
+
{ pull: 'llama3.2:3b', name: 'Llama 3.2 3B', params: 3, vram: '~3 GB', description: 'Tiny + fast; great for quick chat on low-end machines' },
|
|
33
|
+
];
|
|
34
|
+
/** Agent-mode suitability hint for a catalog entry (≥7B → agent-capable). */
|
|
35
|
+
export function catalogAgentHint(params) {
|
|
36
|
+
return params >= 7 ? '✓ agent mode' : '⚠ chat / completions (small)';
|
|
37
|
+
}
|
|
@@ -16,6 +16,7 @@ const MODEL_CONTEXT_WINDOWS = {
|
|
|
16
16
|
'gpt-5.4-mini': 400_000,
|
|
17
17
|
'gpt-5.4-nano': 400_000,
|
|
18
18
|
// Anthropic
|
|
19
|
+
'claude-opus-4-8': 1_000_000,
|
|
19
20
|
'claude-opus-4-7': 1_000_000,
|
|
20
21
|
'claude-opus-4-6': 1_000_000,
|
|
21
22
|
'claude-sonnet-4-6': 1_000_000,
|
|
@@ -25,9 +26,10 @@ const MODEL_CONTEXT_WINDOWS = {
|
|
|
25
26
|
'deepseek-v4-flash': 1_000_000,
|
|
26
27
|
// Google
|
|
27
28
|
'gemini-3.1-pro-preview': 1_048_576,
|
|
29
|
+
'gemini-3.5-flash': 1_000_000,
|
|
28
30
|
'gemini-3-flash-preview': 1_000_000,
|
|
29
31
|
// MiniMax
|
|
30
|
-
'MiniMax-
|
|
32
|
+
'MiniMax-M3': 524_288,
|
|
31
33
|
};
|
|
32
34
|
const DEFAULT_CONTEXT_WINDOW = 128_000;
|
|
33
35
|
/**
|
|
@@ -50,6 +52,7 @@ const MODEL_PRICING = {
|
|
|
50
52
|
'gpt-5.4-mini': { inputPer1M: 0.75, outputPer1M: 4.50 },
|
|
51
53
|
'gpt-5.4-nano': { inputPer1M: 0.20, outputPer1M: 1.25 },
|
|
52
54
|
// Anthropic
|
|
55
|
+
'claude-opus-4-8': { inputPer1M: 5.00, outputPer1M: 25.00 },
|
|
53
56
|
'claude-opus-4-7': { inputPer1M: 5.00, outputPer1M: 25.00 },
|
|
54
57
|
'claude-opus-4-6': { inputPer1M: 5.00, outputPer1M: 25.00 },
|
|
55
58
|
'claude-sonnet-4-6': { inputPer1M: 3.00, outputPer1M: 15.00 },
|
|
@@ -59,9 +62,10 @@ const MODEL_PRICING = {
|
|
|
59
62
|
'deepseek-v4-flash': { inputPer1M: 0.14, outputPer1M: 0.28 },
|
|
60
63
|
// Google
|
|
61
64
|
'gemini-3.1-pro-preview': { inputPer1M: 2.00, outputPer1M: 12.00 },
|
|
65
|
+
'gemini-3.5-flash': { inputPer1M: 1.50, outputPer1M: 9.00 },
|
|
62
66
|
'gemini-3-flash-preview': { inputPer1M: 0.50, outputPer1M: 3.00 },
|
|
63
67
|
// MiniMax
|
|
64
|
-
'MiniMax-
|
|
68
|
+
'MiniMax-M3': { inputPer1M: 0.60, outputPer1M: 2.40 },
|
|
65
69
|
};
|
|
66
70
|
export function getPricingTable() {
|
|
67
71
|
return Object.entries(MODEL_PRICING).map(([model, p]) => ({ model, ...p }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeep",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.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",
|