codeep 3.4.1 → 3.5.0

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.
@@ -9,7 +9,7 @@ import { config, getCurrentProvider, getModelsForCurrentProvider, PROTOCOLS, LAN
9
9
  import { setTelegramToken, clearTelegramToken, hasTelegramToken } from '../utils/telegramCredentials.js';
10
10
  import { getProjectContext } from '../utils/project.js';
11
11
  import { getCurrentVersion } from '../utils/update.js';
12
- import { getProviderList, getProvider, modelSupportsReasoningEffort, reasoningParamsFor, availableReasoningTiers, resolveReasoningTier, REASONING_TIERS } from '../config/providers.js';
12
+ import { getProviderList, getProvider, modelSupportsReasoningEffort, reasoningParamsFor, availableReasoningTiers, resolveReasoningTier, agentTurnReasoningNote, replacementModelFor, REASONING_TIERS } from '../config/providers.js';
13
13
  import { setProjectContext } from '../api/index.js';
14
14
  import { runSkill, runCommandChain } from './agentExecution.js';
15
15
  import { loadProjectIntelligence, saveProjectIntelligence, INTELLIGENCE_NOT_SAVED } from '../utils/projectIntelligence.js';
@@ -301,14 +301,15 @@ export async function handleCommand(command, args, ctx) {
301
301
  ctx.app.notify('Thinking effort: auto — each model uses its own default.');
302
302
  }
303
303
  else if (!supported) {
304
- ctx.app.notify(`Thinking effort set to "${sub}", but ${model} has no graded thinking control — it will be ignored until you switch to a model that does (e.g. Opus 5, GPT-5.x or GPT-6, Gemini 3, DeepSeek V4.1 Flash, GLM-5.x, Kimi K3).`);
304
+ ctx.app.notify(`Thinking effort set to "${sub}", but ${model} has no graded thinking control — it will be ignored until you switch to a model that does (e.g. Opus 5.5, GPT-5.x or GPT-6, Gemini 3, DeepSeek V4, GLM-5.x, Kimi K3).`);
305
305
  }
306
306
  else {
307
307
  // Tell the user what THIS model will actually run (the tier may
308
308
  // collapse onto a level the model distinguishes, e.g. medium→high on Kimi K3).
309
309
  const resolved = resolveReasoningTier(providerId, model, sub);
310
310
  const note = resolved === sub ? '' : ` (${model} runs this as "${resolved}")`;
311
- ctx.app.notify(`Thinking effort: ${sub}${note} — sending ${JSON.stringify(reasoningParamsFor(providerId, model, sub))}.`);
311
+ const agentNote = agentTurnReasoningNote(providerId, model);
312
+ ctx.app.notify(`Thinking effort: ${sub}${note} — sending ${JSON.stringify(reasoningParamsFor(providerId, model, sub))}.${agentNote ? ` ${agentNote}` : ''}`);
312
313
  }
313
314
  break;
314
315
  }
@@ -333,8 +334,11 @@ export async function handleCommand(command, args, ctx) {
333
334
  }
334
335
  if (supported)
335
336
  tLines.push(`**Available** ${available.join(' · ')}`);
337
+ const agentNote = agentTurnReasoningNote(providerId, model);
338
+ if (agentNote)
339
+ tLines.push(`**Agent turns** reasoning off — ${agentNote}`);
336
340
  tLines.push('');
337
- tLines.push('Sets how hard the model reasons. Each model offers only the levels it distinguishes (DeepSeek → high · max; Kimi K3 → low · high · max; Gemini → low · high; Opus/Sonnet & GPT-5.x → the full set). The setting is global and clamps to the active model, so it never sends a value the API rejects. `/effort` is an alias.');
341
+ tLines.push('Sets how hard the model reasons. Each model offers only the levels it distinguishes (DeepSeek & Kimi K3 → low · high · max; Gemini → low · medium · high; Opus/Sonnet & GPT-5.x/6 → the full set). The setting is global and clamps to the active model, so it never sends a value the API rejects. `/effort` is an alias.');
338
342
  ctx.app.addMessage({ role: 'system', content: tLines.join('\n') });
