codeep 2.17.0 → 2.18.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/dist/api/index.js CHANGED
@@ -343,7 +343,13 @@ async function chatOpenAI(message, history, model, apiKey, onChunk, abortSignal)
343
343
  let baseUrl = resolveBaseUrl(providerId, 'openai');
344
344
  const authHeader = getProviderAuthHeader(providerId, 'openai');
345
345
  const useCompletionTokens = usesMaxCompletionTokens(providerId);
346
- const omitTemperature = requiresDefaultTemperature(providerId);
346
+ // Two independent reasons to leave temperature out, and both must be checked
347
+ // here: the provider-level flag (endpoints that only accept the default) and
348
+ // the per-model one (generations that removed the sampling params outright,
349
+ // e.g. gemini-3.7-flash). Testing only the provider flag let a rejecting
350
+ // model through, because every model this rule covers is served over the
351
+ // OpenAI-compatible path, not the Anthropic one.
352
+ const omitTemperature = requiresDefaultTemperature(providerId) || modelRejectsSamplingParams(model);
347
353
  if (!baseUrl) {
348
354
  throw new Error(`Provider ${providerId} does not support OpenAI protocol`);
349
355
  }
@@ -34,6 +34,10 @@ export interface ProviderConfig {
34
34
  subscribeUrl?: string;
35
35
  noApiKey?: boolean;
36
36
  dynamicModels?: boolean;
37
+ /** Billed as a flat subscription (or free) — token counts are real, per-token
38
+ * cost is not. Cost surfaces must say "included in plan" instead of pricing
39
+ * these tokens at the provider's pay-per-use rates. */
40
+ flatFee?: boolean;
37
41
  groupLabel?: string;
38
42
  hint?: string;
39
43
  mcpEndpoints?: {
@@ -60,6 +64,11 @@ export declare function getProviderModels(providerId: string): {
60
64
  }[];
61
65
  export declare function isNoApiKeyProvider(providerId: string): boolean;
62
66
  export declare function isDynamicModelsProvider(providerId: string): boolean;
67
+ /**
68
+ * Returns true if the provider bills a flat subscription (or is free), so any
69
+ * per-token dollar figure we compute for it is invented — see `flatFee`.
70
+ */
71
+ export declare function isFlatFeeProvider(providerId: string): boolean;
63
72
  export declare function getProviderBaseUrl(providerId: string, protocol: 'openai' | 'anthropic'): string | null;
64
73
  export declare function getProviderAuthHeader(providerId: string, protocol: 'openai' | 'anthropic'): 'Bearer' | 'x-api-key';
65
74
  export declare function getProviderMcpEndpoints(providerId: string): ProviderConfig['mcpEndpoints'] | null;
@@ -18,15 +18,19 @@ export const PROVIDERS = {
18
18
  },
19
19
  },
20
20
  models: [
21
- { id: 'glm-5.2', name: 'GLM-5.2', description: 'Latest flagship for project-scale engineering (1M context)' },
21
+ // GLM-5.3 is Coding-Plan only — the standalone pay-per-use API does not
22
+ // accept it yet, so it must NOT be copied into `z.ai-api`.
23
+ { id: 'glm-5.3', name: 'GLM-5.3', description: 'Latest flagship for project-scale engineering (1M context)' },
24
+ { id: 'glm-5.2', name: 'GLM-5.2', description: 'Previous flagship for project-scale engineering (1M context)' },
22
25
  { id: 'glm-5-turbo', name: 'GLM-5 Turbo', description: 'Fast GLM-5 variant, available to all users' },
23
26
  ],
24
- defaultModel: 'glm-5.2',
27
+ defaultModel: 'glm-5.3',
25
28
  defaultProtocol: 'openai',
26
29
  maxOutputTokens: 131_072,
27
30
  envKey: 'ZAI_API_KEY',
28
31
  subscribeUrl: 'https://z.ai/subscribe?ic=NXYNXZOV14',
29
32
  groupLabel: 'Z.AI — Subscription (GLM Coding Plan)',
33
+ flatFee: true,
30
34
  hint: 'Uses your Z.AI subscription — no per-token charges.',
31
35
  mcpEndpoints: {
32
36
  webSearch: 'https://api.z.ai/api/mcp/web_search_prime/mcp',
@@ -81,6 +85,7 @@ export const PROVIDERS = {
81
85
  envKey: 'ZAI_CN_API_KEY',
82
86
  subscribeUrl: 'https://open.bigmodel.cn/glm-coding',
83
87
  groupLabel: 'Z.AI China — Subscription (GLM Coding Plan)',
88
+ flatFee: true,
84
89
  hint: 'Uses your ZhipuAI China subscription.',
85
90
  mcpEndpoints: {
86
91
  webSearch: 'https://open.bigmodel.cn/api/mcp/web_search_prime/mcp',
@@ -133,6 +138,7 @@ export const PROVIDERS = {
133
138
  envKey: 'MINIMAX_API_KEY',
134
139
  subscribeUrl: 'https://platform.minimax.io/subscribe/coding-plan?code=2lWvoWUhrp&source=link',
135
140
  groupLabel: 'MiniMax — Subscription',
141
+ flatFee: true,
136
142
  hint: 'Uses your MiniMax subscription — no per-token charges.',
137
143
  },
138
144
  'minimax-api': {
@@ -178,6 +184,7 @@ export const PROVIDERS = {
178
184
  envKey: 'MINIMAX_CN_API_KEY',
179
185
  subscribeUrl: 'https://platform.minimaxi.com',
180
186
  groupLabel: 'MiniMax China — Subscription',
187
+ flatFee: true,
181
188
  hint: 'Uses your MiniMax China subscription.',
182
189
  },
183
190
  'deepseek': {
@@ -230,6 +237,7 @@ export const PROVIDERS = {
230
237
  envKey: 'KIMI_CODE_API_KEY',
231
238
  subscribeUrl: 'https://www.kimi.com/code',
232
239
  groupLabel: 'Kimi — Subscription (Kimi Code)',
240
+ flatFee: true,
233
241
  hint: 'Uses your Kimi Code subscription — no per-token charges. Key from kimi.com/code/console.',
234
242
  },
235
243
  'kimi-api': {
@@ -283,10 +291,15 @@ export const PROVIDERS = {
283
291
  openai: { baseUrl: 'https://api.x.ai/v1', authHeader: 'Bearer', supportsNativeTools: true },
284
292
  },
285
293
  models: [
286
- { id: 'grok-4.5', name: 'Grok 4.5', description: 'Flagship reasoning model — highest quality, 500K context' },
294
+ { id: 'grok-4.6', name: 'Grok 4.6', description: 'Flagship reasoning model — xAI recommends it for code, 500K context' },
295
+ { id: 'grok-4.5', name: 'Grok 4.5', description: 'Previous flagship reasoning model, 500K context' },
287
296
  { id: 'grok-build-0.1', name: 'Grok Build 0.1', description: 'Agentic coding model — fast, 256K context' },
288
- { id: 'grok-4.3', name: 'Grok 4.3', description: 'Previous flagship, 1M context' },
297
+ { id: 'grok-4.3', name: 'Grok 4.3', description: 'Older flagship, 1M context' },
289
298
  ],
299
+ // Stays on the agentic coder, not the new flagship. grok-4.6 is the better
300
+ // model and xAI recommends it for code, but it bills 2x input and 3x output
301
+ // against grok-build-0.1 — moving every unpinned user onto it silently is
302
+ // not ours to decide. It is one `/model` away for anyone who wants it.
290
303
  defaultModel: 'grok-build-0.1',
291
304
  defaultProtocol: 'openai',
292
305
  useMaxCompletionTokens: true, // reasoning models reject max_tokens
@@ -317,6 +330,7 @@ export const PROVIDERS = {
317
330
  envKey: 'BAILIAN_CODING_PLAN_API_KEY',
318
331
  subscribeUrl: 'https://www.alibabacloud.com/help/en/model-studio/qwen-code-coding-plan',
319
332
  groupLabel: 'Qwen — Subscription (Coding Plan)',
333
+ flatFee: true,
320
334
  hint: 'Uses your Qwen Coding Plan — no per-token charges. sk-sp-… key from Model Studio. Interactive coding use only.',
321
335
  },
322
336
  'qwen-api': {
@@ -359,6 +373,7 @@ export const PROVIDERS = {
359
373
  envKey: 'BAILIAN_TOKEN_PLAN_API_KEY',
360
374
  subscribeUrl: 'https://modelstudio.console.alibabacloud.com/',
361
375
  groupLabel: 'Qwen — Subscription (Token Plan)',
376
+ flatFee: true,
362
377
  hint: 'Uses monthly Token Plan credits. Requires a separate sk-sp-… Token Plan key.',
363
378
  },
364
379
  'qwen-cn': {
@@ -379,6 +394,7 @@ export const PROVIDERS = {
379
394
  envKey: 'BAILIAN_CODING_PLAN_CN_API_KEY',
380
395
  subscribeUrl: 'https://bailian.console.aliyun.com/',
381
396
  groupLabel: 'Qwen China — Subscription (Coding Plan)',
397
+ flatFee: true,
382
398
  hint: 'Uses your Qwen Coding Plan (China). sk-sp-… key from Bailian.',
383
399
  },
384
400
  'qwen-cn-api': {
@@ -418,6 +434,7 @@ export const PROVIDERS = {
418
434
  envKey: 'MODELSCOPE_API_KEY',
419
435
  subscribeUrl: 'https://modelscope.cn/my/myaccesstoken',
420
436
  groupLabel: 'ModelScope — Free (Qwen)',
437
+ flatFee: true,
421
438
  hint: 'Fetches the live free catalog for your ModelScope token; availability and limits vary by account.',
422
439
  },
423
440
  'openai': {
@@ -478,7 +495,8 @@ export const PROVIDERS = {
478
495
  },
479
496
  models: [
480
497
  { id: 'gemini-3.1-pro-preview', name: 'Gemini 3.1 Pro', description: 'Most capable Gemini model' },
481
- { id: 'gemini-3.6-flash', name: 'Gemini 3.6 Flash', description: 'Latest production Flash model' },
498
+ { id: 'gemini-3.7-flash', name: 'Gemini 3.7 Flash', description: 'Latest production Flash model — adjustable thinking, 64K output' },
499
+ { id: 'gemini-3.6-flash', name: 'Gemini 3.6 Flash', description: 'Previous production Flash model' },
482
500
  { id: 'gemini-3.5-flash', name: 'Gemini 3.5 Flash', description: 'Stable frontier Flash model for coding and long agentic tasks' },
483
501
  { id: 'gemini-3.5-flash-lite', name: 'Gemini 3.5 Flash-Lite', description: 'Latest low-latency, low-cost workhorse' },
484
502
  ],
@@ -510,11 +528,11 @@ export const PROVIDERS = {
510
528
  { id: 'anthropic/claude-sonnet-5', name: 'Claude Sonnet 5', description: 'Anthropic — balanced' },
511
529
  { id: 'openai/gpt-5.6-sol', name: 'GPT-5.6 Sol', description: 'OpenAI — flagship' },
512
530
  { id: 'openai/gpt-5.6-luna', name: 'GPT-5.6 Luna', description: 'OpenAI — fast/efficient' },
513
- { id: 'google/gemini-3.6-flash', name: 'Gemini 3.6 Flash', description: 'Google — latest production Flash' },
531
+ { id: 'google/gemini-3.7-flash', name: 'Gemini 3.7 Flash', description: 'Google — latest production Flash' },
514
532
  { id: 'deepseek/deepseek-v4-pro', name: 'DeepSeek V4 Pro', description: 'DeepSeek — flagship agentic model' },
515
533
  { id: 'moonshotai/kimi-k3', name: 'Kimi K3', description: 'Moonshot — long-horizon coding' },
516
534
  { id: 'qwen/qwen3.8-max', name: 'Qwen 3.8 Max', description: 'Alibaba — latest flagship' },
517
- { id: 'x-ai/grok-4.5', name: 'Grok 4.5', description: 'xAI — flagship reasoning' },
535
+ { id: 'x-ai/grok-4.6', name: 'Grok 4.6', description: 'xAI — flagship reasoning' },
518
536
  ],
519
537
  defaultModel: 'openrouter/auto',
520
538
  defaultProtocol: 'openai',
@@ -695,6 +713,13 @@ export function isNoApiKeyProvider(providerId) {
695
713
  export function isDynamicModelsProvider(providerId) {
696
714
  return PROVIDERS[providerId]?.dynamicModels === true;
697
715
  }
716
+ /**
717
+ * Returns true if the provider bills a flat subscription (or is free), so any
718
+ * per-token dollar figure we compute for it is invented — see `flatFee`.
719
+ */
720
+ export function isFlatFeeProvider(providerId) {
721
+ return PROVIDERS[providerId]?.flatFee === true;
722
+ }
698
723
  export function getProviderBaseUrl(providerId, protocol) {
699
724
  const provider = PROVIDERS[providerId];
700
725
  if (!provider)
@@ -745,14 +770,23 @@ export function providerNoStreamWithTools(providerId) {
745
770
  * MODEL-level check, not a provider-level one (requiresDefaultTemperature
746
771
  * can't express it). Omitting the field is always safe — the API treats
747
772
  * omission as default. Kimi K2.x code/thinking models fix temperature
748
- * internally and 400 on any custom value, so they're here too.
773
+ * internally and 400 on any custom value, so they're here too. Google removed
774
+ * the deprecated sampling parameters outright in the Gemini 3.7 generation.
749
775
  */
750
776
  const SAMPLING_PARAMS_REJECTED = [
751
777
  'claude-fable-5', 'claude-opus-5', 'claude-opus-4-8', 'claude-opus-4-7', 'claude-sonnet-5',
752
778
  'kimi-k3', 'kimi-k2.7-code', 'kimi-for-coding', 'k3',
779
+ 'gemini-3.7-flash',
753
780
  ];
754
781
  export function modelRejectsSamplingParams(model) {
755
- return SAMPLING_PARAMS_REJECTED.some(id => model === id || model.startsWith(`${id}-`));
782
+ // Canonicalize both sides. OpenRouter routes these as `google/gemini-3.7-flash`
783
+ // and `anthropic/claude-opus-4.8`, which a raw comparison misses — so the model
784
+ // gets a temperature it rejects, on the one path where the id is namespaced.
785
+ const id = canonicalModelId(model);
786
+ return SAMPLING_PARAMS_REJECTED.some(entry => {
787
+ const canonical = canonicalModelId(entry);
788
+ return id === canonical || id.startsWith(`${canonical}-`);
789
+ });
756
790
  }
757
791
  /**
758
792
  * Returns the effective max output tokens for a provider, capped by the provider's limit.
@@ -807,8 +841,9 @@ export function modelSupportsReasoningEffort(providerId, model) {
807
841
  case 'z.ai-api':
808
842
  case 'z.ai-cn':
809
843
  case 'z.ai-cn-api':
810
- // GLM-5.2 exposes graded High/Max effort; Turbo is a plain toggle.
811
- return idMatches(id, 'glm-5-2');
844
+ // GLM-5.2 exposes graded High/Max effort; GLM-5.3 adds a distinct Low.
845
+ // Turbo is a plain toggle.
846
+ return idMatches(id, 'glm-5-2') || idMatches(id, 'glm-5-3');
812
847
  case 'kimi':
813
848
  return idMatches(id, 'k3');
814
849
  case 'kimi-api':
@@ -854,14 +889,25 @@ export function reasoningParamsFor(providerId, model, tier) {
854
889
  // none/low/medium/high/xhigh — no "max"; map our Max → xhigh (the ceiling).
855
890
  return { reasoning_effort: tier === 'max' ? 'xhigh' : tier };
856
891
  case 'google':
857
- // Gemini 3 (OpenAI-compat) accepts ONLY low/high — "medium" 400s.
858
- return { reasoning_effort: tier === 'low' ? 'low' : 'high' };
892
+ // Gemini's OpenAI-compat layer maps reasoning_effort onto thinking_level
893
+ // and documents low | medium | high. (Medium 400'd on Gemini 3 Preview,
894
+ // which is why this used to collapse it — that was a preview-era bug and
895
+ // is fixed.) 'max' has no Gemini equivalent, so it tops out at high.
896
+ // 'minimal' is deliberately not emitted: 3.7 Flash rejects it outright.
897
+ return { reasoning_effort: tier === 'max' ? 'high' : tier };
859
898
  case 'deepseek':
899
+ // Graded thinking depth: high (default) or max. Lower tiers collapse to high.
900
+ return { reasoning_effort: tier === 'max' ? 'max' : 'high' };
860
901
  case 'z.ai':
861
902
  case 'z.ai-api':
862
903
  case 'z.ai-cn':
863
904
  case 'z.ai-cn-api':
864
- // Graded thinking depth: high (default) or max. Lower tiers collapse to high.
905
+ // GLM-5.3 accepts low/high/max; GLM-5.2 grades only high|max, so lower
906
+ // tiers collapse to high there. Either way we always send an effort and
907
+ // never a disabled thinking block — GLM-5.3 rejects "disabled" outright.
908
+ if (idMatches(canonicalModelId(model), 'glm-5-3')) {
909
+ return { reasoning_effort: tier === 'low' ? 'low' : tier === 'max' ? 'max' : 'high' };
910
+ }
865
911
  return { reasoning_effort: tier === 'max' ? 'max' : 'high' };
866
912
  case 'kimi':
867
913
  case 'kimi-api':
@@ -896,14 +942,20 @@ export function availableReasoningTiers(providerId, model) {
896
942
  case 'openai':
897
943
  return ['auto', 'low', 'medium', 'high', 'max'];
898
944
  case 'google':
899
- // OpenAI-compat layer accepts only low/high — "medium" 400s.
900
- return ['auto', 'low', 'high'];
945
+ // low | medium | high, per Gemini's OpenAI-compat mapping table. Medium
946
+ // is 3.7 Flash's own default and the tier Google recommends for agentic
947
+ // coding, so collapsing it hid the setting most users want.
948
+ return ['auto', 'low', 'medium', 'high'];
901
949
  case 'deepseek':
950
+ return ['auto', 'high', 'max'];
902
951
  case 'z.ai':
903
952
  case 'z.ai-api':
904
953
  case 'z.ai-cn':
905
954
  case 'z.ai-cn-api':
906
- return ['auto', 'high', 'max'];
955
+ // GLM-5.3 distinguishes a Low tier; GLM-5.2 grades only high|max.
956
+ return idMatches(canonicalModelId(model), 'glm-5-3')
957
+ ? ['auto', 'low', 'high', 'max']
958
+ : ['auto', 'high', 'max'];
907
959
  case 'kimi':
908
960
  case 'kimi-api':
909
961
  case 'kimi-cn':
@@ -2636,8 +2636,14 @@ export class App {
2636
2636
  `runtime ${elapsed}`,
2637
2637
  `tokens ${formatTokenCount(totalTokens)}`,
2638
2638
  ];
2639
- if (typeof stats.estimatedCost === 'number' && stats.estimatedCost > 0) {
2640
- leftParts.push(`cost $${stats.estimatedCost < 0.01 ? stats.estimatedCost.toFixed(4) : stats.estimatedCost.toFixed(2)}`);
2639
+ // Only pay-per-use tokens carry a real price; flat-fee providers get the
2640
+ // short "in plan" wording so the segment can't crowd the right-edge hint.
2641
+ const billable = typeof stats.billableCost === 'number' ? stats.billableCost : (stats.estimatedCost ?? 0);
2642
+ if (billable > 0) {
2643
+ leftParts.push(`cost $${billable < 0.01 ? billable.toFixed(4) : billable.toFixed(2)}${stats.hasFlatFeeUsage ? ' + in plan' : ''}`);
2644
+ }
2645
+ else if (stats.hasFlatFeeUsage) {
2646
+ leftParts.push('cost in plan');
2641
2647
  }
2642
2648
  // Thinking-effort tier, same chip the compact fallback shows beside the
2643
2649
  // model. Only present when set + supported — see getStatus.
@@ -77,6 +77,8 @@ export interface StatsTotals {
77
77
  totalTokens: number;
78
78
  totalPromptTokens: number;
79
79
  totalCompletionTokens: number;
80
+ /** Raw sum across every entry. The report's total is derived from the
81
+ * breakdown instead, so flat-fee rows don't contribute dollars. */
80
82
  estimatedCost: number;
81
83
  }
82
84
  export interface StatsCache {
@@ -7,6 +7,7 @@
7
7
  * alongside `ctx.app.*` calls. Pulling them here gives them direct unit
8
8
  * coverage.
9
9
  */
10
+ import { isFlatFeeProvider } from '../../config/providers.js';
10
11
  /** Snippet window: chars of context before / after the match. */
11
12
  export const SEARCH_SNIPPET_BEFORE = 30;
12
13
  export const SEARCH_SNIPPET_AFTER = 50;
@@ -121,6 +122,10 @@ export function formatMemoryList(notes) {
121
122
  export function formatModelCost(provider, estimatedCost) {
122
123
  if (provider === 'ollama')
123
124
  return 'free';
125
+ // Flat-fee providers meter real tokens but charge nothing per token — the
126
+ // dollar figure our pricing table computes for them is invented.
127
+ if (isFlatFeeProvider(provider))
128
+ return 'included in plan';
124
129
  return estimatedCost > 0 ? `~$${estimatedCost.toFixed(4)}` : '(no pricing data)';
125
130
  }
126
131
  /**
@@ -143,11 +148,20 @@ export function formatStatsReport(args) {
143
148
  lines.push(`- **${b.model}** (${b.provider}): ${fmt(b.promptTokens)} in / ${fmt(b.completionTokens)} out — ${costStr}`);
144
149
  }
145
150
  lines.push('');
151
+ // The total prices only pay-per-use rows; flat-fee rows are called out
152
+ // rather than folded in (or quietly dropped) — see formatModelCost.
153
+ const planRows = breakdown.filter(b => isFlatFeeProvider(b.provider));
154
+ const billable = breakdown
155
+ .filter(b => !isFlatFeeProvider(b.provider))
156
+ .reduce((s, b) => s + b.estimatedCost, 0);
146
157
  if (currentProvider === 'ollama') {
147
158
  lines.push(`**Total: free · ${fmt(totals.totalTokens)} tokens**`);
148
159
  }
149
- else if (totals.estimatedCost > 0) {
150
- lines.push(`**Total: ~$${totals.estimatedCost.toFixed(4)}**`);
160
+ else if (planRows.length === breakdown.length) {
161
+ lines.push(`**Total: included in plan · ${fmt(totals.totalTokens)} tokens**`);
162
+ }
163
+ else if (billable > 0) {
164
+ lines.push(`**Total: ~$${billable.toFixed(4)}${planRows.length > 0 ? ' + usage included in plan' : ''}**`);
151
165
  }
152
166
  }
153
167
  if (cache.cacheReadTokens > 0 || cache.cacheCreationTokens > 0) {
@@ -21,6 +21,12 @@ export interface StatusInfo {
21
21
  completionTokens: number;
22
22
  requestCount: number;
23
23
  estimatedCost?: number;
24
+ /** `estimatedCost` minus flat-fee entries — the only spend we may show in
25
+ * dollars (see providers.flatFee). */
26
+ billableCost?: number;
27
+ /** At least one entry came from a flat-fee provider, so the footer says
28
+ * "in plan" instead of pricing those tokens. */
29
+ hasFlatFeeUsage?: boolean;
24
30
  };
25
31
  }
26
32
  /**
@@ -111,6 +111,8 @@ function getStatus() {
111
111
  completionTokens: stats.totalCompletionTokens,
112
112
  requestCount: stats.requestCount,
113
113
  estimatedCost: stats.estimatedCost,
114
+ billableCost: stats.billableCost,
115
+ hasFlatFeeUsage: stats.hasFlatFeeUsage,
114
116
  },
115
117
  };
116
118
  }
@@ -19,6 +19,12 @@ export interface SessionTokenStats {
19
19
  totalTokens: number;
20
20
  requestCount: number;
21
21
  estimatedCost: number;
22
+ /** `estimatedCost` minus every flat-fee entry — the only figure we may show
23
+ * as a dollar total, since flat-fee tokens carry no per-token charge. */
24
+ billableCost: number;
25
+ /** True when at least one entry came from a flat-fee provider, so callers can
26
+ * say "included in plan" instead of silently dropping that usage. */
27
+ hasFlatFeeUsage: boolean;
22
28
  /** Anthropic prompt caching: total tokens written to cache this session. */
23
29
  totalCacheCreationTokens: number;
24
30
  /** Anthropic prompt caching: total tokens read from cache this session. */
@@ -105,8 +111,15 @@ export declare function getCostBreakdown(startIndex?: number): ProviderCostBreak
105
111
  export interface CacheStats {
106
112
  cacheCreationTokens: number;
107
113
  cacheReadTokens: number;
108
- /** Sum of estimatedSavings across all Anthropic-priced records. */
114
+ /** Sum of estimatedSavings across pay-per-use records only. */
109
115
  estimatedSavingsUsd: number;
116
+ /** True when some cached tokens came from a flat-fee plan, whose "savings"
117
+ * are not a dollar amount at all. Lets the report say so instead of quoting
118
+ * a figure that silently covers only part of the session. */
119
+ hasFlatFeeCacheUsage: boolean;
120
+ /** True when EVERY cached token came from a flat-fee plan — there is no
121
+ * metered spend to have saved against. */
122
+ isEntirelyFlatFeeCache: boolean;
110
123
  }
111
124
  export declare function getCacheStats(): CacheStats;
112
125
  /**
@@ -2,12 +2,14 @@
2
2
  * Token and cost tracking for API usage
3
3
  */
4
4
  import { AsyncLocalStorage } from 'node:async_hooks';
5
+ import { isFlatFeeProvider } from '../config/providers.js';
5
6
  import { formatResourceImpactReport } from './resourceImpact.js';
6
7
  // Context window sizes per model (in tokens). Primarily mirrors providers.ts;
7
8
  // retired aliases remain only where restored historical sessions still need a
8
9
  // meaningful context/cost display.
9
10
  const MODEL_CONTEXT_WINDOWS = {
10
11
  // Z.AI / ZhipuAI
12
+ 'glm-5.3': 1_000_000,
11
13
  'glm-5.2': 1_000_000,
12
14
  'glm-5.1': 200_000,
13
15
  'glm-5-turbo': 202_752,
@@ -29,6 +31,7 @@ const MODEL_CONTEXT_WINDOWS = {
29
31
  'deepseek-v4-flash': 1_000_000,
30
32
  // Google
31
33
  'gemini-3.1-pro-preview': 1_048_576,
34
+ 'gemini-3.7-flash': 1_048_576,
32
35
  'gemini-3.6-flash': 1_048_576,
33
36
  'gemini-3.5-flash': 1_048_576,
34
37
  'gemini-3.5-flash-lite': 1_048_576,
@@ -45,6 +48,7 @@ const MODEL_CONTEXT_WINDOWS = {
45
48
  'k3': 1_000_000,
46
49
  'k3-256k': 262_144,
47
50
  // Grok (xAI)
51
+ 'grok-4.6': 500_000,
48
52
  'grok-4.5': 500_000,
49
53
  'grok-build-0.1': 256_000,
50
54
  'grok-4.3': 1_000_000,
@@ -72,6 +76,9 @@ export function getModelContextWindow(model) {
72
76
  const MODEL_PRICING = {
73
77
  // Z.AI / ZhipuAI
74
78
  // Coding Plan is flat-fee; these official rates apply to pay-per-use.
79
+ // `glm-5.3` is GLM Coding Plan only — Z.AI publishes no per-token rate for it
80
+ // (the standalone model API is still "coming soon"), so it stays unpriced
81
+ // rather than borrowing GLM-5.2's.
75
82
  'glm-5.2': { inputPer1M: 1.40, outputPer1M: 4.40 },
76
83
  'glm-5.1': { inputPer1M: 1.40, outputPer1M: 4.40 },
77
84
  'glm-5-turbo': { inputPer1M: 1.20, outputPer1M: 4.00 },
@@ -89,11 +96,20 @@ const MODEL_PRICING = {
89
96
  'claude-sonnet-5': { inputPer1M: 3.00, outputPer1M: 15.00 },
90
97
  'claude-haiku-4-5-20251001': { inputPer1M: 1.00, outputPer1M: 5.00 },
91
98
  // DeepSeek (cache-miss input pricing)
99
+ // DeepSeek moved to peak / off-peak billing on 2026-08-16, with off-peak at
100
+ // half these rates. This table holds one rate per model and has no notion of
101
+ // wall-clock time, so it keeps the PEAK figures: an over-estimate is the
102
+ // honest direction for a cost estimate, and rule 5 of the catalogue policy
103
+ // allows a clearly-labelled conservative approximation but never an invented
104
+ // number. Cache-miss input; cache hits are ~1/50th and not modelled here.
92
105
  'deepseek-v4-pro': { inputPer1M: 0.435, outputPer1M: 0.87 },
93
106
  'deepseek-v4-flash': { inputPer1M: 0.14, outputPer1M: 0.28 },
94
107
  // Google
108
+ // Gemini 3.6/3.7 Flash carry PROMOTIONAL rates that run through 2026-12-31 and
109
+ // step up to 1.50/7.50 on 2027-01-01 — revisit both rows on that date.
95
110
  'gemini-3.1-pro-preview': { inputPer1M: 2.00, outputPer1M: 12.00 },
96
- 'gemini-3.6-flash': { inputPer1M: 1.50, outputPer1M: 7.50 },
111
+ 'gemini-3.7-flash': { inputPer1M: 0.75, outputPer1M: 3.75 },
112
+ 'gemini-3.6-flash': { inputPer1M: 0.75, outputPer1M: 3.75 },
97
113
  'gemini-3.5-flash': { inputPer1M: 1.50, outputPer1M: 9.00 },
98
114
  'gemini-3.5-flash-lite': { inputPer1M: 0.30, outputPer1M: 2.50 },
99
115
  'gemini-3-flash-preview': { inputPer1M: 0.50, outputPer1M: 3.00 },
@@ -110,7 +126,9 @@ const MODEL_PRICING = {
110
126
  'kimi-for-coding-highspeed': { inputPer1M: 0.95, outputPer1M: 4.00 },
111
127
  'k3': { inputPer1M: 3.00, outputPer1M: 15.00 },
112
128
  'k3-256k': { inputPer1M: 3.00, outputPer1M: 15.00 },
113
- // Grok (xAI)
129
+ // Grok (xAI) — base-tier rates. xAI doubles Grok 4.5/4.6 at prompts ≥200K;
130
+ // this table stores one flat rate per model, so the base tier is what we show.
131
+ 'grok-4.6': { inputPer1M: 2.00, outputPer1M: 6.00 },
114
132
  'grok-4.5': { inputPer1M: 2.00, outputPer1M: 6.00 },
115
133
  'grok-build-0.1': { inputPer1M: 1.00, outputPer1M: 2.00 },
116
134
  'grok-4.3': { inputPer1M: 1.25, outputPer1M: 2.50 },
@@ -262,9 +280,20 @@ export function getCacheStats() {
262
280
  let cacheCreate = 0;
263
281
  let cacheRead = 0;
264
282
  let savings = 0;
283
+ let flatFeeCached = 0;
284
+ let meteredCached = 0;
265
285
  for (const record of currentRecords()) {
286
+ const cached = (record.cacheCreationTokens ?? 0) + (record.cacheReadTokens ?? 0);
266
287
  cacheCreate += record.cacheCreationTokens ?? 0;
267
288
  cacheRead += record.cacheReadTokens ?? 0;
289
+ // A plan bills a flat fee, so caching saves latency but not money — pricing
290
+ // its cached tokens would invent a dollar figure the same way the per-model
291
+ // cost lines used to. Count the tokens (measured), skip the arithmetic.
292
+ if (isFlatFeeProvider(record.provider)) {
293
+ flatFeeCached += cached;
294
+ continue;
295
+ }
296
+ meteredCached += cached;
268
297
  // Savings = what cache-read tokens would have cost at full input rate,
269
298
  // minus what they actually cost at 0.1×. (Cache creation is a slight
270
299
  // *penalty* of 0.25× — netted in for honest reporting.)
@@ -275,7 +304,13 @@ export function getCacheStats() {
275
304
  savings += cReadSaved - cCreateCost;
276
305
  }
277
306
  }
278
- return { cacheCreationTokens: cacheCreate, cacheReadTokens: cacheRead, estimatedSavingsUsd: Math.max(0, savings) };
307
+ return {
308
+ cacheCreationTokens: cacheCreate,
309
+ cacheReadTokens: cacheRead,
310
+ estimatedSavingsUsd: Math.max(0, savings),
311
+ hasFlatFeeCacheUsage: flatFeeCached > 0,
312
+ isEntirelyFlatFeeCache: flatFeeCached > 0 && meteredCached === 0,
313
+ };
279
314
  }
280
315
  /**
281
316
  * Get session stats
@@ -293,13 +328,20 @@ export function getSessionStats() {
293
328
  totalCacheCreationTokens += record.cacheCreationTokens ?? 0;
294
329
  totalCacheReadTokens += record.cacheReadTokens ?? 0;
295
330
  }
296
- const estimatedCost = getCostBreakdown().reduce((s, b) => s + b.estimatedCost, 0);
331
+ const breakdown = getCostBreakdown();
332
+ const estimatedCost = breakdown.reduce((s, b) => s + b.estimatedCost, 0);
333
+ const billableCost = breakdown
334
+ .filter(b => !isFlatFeeProvider(b.provider))
335
+ .reduce((s, b) => s + b.estimatedCost, 0);
336
+ const hasFlatFeeUsage = breakdown.some(b => isFlatFeeProvider(b.provider));
297
337
  return {
298
338
  totalPromptTokens,
299
339
  totalCompletionTokens,
300
340
  totalTokens,
301
341
  requestCount: currentRecords().length,
302
342
  estimatedCost,
343
+ billableCost,
344
+ hasFlatFeeUsage,
303
345
  totalCacheCreationTokens,
304
346
  totalCacheReadTokens,
305
347
  };
@@ -350,36 +392,54 @@ export function formatCostReport() {
350
392
  return '_No API requests in this session yet._';
351
393
  }
352
394
  const breakdown = getCostBreakdown();
395
+ // Flat-fee providers (subscriptions / free tiers) bill nothing per token, so
396
+ // their notional cost is never shown and never folded into the total. A
397
+ // session can mix them with pay-per-use models, so decide per entry.
398
+ const planEntries = breakdown.filter(b => isFlatFeeProvider(b.provider));
399
+ const costLine = planEntries.length === breakdown.length
400
+ ? '**Estimated cost:** included in plan'
401
+ : `**Estimated cost:** $${stats.billableCost.toFixed(4)}${planEntries.length > 0 ? ' + usage included in plan' : ''}`;
353
402
  const lines = [
354
403
  '## Session Cost',
355
404
  '',
356
405
  `**Requests:** ${stats.requestCount} · **Input:** ${formatTokenCount(stats.totalPromptTokens)} · **Output:** ${formatTokenCount(stats.totalCompletionTokens)} · **Total:** ${formatTokenCount(stats.totalTokens)}`,
357
- `**Estimated cost:** $${stats.estimatedCost.toFixed(4)}`,
406
+ costLine,
358
407
  '',
359
408
  ];
360
- if (breakdown.length > 1 || (breakdown.length === 1 && breakdown[0].estimatedCost > 0)) {
409
+ if (breakdown.length > 1 || (breakdown.length === 1 && (breakdown[0].estimatedCost > 0 || planEntries.length > 0))) {
361
410
  lines.push('| Provider / Model | Input | Output | Cost |');
362
411
  lines.push('|---|---:|---:|---:|');
363
412
  for (const b of breakdown) {
364
- lines.push(`| \`${b.provider}\` / \`${b.model}\` | ${formatTokenCount(b.promptTokens)} | ${formatTokenCount(b.completionTokens)} | $${b.estimatedCost.toFixed(4)} |`);
413
+ const cost = isFlatFeeProvider(b.provider) ? 'included in plan' : `$${b.estimatedCost.toFixed(4)}`;
414
+ lines.push(`| \`${b.provider}\` / \`${b.model}\` | ${formatTokenCount(b.promptTokens)} | ${formatTokenCount(b.completionTokens)} | ${cost} |`);
365
415
  }
366
416
  }
367
417
  // Prompt caching summary — only shown if at least one cached call landed.
368
418
  const cache = getCacheStats();
369
419
  if (cache.cacheReadTokens > 0 || cache.cacheCreationTokens > 0) {
370
420
  lines.push('', '### Prompt caching');
371
- lines.push(`**Cache reads:** ${formatTokenCount(cache.cacheReadTokens)} tokens (billed at 0.1× input rate)`);
421
+ // The billing multipliers only describe a metered account. On a plan
422
+ // nothing is billed per token, so quoting a rate there would be as invented
423
+ // as the per-model prices this report already refuses to show.
424
+ const readNote = cache.isEntirelyFlatFeeCache ? '' : ' (billed at 0.1× input rate)';
425
+ const writeNote = cache.isEntirelyFlatFeeCache ? '' : ' (billed at 1.25× input rate)';
426
+ lines.push(`**Cache reads:** ${formatTokenCount(cache.cacheReadTokens)} tokens${readNote}`);
372
427
  if (cache.cacheCreationTokens > 0) {
373
- lines.push(`**Cache writes:** ${formatTokenCount(cache.cacheCreationTokens)} tokens (billed at 1.25× input rate)`);
428
+ lines.push(`**Cache writes:** ${formatTokenCount(cache.cacheCreationTokens)} tokens${writeNote}`);
429
+ }
430
+ if (cache.isEntirelyFlatFeeCache) {
431
+ lines.push('**Savings:** caching saves latency, not money — this session is on a plan');
374
432
  }
375
- if (cache.estimatedSavingsUsd > 0) {
376
- lines.push(`**Estimated savings vs no caching:** $${cache.estimatedSavingsUsd.toFixed(4)}`);
433
+ else if (cache.estimatedSavingsUsd > 0) {
434
+ // Name the partial coverage rather than letting one figure look total.
435
+ const scope = cache.hasFlatFeeCacheUsage ? ' (pay-per-use models only)' : '';
436
+ lines.push(`**Estimated savings vs no caching:** $${cache.estimatedSavingsUsd.toFixed(4)}${scope}`);
377
437
  }
378
438
  }
379
439
  lines.push('', ...formatResourceImpactReport(stats.totalTokens));
380
440
  // Models with no pricing entry don't contribute to cost — flag so users
381
441
  // aren't surprised the total looks low.
382
- const untracked = breakdown.filter(b => b.estimatedCost === 0 && (b.promptTokens + b.completionTokens) > 0);
442
+ const untracked = breakdown.filter(b => b.estimatedCost === 0 && (b.promptTokens + b.completionTokens) > 0 && !isFlatFeeProvider(b.provider));
383
443
  if (untracked.length > 0) {
384
444
  lines.push('', `_Note: ${untracked.length} model${untracked.length === 1 ? '' : 's'} (${untracked.map(u => `\`${u.model}\``).join(', ')}) have no pricing entry — token counts are tracked but not priced._`);
385
445
  }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "2.17.0";
1
+ export declare const VERSION = "2.18.1";
package/dist/version.js CHANGED
@@ -1,4 +1,4 @@
1
1
  // AUTO-GENERATED by scripts/gen-version.js — do not edit by hand.
2
2
  // Baked from package.json at build time so the bun-compiled binary reports
3
3
  // the right version (it has no package.json on disk to read at runtime).
4
- export const VERSION = '2.17.0';
4
+ export const VERSION = '2.18.1';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeep",
3
- "version": "2.17.0",
3
+ "version": "2.18.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",
@@ -16,6 +16,7 @@
16
16
  "test:watch": "vitest",
17
17
  "test:coverage": "vitest run --coverage",
18
18
  "version": "node scripts/gen-version.js && git add src/version.ts",
19
+ "export:catalogue": "node --import tsx scripts/export-catalogue.ts",
19
20
  "release": "node scripts/release.js"
20
21
  },
21
22
  "repository": {