339
343
  break;
340
344
  }
@@ -1737,14 +1741,19 @@ Format: use headers per category, only include categories where you found issues
1737
1741
  const replacedCount = ctx.app.getMessages().length;
1738
1742
  ctx.app.setMessages(cp.messages);
1739
1743
  saveSession(ctx.sessionId, cp.messages, ctx.projectPath);
1740
- // Switch provider/model back to checkpoint state if different.
1744
+ // Switch provider/model back to checkpoint state if different. A
1745
+ // checkpoint predates any later retirement, so its model goes through the
1746
+ // same map as a stored config (`gpt-6-astra` comes back as `gpt-6-sol`),
1747
+ // looked up on the provider actually active after the switch.
1741
1748
  if (cp.provider && cp.provider !== getCurrentProvider().id)
1742
1749
  setProvider(cp.provider);
1743
- if (cp.model && cp.model !== config.get('model'))
1744
- config.set('model', cp.model);
1750
+ const cpModel = cp.model && (replacementModelFor(config.get('provider'), cp.model) ?? cp.model);
1751
+ if (cpModel && cpModel !== config.get('model'))
1752
+ config.set('model', cpModel);
1753
+ const movedNote = cpModel !== cp.model ? ` (the checkpoint's \`${cp.model}\` is no longer offered)` : '';
1745
1754
  ctx.app.addMessage({
1746
1755
  role: 'system',
1747
- content: `# Rewound to ${cp.name ? `**${cp.name}**` : `\`${cp.id}\``}\n\nRestored ${cp.messages.length} message${cp.messages.length === 1 ? '' : 's'} (was ${replacedCount}). Provider: \`${cp.provider}\` · Model: \`${cp.model}\`\n\n${buildRewindGitHint(cp)}`,
1756
+ content: `# Rewound to ${cp.name ? `**${cp.name}**` : `\`${cp.id}\``}\n\nRestored ${cp.messages.length} message${cp.messages.length === 1 ? '' : 's'} (was ${replacedCount}). Provider: \`${cp.provider}\` · Model: \`${cpModel}\`${movedNote}\n\n${buildRewindGitHint(cp)}`,
1748
1757
  });
1749
1758
  break;
1750
1759
  }
@@ -14,6 +14,22 @@ import { type TrustBearingWrite } from './toolExecution';
14
14
  import { undoLastAction, undoAllActions, getCurrentSession, getRecentSessions, formatSession, ActionSession } from './history';
15
15
  import { VerifyResult } from './verify';
16
16
  import { TaskPlan, SubTask } from './taskPlanner';
17
+ /**
18
+ * The text an assistant turn is kept as in the history sent back next time.
19
+ *
20
+ * The loop stores each turn as plain text, and a turn that only called tools
21
+ * has none: Claude often skips the narration, and Opus 5.5 and Fable 5.1 move
22
+ * it into thinking blocks, which the stream parser does not keep. Stored as
23
+ * '', that turn is an empty non-final message on the next request, which
24
+ * Anthropic's Messages API refuses with a 400 ("all messages must have
25
+ * non-empty content except for the optional final assistant message").
26
+ * agentChat turns that 400 into the text-tool fallback, which sends the same
27
+ * history and fails the same way, so the run died on its second iteration.
28
+ * Naming the tools keeps the turn truthful and non-empty.
29
+ */
30
+ export declare function assistantHistoryText(content: string, toolCalls: ReadonlyArray<{
31
+ tool: string;
32
+ }>): string;
17
33
  export type PermissionOutcome = 'allow_once' | 'allow_always' | 'reject_once' | 'reject_always';
18
34
  export type PermissionDecision = 'allow-once' | 'allow-always' | 'deny-once' | 'deny-always';
19
35
  /**
@@ -56,6 +56,27 @@ function truncateToolResult(output, toolName) {
56
56
  const truncated = output.length - TOOL_RESULT_MAX_CHARS;
57
57
  return `${kept}\n[... ${truncated} chars truncated — use search_code or read specific sections if you need more]`;
58
58
  }
59
+ // ─── Assistant turns in the flattened history ─────────────────────────────────
60
+ /**
61
+ * The text an assistant turn is kept as in the history sent back next time.
62
+ *
63
+ * The loop stores each turn as plain text, and a turn that only called tools
64
+ * has none: Claude often skips the narration, and Opus 5.5 and Fable 5.1 move
65
+ * it into thinking blocks, which the stream parser does not keep. Stored as
66
+ * '', that turn is an empty non-final message on the next request, which
67
+ * Anthropic's Messages API refuses with a 400 ("all messages must have
68
+ * non-empty content except for the optional final assistant message").
69
+ * agentChat turns that 400 into the text-tool fallback, which sends the same
70
+ * history and fails the same way, so the run died on its second iteration.
71
+ * Naming the tools keeps the turn truthful and non-empty.
72
+ */
73
+ export function assistantHistoryText(content, toolCalls) {
74
+ if (content.trim())
75
+ return content;
76
+ if (toolCalls.length > 0)
77
+ return `Using ${[...new Set(toolCalls.map(t => t.tool))].join(', ')}.`;
78
+ return '(no reply)';
79
+ }
59
80
  // ─── Context window compression ───────────────────────────────────────────────
60
81
  const CONTEXT_COMPRESS_THRESHOLD = 200_000; // ~50K tokens, safe for all providers
61
82
  const RECENT_MESSAGES_TO_KEEP = 6; // Always preserve the last N messages verbatim
@@ -1120,7 +1141,7 @@ export async function runAgent(prompt, projectContext, options = {}) {
1120
1141
  if (hasIncompleteWork) {
1121
1142
  debug('Model wants to continue, prompting for next action');
1122
1143
  incompleteWorkRetries++;
1123
- messages.push({ role: 'assistant', content });
1144
+ messages.push({ role: 'assistant', content: assistantHistoryText(content, toolCalls) });
1124
1145
  messages.push({
1125
1146
  role: 'user',
1126
1147
  content: 'Continue. Execute the tool calls now.'
@@ -1136,8 +1157,8 @@ export async function runAgent(prompt, projectContext, options = {}) {
1136
1157
  debug(`Agent finished at iteration ${iteration}`);
1137
1158
  break;
1138
1159
  }
1139
- // Add assistant response to history
1140
- messages.push({ role: 'assistant', content });
1160
+ // Add assistant response to history — never empty (see assistantHistoryText).
1161
+ messages.push({ role: 'assistant', content: assistantHistoryText(content, toolCalls) });
1141
1162
  // Execute tool calls
1142
1163
  const toolResults = [];
1143
1164
  for (const toolCall of toolCalls) {
@@ -20,7 +20,7 @@ import { config, getApiKey, resolveBaseUrl } from '../config/index.js';
20
20
  import { loadProjectIntelligence, generateContextFromIntelligence } from './projectIntelligence.js';
21
21
  import { formatCommandIndex } from './commandIndex.js';
22
22
  import { syncProgress, generateProjectId } from './codeepCloud.js';
23
- import { getProviderAuthHeader, supportsNativeTools, getEffectiveMaxTokens, usesMaxCompletionTokens, requiresDefaultTemperature, modelRejectsSamplingParams, isNoApiKeyProvider, reasoningParamsFor, providerNoStreamWithTools } from '../config/providers.js';
23
+ import { getProviderAuthHeader, supportsNativeTools, getEffectiveMaxTokens, usesMaxCompletionTokens, requiresDefaultTemperature, modelRejectsSamplingParams, isNoApiKeyProvider, reasoningParamsFor, providerNoStreamWithTools, minResponseTokensFor } from '../config/providers.js';
24
24
  import { recordTokenUsage, extractOpenAIUsage, extractAnthropicUsage } from './tokenTracker.js';
25
25
  import { parseOpenAIToolCalls, parseAnthropicToolCalls, parseToolCalls } from './toolParsing.js';
26
26
  import { formatToolDefinitions, getOpenAITools, getAnthropicTools } from './tools.js';
@@ -484,10 +484,17 @@ additionalTools, runtime) {
484
484
  // Provider-level guard (OpenAI GPT-5+) OR model-level guard — Anthropic's
485
485
  // Fable 5 / Opus 4.7+ reject temperature with a 400; omission is safe.
486
486
  const tempParam = (requiresDefaultTemperature(providerId) || modelRejectsSamplingParams(model)) ? {} : { temperature: config.get('temperature') };
487
- // Thinking-effort tier → provider-shaped param ({} for 'auto'/unsupported).
488
- const reasoningParam = reasoningParamsFor(providerId, model, config.get('reasoningEffort'));
487
+ const tier = config.get('reasoningEffort');
488
+ // Room for the answer after the thinking (Opus 5.5 thinks on every turn).
489
+ const responseBudget = Math.max(config.get('maxTokens'), 16384, minResponseTokensFor(model, tier));
489
490
  if (protocol === 'openai') {
490
- const maxTok = getEffectiveMaxTokens(providerId, Math.max(config.get('maxTokens'), 16384));
491
+ const openAITools = getOpenAITools(additionalTools, allowedTools);
492
+ // Thinking-effort tier → provider-shaped param ({} for 'auto'/unsupported).
493
+ // This request carries tools, which GPT-6 Sol/Luna on Chat Completions
494
+ // accept only at reasoning_effort "none"; told so, reasoningParamsFor
495
+ // sends that for them whatever the tier.
496
+ const openAIReasoning = reasoningParamsFor(providerId, model, tier, { tools: openAITools.length > 0 });
497
+ const maxTok = getEffectiveMaxTokens(providerId, responseBudget);
491
498
  const tokParam = usesMaxCompletionTokens(providerId) ? { max_completion_tokens: maxTok } : { max_tokens: maxTok };
492
499
  endpoint = `${baseUrl}/chat/completions`;
493
500
  // OpenRouter-specific extras: request `usage` block in the response
@@ -537,8 +544,8 @@ additionalTools, runtime) {
537
544
  }
538
545
  body = {
539
546
  model, messages: [{ role: 'system', content: systemPrompt }, ...messages],
540
- tools: getOpenAITools(additionalTools, allowedTools), tool_choice: 'auto', stream: useStreaming,
541
- ...tempParam, ...tokParam, ...reasoningParam,
547
+ tools: openAITools, tool_choice: 'auto', stream: useStreaming,
548
+ ...tempParam, ...tokParam, ...openAIReasoning,
542
549
  // Ask ALL OpenAI-compatible providers to emit a usage block in the
543
550
  // stream — without this most (DeepSeek/Kimi/Grok/Qwen/GLM/…) send no
544
551
  // usage on streamed responses and the whole turn records zero tokens.
@@ -568,7 +575,8 @@ additionalTools, runtime) {
568
575
  system: [{ type: 'text', text: systemPrompt, cache_control: { type: 'ephemeral' } }],
569
576
  messages,
570
577
  tools: cachedTools, stream: useStreaming,
571
- ...tempParam, ...reasoningParam, max_tokens: getEffectiveMaxTokens(providerId, Math.max(config.get('maxTokens'), 16384)),
578
+ ...tempParam, ...reasoningParamsFor(providerId, model, tier),
579
+ max_tokens: getEffectiveMaxTokens(providerId, responseBudget),
572
580
  };
573
581
  }
574
582
  const response = await fetch(endpoint, {
@@ -701,10 +709,14 @@ export async function agentChatFallback(messages, systemPrompt, onChunk, abortSi
701
709
  // Provider-level guard (OpenAI GPT-5+) OR model-level guard — Anthropic's
702
710
  // Fable 5 / Opus 4.7+ reject temperature with a 400; omission is safe.
703
711
  const tempParam = (requiresDefaultTemperature(providerId) || modelRejectsSamplingParams(model)) ? {} : { temperature: config.get('temperature') };
712
+ const tier = config.get('reasoningEffort');
704
713
  // Thinking-effort tier → provider-shaped param ({} for 'auto'/unsupported).
705
- const reasoningParam = reasoningParamsFor(providerId, model, config.get('reasoningEffort'));
714
+ // No `tools` array goes out on this path — the tools are text in the
715
+ // prompt — so GPT-6 Sol/Luna keep the user's tier here.
716
+ const reasoningParam = reasoningParamsFor(providerId, model, tier);
717
+ const responseBudget = Math.max(config.get('maxTokens'), 16384, minResponseTokensFor(model, tier));
706
718
  if (protocol === 'openai') {
707
- const maxTok = getEffectiveMaxTokens(providerId, Math.max(config.get('maxTokens'), 16384));
719
+ const maxTok = getEffectiveMaxTokens(providerId, responseBudget);
708
720
  const tokParam = usesMaxCompletionTokens(providerId) ? { max_completion_tokens: maxTok } : { max_tokens: maxTok };
709
721
  endpoint = `${baseUrl}/chat/completions`;
710
722
  body = {
@@ -727,7 +739,7 @@ export async function agentChatFallback(messages, systemPrompt, onChunk, abortSi
727
739
  ...messages,
728
740
  ],
729
741
  stream: Boolean(onChunk), ...tempParam, ...reasoningParam,
730
- max_tokens: getEffectiveMaxTokens(providerId, Math.max(config.get('maxTokens'), 16384)),
742
+ max_tokens: getEffectiveMaxTokens(providerId, responseBudget),
731
743
  };
732
744
  }
733
745
  const response = await fetch(endpoint, {
@@ -39,7 +39,7 @@ import { basename, join } from 'path';
39
39
  import { homedir } from 'os';
40
40
  import { leadsOutsideProject } from './projectPaths.js';
41
41
  import { config } from '../config/index.js';
42
- import { getProvider } from '../config/providers.js';
42
+ import { getProvider, replacementModelFor } from '../config/providers.js';
43
43
  const CAPABILITIES = new Set([
44
44
  'files', 'terminal', 'tests', 'git', 'web', 'mcp',
45
45
  ]);
@@ -126,7 +126,13 @@ function exactModelPreference(preference) {
126
126
  return null;
127
127
  const providerId = value.slice(0, slash).trim();
128
128
  const model = value.slice(slash + 1).trim();
129
- return providerId && model ? { providerId, model } : null;
129
+ if (!providerId || !model)
130
+ return null;
131
+ // A bot written before a vendor retired its model (or before Codeep stopped
132
+ // offering one, like `openai/gpt-6-astra`) names an id the picker no longer
133
+ // has, and the exact check below would make the whole bot unavailable. Map it
134
+ // the way the startup migration and applyProfile map a stored id.
135
+ return { providerId, model: replacementModelFor(providerId, model) ?? model };
130
136
  }
131
137
  /** Whether a structured bot's model field satisfies the portable v1 contract. */
132
138
  export function isPersonalityModelPreferenceValid(personality) {
@@ -2,7 +2,7 @@
2
2
  * Task Planning - breaks down complex tasks into subtasks
3
3
  */
4
4
  import { config, getApiKey, resolveBaseUrl } from '../config/index.js';
5
- import { getProviderAuthHeader, isNoApiKeyProvider, requiresDefaultTemperature } from '../config/providers.js';
5
+ import { getProviderAuthHeader, isNoApiKeyProvider, requiresDefaultTemperature, modelRejectsSamplingParams, minResponseTokensFor } from '../config/providers.js';
6
6
  /**
7
7
  * Ask AI to break down a complex task into subtasks
8
8
  */
@@ -51,10 +51,14 @@ Break this down into subtasks. Each task = one file or one logical unit. Respond
51
51
  const messages = [
52
52
  { role: 'user', content: systemPrompt }
53
53
  ];
54
+ // 2048 is plenty for a JSON plan, but not for a model that thinks on every
55
+ // request inside the same limit (Opus 5.5 — see minResponseTokensFor). The
56
+ // planner sends no effort, so the model runs at its own default: 'auto'.
57
+ const maxTokens = Math.max(2048, minResponseTokensFor(model, 'auto'));
54
58
  const requestBody = protocol === 'anthropic'
55
59
  ? {
56
60
  model,
57
- max_tokens: 2048,
61
+ max_tokens: maxTokens,
58
62
  messages,
59
63
  system: 'You are a task planning assistant. Respond with JSON only.',
60
64
  }
@@ -64,8 +68,12 @@ Break this down into subtasks. Each task = one file or one logical unit. Respond
64
68
  { role: 'system', content: 'You are a task planning assistant. Respond with JSON only.' },
65
69
  ...messages
66
70
  ],
67
- ...(requiresDefaultTemperature(provider) ? {} : { temperature: 0.3 }),
68
- max_tokens: 2048,
71
+ // Both guards, as in agentChat: the provider flag covers direct
72
+ // OpenAI, and the model list covers ids that reach this branch through
73
+ // OpenRouter (anthropic/claude-opus-5.5, openai/gpt-6-*), which 400 on
74
+ // a temperature they do not accept.
75
+ ...(requiresDefaultTemperature(provider) || modelRejectsSamplingParams(model) ? {} : { temperature: 0.3 }),
76
+ max_tokens: maxTokens,
69
77
  };
70
78
  const headers = {
71
79
  'Content-Type': 'application/json',
@@ -5,9 +5,10 @@ export interface TokenUsage {
5
5
  promptTokens: number;
6
6
  completionTokens: number;
7
7
  totalTokens: number;
8
- /** Anthropic prompt caching: tokens written to the cache on this call
9
- * (billed at ~1.25× input rate). Undefined for providers that don't
10
- * support caching or for calls below the cache size threshold. */
8
+ /** Prompt caching: tokens written to the cache on this call — Anthropic's
9
+ * cache_creation_input_tokens, or OpenAI-protocol
10
+ * prompt_tokens_details.cache_write_tokens (GPT-5.6+, Kimi K3). Billed at
11
+ * cacheWriteRateFor(). Undefined when the provider reports none. */
11
12
  cacheCreationTokens?: number;
12
13
  /** Anthropic prompt caching: tokens read from cache on this call
13
14
  * (billed at ~0.1× input rate — the big savings live here). */
@@ -85,8 +86,8 @@ export interface ProviderCostBreakdown {
85
86
  model: string;
86
87
  promptTokens: number;
87
88
  completionTokens: number;
88
- /** Anthropic prompt caching: tokens written to cache (billed ~1.25× input).
89
- * 0 for providers that don't report caching. */
89
+ /** Prompt caching: tokens written to cache (billed at cacheWriteRateFor()).
90
+ * 0 for providers that don't report writes. */
90
91
  cacheCreationTokens: number;
91
92
  /** Anthropic prompt caching: tokens read from cache (billed ~0.1× input).
92
93
  * 0 for providers that don't report caching. */
@@ -102,6 +103,11 @@ export interface ProviderCostBreakdown {
102
103
  * cost and a "saved" figure that disagreed with each other.
103
104
  */
104
105
  export declare function cacheReadRateFor(model: string, provider: string | undefined): number;
106
+ /**
107
+ * What writing a prompt token into the cache costs, as a multiple of the
108
+ * model's input rate. Same order as reads: model, then provider, then default.
109
+ */
110
+ export declare function cacheWriteRateFor(model: string, provider: string | undefined): number;
105
111
  /**
106
112
  * The rate note for a report. One rate reads as "0.02×"; a session mixing
107
113
  * providers reads as a range, because any single number there would be wrong
@@ -138,6 +144,8 @@ export interface CacheStats {
138
144
  /** The read rate of each metered record that read from cache, so a report
139
145
  * can state the rate that actually applied instead of assuming 0.1×. */
140
146
  cacheReadRates: number[];
147
+ /** Likewise for writes, which are not 1.25× everywhere (Kimi: 1.0×). */
148
+ cacheWriteRates: number[];
141
149
  }
142
150
  export declare function getCacheStats(): CacheStats;
143
151
  /**