praxis-agent 0.62.0 → 0.62.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -98,6 +98,16 @@ cd /path/to/project
98
98
  praxis
99
99
  ```
100
100
 
101
+ Anthropic models use a 200,000-token context window by default, including
102
+ unknown model IDs. Add the exact terminal `[1m]` suffix (for example,
103
+ `claude-sonnet-4-20250514[1m]`) to request a 1,000,000-token context window;
104
+ Praxis keeps that selected model public, removes the suffix on the wire, and
105
+ adds the `context-1m-2025-08-07` Anthropic beta once. An explicit
106
+ `PRAXIS_CONTEXT_WINDOW_TOKENS` value overrides either inferred window.
107
+ Exact `claude-sonnet-4-6` and `claude-opus-4-6` selections also support
108
+ `--thinking adaptive`; use `--thinking enabled --max-thinking-tokens <n>`
109
+ when a fixed thinking-token budget is required.
110
+
101
111
  Common non-interactive operations:
102
112
 
103
113
  ```sh
@@ -159,7 +169,9 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
159
169
  resize-safe lifecycle, Ctrl-C restoration, fullscreen `Ctrl+L` redraw, and
160
170
  mouse-wheel/drag selection with edge autoscroll and OSC 52 copy,
161
171
  interactive `/doctor` diagnostics, per-session model/effort/permission controls,
162
- context/status/skill/task dashboards, prompt stash and continuation shortcuts,
172
+ context/status/skill/task dashboards whose context view previews the selected
173
+ provider/model capacity before the first turn and reports unavailable capacity
174
+ without fabricated percentages, prompt stash and continuation shortcuts,
163
175
  filterable `@` file and agent references, composer undo, `Ctrl+G` external
164
176
  editing, shared `/keybindings` creation/editing and supported-action remapping,
165
177
  shared built-in and custom `/theme` profiles with immediate
@@ -177,7 +189,7 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
177
189
  patches and readable binary/conflict/transient-path notes, semantic plan/question decision panels with complete
178
190
  screen-reader actions, semantic screen projection across selectable surfaces,
179
191
  deterministic resize-aware URL/form elicitation rendering, and measured
180
- context budgets; print mode,
192
+ context budgets with base64-payload-independent image estimates; print mode,
181
193
  structured JSON/JSONL, context compaction, tool loops, and bounded execution.
182
194
  - **Built-in tools** — read, write, edit, `ApplyPatch` for bounded ordered exact
183
195
  multi-file replacements, configured plugin LSP navigation with fresh bounded
@@ -294,7 +306,9 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
294
306
  route, whether primary or fallback, stays sticky only through that logical
295
307
  Turn's tool continuations; incompatible routes fail closed, and the next
296
308
  independent Turn starts from primary. Recovery may persist only an optional
297
- selected model, never provider route or wire state.
309
+ selected model, never provider route or wire state. Anthropic uses a
310
+ 200,000-token default or exact terminal `[1m]` model syntax for 1,000,000
311
+ tokens; `PRAXIS_CONTEXT_WINDOW_TOKENS` overrides the advertised window.
298
312
  - **Transactional self-update** — `praxis update` verifies the package before
299
313
  installing it, rejects concurrent updates, and can roll back after an
300
314
  interruption or crash.
@@ -85,6 +85,7 @@ interface InteractiveSessionCommands {
85
85
  }
86
86
  export interface InteractiveServiceFactory {
87
87
  scheduledPrompts?: boolean;
88
+ contextWindowTokens?(modelId?: string): number | undefined;
88
89
  createService(options: {
89
90
  eventSink: RuntimeEventSink;
90
91
  requireProvider: boolean;
@@ -471,7 +471,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
471
471
  const [, setTurnDuration] = useState();
472
472
  const [usage, setUsage] = useState();
473
473
  const [costUsd, setCostUsd] = useState();
474
- const [contextWindowTokens, setContextWindowTokens] = useState(display.contextWindowTokens);
474
+ const [contextWindowTokens, setContextWindowTokens] = useState(display.contextWindowTokens ?? factory.contextWindowTokens?.());
475
475
  const [historyState, setHistoryState] = useState(() => {
476
476
  const items = [...initialHistory];
477
477
  return {
@@ -2237,8 +2237,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
2237
2237
  setAvailableSlashCommands(created.slashCommands?.() ?? slashCommands);
2238
2238
  setAvailableAgents(created.agentDefinitions?.() ?? agents);
2239
2239
  const runtimeInfo = created.runtimeInfo?.();
2240
- if (runtimeInfo?.contextWindowTokens !== undefined)
2241
- setContextWindowTokens(runtimeInfo.contextWindowTokens);
2240
+ setContextWindowTokens(runtimeInfo?.contextWindowTokens);
2242
2241
  return created;
2243
2242
  }
2244
2243
  finally {
@@ -2314,6 +2313,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
2314
2313
  void loading.finally(() => onTurnChange?.(null));
2315
2314
  };
2316
2315
  const changeModel = (model) => {
2316
+ setContextWindowTokens(factory.contextWindowTokens?.(model));
2317
2317
  updateRuntimePreferences((current) => {
2318
2318
  if (model !== undefined)
2319
2319
  return { ...current, model };
@@ -6726,7 +6726,9 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
6726
6726
  const contextEntry = {
6727
6727
  kind: 'context',
6728
6728
  usedTokens: Math.max(measuredTokens, skillTokens),
6729
- contextWindowTokens: runtimeDisplay.contextWindowTokens ?? 200_000,
6729
+ ...(runtimeDisplay.contextWindowTokens === undefined
6730
+ ? {}
6731
+ : { contextWindowTokens: runtimeDisplay.contextWindowTokens }),
6730
6732
  model: runtimeDisplay.model ?? 'provider default',
6731
6733
  skills,
6732
6734
  memoryFiles: [],
@@ -390,6 +390,15 @@ function percent(tokens, total) {
390
390
  }
391
391
  function ContextUsageBlock({ usedTokens, contextWindowTokens, model, skills, memoryFiles, screenReader, }) {
392
392
  const theme = useTuiTheme();
393
+ const resourceTree = (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { ...theme.text.heading, bold: true, children: "Memory files \u00B7 /memory" }), memoryFiles.length === 0 ? (_jsx(Text, { dimColor: true, children: "\u2514 No memory files" })) : (memoryFiles.map((file, index) => (_jsxs(Text, { dimColor: true, children: [index === memoryFiles.length - 1 ? '└' : '├', " ", file.path, ":", ' ', file.tokens, " tokens"] }, file.path)))), _jsx(Text, { children: " " }), _jsx(Text, { ...theme.text.heading, bold: true, children: "Skills \u00B7 /skills" }), _jsx(Text, { children: " " }), _jsx(Text, { children: "Loaded" }), skills.length === 0 ? (_jsx(Text, { dimColor: true, children: "\u2514 No skills loaded" })) : (skills.map((skill, index) => (_jsxs(Text, { dimColor: true, children: [index === skills.length - 1 ? '└' : '├', " ", skill.name, ": ~", skill.tokens, " tokens"] }, skill.name))))] }));
394
+ if (screenReader && contextWindowTokens === undefined) {
395
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { ...theme.text.heading, children: "Context Usage" }), _jsxs(Text, { children: [model ?? 'provider default', " \u00B7 ", usedTokens.toLocaleString(), " tokens \u00B7 Context capacity unavailable"] }), _jsxs(Text, { children: ["Memory files:", ' ', memoryFiles
396
+ .map((file) => `${file.path} (${file.tokens} tokens)`)
397
+ .join(', ') || 'none'] }), _jsxs(Text, { children: ["Skills: ", skills.map(({ name }) => name).join(', ') || 'none'] })] }));
398
+ }
399
+ if (contextWindowTokens === undefined) {
400
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, children: [_jsx(Text, { ...theme.text.heading, bold: true, children: "Context Usage" }), _jsxs(Text, { dimColor: true, children: [model ?? 'provider default', " \u00B7 ", usedTokens.toLocaleString(), " tokens \u00B7 Context capacity unavailable"] }), resourceTree] }));
401
+ }
393
402
  const totalTokens = Math.max(1, contextWindowTokens);
394
403
  const compactBuffer = Math.round(totalTokens * 0.165);
395
404
  const usable = Math.max(1, totalTokens - compactBuffer);
@@ -399,7 +408,7 @@ function ContextUsageBlock({ usedTokens, contextWindowTokens, model, skills, mem
399
408
  const usedCells = Math.min(25, Math.round((usedTokens / totalTokens) * 25));
400
409
  const bufferCells = Math.min(25 - usedCells, Math.round((compactBuffer / totalTokens) * 25));
401
410
  const cells = Array.from({ length: 25 }, (_, index) => index < usedCells ? '⛁' : index >= 25 - bufferCells ? '⛝' : '⛶');
402
- return (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, children: [_jsx(Text, { ...theme.text.heading, bold: true, children: "Context Usage" }), _jsxs(Box, { children: [_jsx(Box, { flexDirection: "column", marginRight: 2, children: Array.from({ length: 5 }, (_, row) => (_jsx(Text, { children: cells.slice(row * 5, row * 5 + 5).join(' ') }, row))) }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: [model ?? 'provider default', " \u00B7 ", compactTokens(usedTokens), "/", compactTokens(totalTokens), " tokens (", percent(usedTokens, totalTokens), ")"] }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, italic: true, children: "Estimated usage by category" }), _jsxs(Text, { children: ["\u26C1 Messages and other context: ", compactTokens(usedTokens), " tokens (", percent(usedTokens, totalTokens), ")"] }), _jsxs(Text, { children: ["\u26F6 Free space: ", compactTokens(Math.max(0, usable - usedTokens)), " (", percent(Math.max(0, usable - usedTokens), totalTokens), ")"] }), _jsxs(Text, { children: ["\u26DD Autocompact buffer: ", compactTokens(compactBuffer), " tokens (", percent(compactBuffer, totalTokens), ")"] })] })] }), _jsx(Text, { children: " " }), _jsx(Text, { ...theme.text.heading, bold: true, children: "Memory files \u00B7 /memory" }), memoryFiles.length === 0 ? (_jsx(Text, { dimColor: true, children: "\u2514 No memory files" })) : (memoryFiles.map((file, index) => (_jsxs(Text, { dimColor: true, children: [index === memoryFiles.length - 1 ? '└' : '├', " ", file.path, ":", ' ', file.tokens, " tokens"] }, file.path)))), _jsx(Text, { children: " " }), _jsx(Text, { ...theme.text.heading, bold: true, children: "Skills \u00B7 /skills" }), _jsx(Text, { children: " " }), _jsx(Text, { children: "Loaded" }), skills.length === 0 ? (_jsx(Text, { dimColor: true, children: "\u2514 No skills loaded" })) : (skills.map((skill, index) => (_jsxs(Text, { dimColor: true, children: [index === skills.length - 1 ? '└' : '├', " ", skill.name, ": ~", skill.tokens, " tokens"] }, skill.name))))] }));
411
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, children: [_jsx(Text, { ...theme.text.heading, bold: true, children: "Context Usage" }), _jsxs(Box, { children: [_jsx(Box, { flexDirection: "column", marginRight: 2, children: Array.from({ length: 5 }, (_, row) => (_jsx(Text, { children: cells.slice(row * 5, row * 5 + 5).join(' ') }, row))) }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: [model ?? 'provider default', " \u00B7 ", compactTokens(usedTokens), "/", compactTokens(totalTokens), " tokens (", percent(usedTokens, totalTokens), ")"] }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, italic: true, children: "Estimated usage by category" }), _jsxs(Text, { children: ["\u26C1 Messages and other context: ", compactTokens(usedTokens), " tokens (", percent(usedTokens, totalTokens), ")"] }), _jsxs(Text, { children: ["\u26F6 Free space: ", compactTokens(Math.max(0, usable - usedTokens)), " (", percent(Math.max(0, usable - usedTokens), totalTokens), ")"] }), _jsxs(Text, { children: ["\u26DD Autocompact buffer: ", compactTokens(compactBuffer), " tokens (", percent(compactBuffer, totalTokens), ")"] })] })] }), resourceTree] }));
403
412
  }
404
413
  const MARKDOWN_TEXT_CACHE_MAX = 2048;
405
414
  const markdownTextCache = new Map();
@@ -32,7 +32,9 @@ export function conversationExportText(_display, items) {
32
32
  output.push(...lines(item.text, ' ⎿ '));
33
33
  }
34
34
  else if (item.kind === 'context') {
35
- output.push('', `Context Usage: ${item.usedTokens}/${item.contextWindowTokens} tokens`);
35
+ output.push('', item.contextWindowTokens === undefined
36
+ ? `Context Usage: ${item.usedTokens} tokens · Context capacity unavailable`
37
+ : `Context Usage: ${item.usedTokens}/${item.contextWindowTokens} tokens`);
36
38
  }
37
39
  else {
38
40
  output.push(...lines(item.text, item.kind === 'warning' ? '⚠ ' : '· '));
@@ -22,7 +22,7 @@ export type TranscriptItem = {
22
22
  } | {
23
23
  kind: 'context';
24
24
  usedTokens: number;
25
- contextWindowTokens: number;
25
+ contextWindowTokens?: number;
26
26
  model?: string;
27
27
  skills: readonly {
28
28
  name: string;
@@ -262,6 +262,21 @@ function percent(tokens, total) {
262
262
  return `${Math.round((tokens / Math.max(1, total)) * 100 * 10) / 10}%`;
263
263
  }
264
264
  function contextUsageLineCount(item, width) {
265
+ if (item.contextWindowTokens === undefined) {
266
+ const resourceWidth = Math.max(1, width - 2);
267
+ const memoryRows = item.memoryFiles.length
268
+ ? item.memoryFiles.reduce((rows, file, index) => rows +
269
+ wrappedLineCount(`${index === item.memoryFiles.length - 1 ? '└' : '├'} ${file.path}: ${file.tokens} tokens`, resourceWidth), 0)
270
+ : wrappedLineCount('└ No memory files', resourceWidth);
271
+ const skillRows = item.skills.length
272
+ ? item.skills.reduce((rows, skill, index) => rows +
273
+ wrappedLineCount(`${index === item.skills.length - 1 ? '└' : '├'} ${skill.name}: ~${skill.tokens} tokens`, resourceWidth), 0)
274
+ : wrappedLineCount('└ No skills loaded', resourceWidth);
275
+ return (8 +
276
+ wrappedLineCount(`${item.model ?? 'provider default'} · ${item.usedTokens.toLocaleString()} tokens · Context capacity unavailable`, resourceWidth) +
277
+ memoryRows +
278
+ skillRows);
279
+ }
265
280
  const totalTokens = Math.max(1, item.contextWindowTokens);
266
281
  const compactBuffer = Math.round(totalTokens * 0.165);
267
282
  const usable = Math.max(1, totalTokens - compactBuffer);
@@ -367,6 +382,14 @@ function visibleOutputLineCount(text, width, mode) {
367
382
  : 0));
368
383
  }
369
384
  function screenReaderContextLineCount(item, width) {
385
+ if (item.contextWindowTokens === undefined) {
386
+ return [
387
+ 'Context Usage',
388
+ `${item.model ?? 'provider default'} · ${item.usedTokens.toLocaleString()} tokens · Context capacity unavailable`,
389
+ `Memory files: ${item.memoryFiles.map((file) => `${file.path} (${file.tokens} tokens)`).join(', ') || 'none'}`,
390
+ `Skills: ${item.skills.map(({ name }) => name).join(', ') || 'none'}`,
391
+ ].reduce((rows, line) => rows + wrappedLineCount(line, width), 0);
392
+ }
370
393
  const totalTokens = Math.max(1, item.contextWindowTokens);
371
394
  const compactBuffer = Math.round(totalTokens * 0.165);
372
395
  return [
@@ -38,7 +38,9 @@ function entryText(entry) {
38
38
  };
39
39
  if (item.kind === 'context')
40
40
  return {
41
- text: `Context Usage · ${item.usedTokens}/${item.contextWindowTokens} tokens`,
41
+ text: item.contextWindowTokens === undefined
42
+ ? `Context Usage · ${item.usedTokens} tokens · Context capacity unavailable`
43
+ : `Context Usage · ${item.usedTokens}/${item.contextWindowTokens} tokens`,
42
44
  role: 'heading',
43
45
  };
44
46
  }
@@ -232,6 +232,7 @@ export declare function resolveInteractiveProviderStartup(options: {
232
232
  }): Promise<{
233
233
  effectiveModel: string | undefined;
234
234
  trustProjectRequestAvailable: boolean;
235
+ contextWindowTokensForModel: (modelId?: string) => number | undefined;
235
236
  }>;
236
237
  export declare function createDefaultDependencies(entrypoint?: string): CliDependencies;
237
238
  export declare function createBackgroundWorkerRuntime(workerSink: RuntimeEventSink, dispatch: {
@@ -51,7 +51,7 @@ import { FallbackModelProvider } from './providers/fallback-provider.js';
51
51
  import { ProviderCredentialVault } from './persistence/provider-credential-vault.js';
52
52
  import { parseContextEnvironment, parseProviderEnvironment, } from './providers/environment.js';
53
53
  import { ProviderAuthenticationError, resolveProviderCredential, } from './providers/provider-auth.js';
54
- import { resolveProviderRegistry } from './providers/provider-registry.js';
54
+ import { resolveProviderContextWindowTokens, resolveProviderRegistry, } from './providers/provider-registry.js';
55
55
  import { ProviderSettingsError, resolveProviderTarget, } from './providers/provider-settings.js';
56
56
  import { ModelPricingRegistry, usageCostUsd } from './core/usage.js';
57
57
  import { LocalToolRegistry } from './tools/local-tools.js';
@@ -2482,8 +2482,9 @@ export async function resolveInteractiveProviderStartup(options) {
2482
2482
  truthyEnvironmentValue(environment.CLAUDE_CODE_SIMPLE);
2483
2483
  if (disabled) {
2484
2484
  let effectiveModel;
2485
+ let protocol;
2485
2486
  try {
2486
- effectiveModel = (await resolveProviderTarget({
2487
+ const target = await resolveProviderTarget({
2487
2488
  configRoot: options.configRoot,
2488
2489
  cwd: options.cwd,
2489
2490
  environment,
@@ -2497,16 +2498,28 @@ export async function resolveInteractiveProviderStartup(options) {
2497
2498
  ? {}
2498
2499
  : { profile: options.controls.providerProfile }),
2499
2500
  includeSettings: false,
2500
- })).modelId;
2501
+ });
2502
+ effectiveModel = target.modelId;
2503
+ protocol = target.protocol;
2501
2504
  }
2502
2505
  catch (error) {
2503
2506
  if (!(error instanceof ProviderSettingsError &&
2504
2507
  error.code === 'model_required'))
2505
2508
  throw error;
2506
2509
  }
2510
+ const context = parseContextEnvironment(environment);
2507
2511
  return {
2508
2512
  effectiveModel,
2509
2513
  trustProjectRequestAvailable: options.controls.trustProject,
2514
+ contextWindowTokensForModel: (modelId) => protocol === undefined
2515
+ ? undefined
2516
+ : resolveProviderContextWindowTokens({
2517
+ protocol,
2518
+ modelId: modelId ?? effectiveModel ?? '',
2519
+ ...(context.contextWindowTokens === undefined
2520
+ ? {}
2521
+ : { explicitContextWindowTokens: context.contextWindowTokens }),
2522
+ }),
2510
2523
  };
2511
2524
  }
2512
2525
  const assessment = await assessCurrentWorkspaceProviderSelection({
@@ -2531,8 +2544,9 @@ export async function resolveInteractiveProviderStartup(options) {
2531
2544
  }
2532
2545
  }
2533
2546
  let effectiveModel;
2547
+ let protocol;
2534
2548
  try {
2535
- effectiveModel = (await resolveProviderTarget({
2549
+ const target = await resolveProviderTarget({
2536
2550
  configRoot: options.configRoot,
2537
2551
  cwd: options.cwd,
2538
2552
  environment,
@@ -2547,13 +2561,28 @@ export async function resolveInteractiveProviderStartup(options) {
2547
2561
  : { profile: options.controls.providerProfile }),
2548
2562
  includeSettings: true,
2549
2563
  includeProjectSettings,
2550
- })).modelId;
2564
+ });
2565
+ effectiveModel = target.modelId;
2566
+ protocol = target.protocol;
2551
2567
  }
2552
2568
  catch (error) {
2553
2569
  if (!(error instanceof ProviderSettingsError && error.code === 'model_required'))
2554
2570
  throw error;
2555
2571
  }
2556
- return { effectiveModel, trustProjectRequestAvailable };
2572
+ const context = parseContextEnvironment(environment);
2573
+ return {
2574
+ effectiveModel,
2575
+ trustProjectRequestAvailable,
2576
+ contextWindowTokensForModel: (modelId) => protocol === undefined
2577
+ ? undefined
2578
+ : resolveProviderContextWindowTokens({
2579
+ protocol,
2580
+ modelId: modelId ?? effectiveModel ?? '',
2581
+ ...(context.contextWindowTokens === undefined
2582
+ ? {}
2583
+ : { explicitContextWindowTokens: context.contextWindowTokens }),
2584
+ }),
2585
+ };
2557
2586
  }
2558
2587
  export function createDefaultDependencies(entrypoint = fileURLToPath(import.meta.url)) {
2559
2588
  const dependencies = {
@@ -2629,6 +2658,7 @@ export function createDefaultDependencies(entrypoint = fileURLToPath(import.meta
2629
2658
  statePath: interactiveStatePath,
2630
2659
  factory: {
2631
2660
  createService: createInteractiveService,
2661
+ contextWindowTokens: startup.contextWindowTokensForModel,
2632
2662
  scheduledPrompts: Boolean(effectiveModel),
2633
2663
  },
2634
2664
  ...(signal ? { signal } : {}),
@@ -24,6 +24,12 @@ export function estimateTextTokens(value) {
24
24
  }
25
25
  return Math.ceil(ascii / 4) + nonAscii;
26
26
  }
27
+ const IMAGE_VISUAL_TOKEN_ESTIMATE = 1_600;
28
+ // This provider-neutral conservative fallback never interprets billed usage;
29
+ // observed provider usage remains authoritative at a ContextBudget watermark.
30
+ function estimateImageTokens(mediaType) {
31
+ return 8 + estimateTextTokens(mediaType) + IMAGE_VISUAL_TOKEN_ESTIMATE;
32
+ }
27
33
  function estimateMessageTokens(message) {
28
34
  let tokens = 4 + estimateTextTokens(message.role);
29
35
  if (message.role === 'tool') {
@@ -31,16 +37,14 @@ function estimateMessageTokens(message) {
31
37
  estimateTextTokens(message.toolCallId) +
32
38
  estimateTextTokens(message.content);
33
39
  for (const image of message.images ?? []) {
34
- tokens +=
35
- 8 + estimateTextTokens(image.mediaType) + estimateTextTokens(image.data);
40
+ tokens += estimateImageTokens(image.mediaType);
36
41
  }
37
42
  return tokens + (message.isError ? 1 : 0);
38
43
  }
39
44
  tokens += estimateTextTokens(message.content);
40
45
  if (message.role === 'user') {
41
46
  for (const image of message.images ?? []) {
42
- tokens +=
43
- 8 + estimateTextTokens(image.mediaType) + estimateTextTokens(image.data);
47
+ tokens += estimateImageTokens(image.mediaType);
44
48
  }
45
49
  for (const document of message.documents ?? []) {
46
50
  tokens += 8 + estimateTextTokens(document.mediaType) + 2000;
@@ -4,6 +4,10 @@ export interface AnthropicCompatibleProviderOptions {
4
4
  baseUrl: string;
5
5
  apiKey: string;
6
6
  model: string;
7
+ promptCacheResolver?: (target: {
8
+ baseUrl: string;
9
+ model: string;
10
+ }) => AnthropicPromptCachePolicy;
7
11
  maxOutputTokens?: number;
8
12
  anthropicVersion?: string;
9
13
  webSearch?: boolean;
@@ -32,6 +36,9 @@ export declare class AnthropicCompatibleProvider implements ModelProvider {
32
36
  private readonly maxToolMetadataBytes;
33
37
  private readonly maxErrorBodyBytes;
34
38
  private readonly thinking;
39
+ private readonly wireModel;
40
+ private readonly supportsAdaptiveThinking;
41
+ private readonly providerBetas;
35
42
  private readonly promptCaching;
36
43
  private readonly streaming;
37
44
  constructor(options: AnthropicCompatibleProviderOptions);
@@ -3,6 +3,7 @@ import { transportFailureKind } from './provider-errors.js';
3
3
  import { reportProviderTransportActivity } from './provider-transport-activity.js';
4
4
  import { markNonStreamingFallbackEligible } from './non-streaming-fallback-provider.js';
5
5
  import { createAnthropicPromptCachePolicyResolver, } from './anthropic-prompt-cache.js';
6
+ import { resolveAnthropicModelSpec } from './anthropic-model-spec.js';
6
7
  function isRecord(value) {
7
8
  return typeof value === 'object' && value !== null && !Array.isArray(value);
8
9
  }
@@ -49,17 +50,23 @@ function positiveInteger(value, label) {
49
50
  }
50
51
  return value;
51
52
  }
52
- function validateThinking(thinking) {
53
+ function validateThinking(thinking, supportsAdaptiveThinking) {
53
54
  if (!thinking)
54
55
  return undefined;
55
56
  if (!['enabled', 'adaptive', 'disabled'].includes(thinking.mode)) {
56
57
  throw new Error(`Unsupported thinking mode: ${thinking.mode}`);
57
58
  }
59
+ if (thinking.mode === 'adaptive' && !supportsAdaptiveThinking) {
60
+ throw new Error('Adaptive thinking is only supported for explicit Claude Sonnet 4.6 or Opus 4.6 models');
61
+ }
58
62
  if (thinking.maxTokens !== undefined) {
59
63
  positiveInteger(thinking.maxTokens, 'Max thinking tokens');
60
64
  if (thinking.mode === 'disabled') {
61
65
  throw new Error('Max thinking tokens cannot be used when thinking is disabled');
62
66
  }
67
+ if (thinking.mode === 'adaptive') {
68
+ throw new Error('Max thinking tokens cannot be used with adaptive thinking; use enabled thinking for a fixed budget');
69
+ }
63
70
  }
64
71
  return thinking;
65
72
  }
@@ -643,21 +650,26 @@ export class AnthropicCompatibleProvider {
643
650
  maxToolMetadataBytes;
644
651
  maxErrorBodyBytes;
645
652
  thinking;
653
+ wireModel;
654
+ supportsAdaptiveThinking;
655
+ providerBetas;
646
656
  promptCaching;
647
657
  streaming;
648
658
  constructor(options) {
649
659
  this.options = options;
650
- if (options.contextWindowTokens !== undefined) {
651
- positiveInteger(options.contextWindowTokens, 'Context window tokens');
652
- }
660
+ const modelSpec = resolveAnthropicModelSpec(options.model, options.contextWindowTokens);
661
+ positiveInteger(modelSpec.contextWindowTokens, 'Context window tokens');
653
662
  this.endpoint = `${options.baseUrl.replace(/\/+$/, '')}/messages`;
654
- this.model = options.model;
663
+ this.model = modelSpec.model;
664
+ this.wireModel = modelSpec.wireModel;
665
+ this.supportsAdaptiveThinking = modelSpec.supportsAdaptiveThinking;
666
+ this.providerBetas = Object.freeze([...modelSpec.betas]);
655
667
  this.streaming = options.streaming ?? true;
656
668
  this.fetchImplementation = options.fetchImplementation ?? fetch;
657
669
  this.maxOutputTokens = positiveInteger(options.maxOutputTokens ??
658
- (options.model.includes('claude-opus-4-6')
670
+ (this.wireModel.includes('claude-opus-4-6')
659
671
  ? 64_000
660
- : options.model.startsWith('claude-')
672
+ : this.wireModel.startsWith('claude-')
661
673
  ? 32_000
662
674
  : 8192), 'Max output tokens');
663
675
  this.capabilities = {
@@ -668,22 +680,25 @@ export class AnthropicCompatibleProvider {
668
680
  documents: true,
669
681
  webSearch: options.webSearch === true,
670
682
  thinking: {
671
- modes: ['enabled', 'adaptive', 'disabled'],
683
+ modes: modelSpec.supportsAdaptiveThinking
684
+ ? ['enabled', 'adaptive', 'disabled']
685
+ : ['enabled', 'disabled'],
672
686
  maxTokens: true,
673
687
  },
674
- ...(options.contextWindowTokens === undefined
675
- ? {}
676
- : { contextWindowTokens: options.contextWindowTokens }),
688
+ contextWindowTokens: modelSpec.contextWindowTokens,
677
689
  maxOutputTokens: this.maxOutputTokens,
678
690
  terminalReasons: true,
679
691
  };
680
- this.thinking = validateThinking(options.thinking);
681
- const promptCaching = options.promptCaching === false
682
- ? undefined
683
- : (options.promptCaching ??
692
+ this.thinking = validateThinking(options.thinking, this.supportsAdaptiveThinking);
693
+ const promptCaching = options.promptCaching !== undefined
694
+ ? options.promptCaching
695
+ : (options.promptCacheResolver?.({
696
+ baseUrl: options.baseUrl,
697
+ model: this.wireModel,
698
+ }) ??
684
699
  createAnthropicPromptCachePolicyResolver({}, 'native')({
685
700
  baseUrl: options.baseUrl,
686
- model: options.model,
701
+ model: this.wireModel,
687
702
  }));
688
703
  this.promptCaching = promptCaching
689
704
  ? cacheControl(promptCaching.ttl)
@@ -703,17 +718,20 @@ export class AnthropicCompatibleProvider {
703
718
  if (request.webSearch && request.tools?.length) {
704
719
  throw new Error('Web search cannot be combined with model tools');
705
720
  }
706
- const thinking = validateThinking(request.thinking ?? this.thinking);
721
+ const thinking = validateThinking(request.thinking ?? this.thinking, this.supportsAdaptiveThinking);
707
722
  const maxTokens = Math.max(this.maxOutputTokens, thinking?.maxTokens === undefined ? 0 : thinking.maxTokens + 1);
708
723
  const thinkingPayload = thinking === undefined
709
724
  ? undefined
710
725
  : thinking.mode === 'disabled'
711
726
  ? { type: 'disabled' }
712
- : {
713
- type: 'enabled',
714
- budget_tokens: thinking.maxTokens ?? maxTokens - 1,
715
- };
727
+ : thinking.mode === 'adaptive'
728
+ ? { type: 'adaptive' }
729
+ : {
730
+ type: 'enabled',
731
+ budget_tokens: thinking.maxTokens ?? maxTokens - 1,
732
+ };
716
733
  const betas = [
734
+ ...this.providerBetas,
717
735
  ...(request.betas ?? []),
718
736
  ...(thinking && thinking.mode !== 'disabled'
719
737
  ? ['interleaved-thinking-2025-05-14']
@@ -730,7 +748,7 @@ export class AnthropicCompatibleProvider {
730
748
  ...(betas.length ? { 'anthropic-beta': betas.join(',') } : {}),
731
749
  },
732
750
  body: JSON.stringify({
733
- model: this.options.model,
751
+ model: this.wireModel,
734
752
  max_tokens: maxTokens,
735
753
  messages: serialized.messages,
736
754
  stream: this.streaming,
@@ -0,0 +1,10 @@
1
+ export declare const ANTHROPIC_LONG_CONTEXT_BETA = "context-1m-2025-08-07";
2
+ export interface ResolvedAnthropicModelSpec {
3
+ readonly model: string;
4
+ readonly wireModel: string;
5
+ readonly contextWindowTokens: number;
6
+ readonly betas: readonly string[];
7
+ readonly supportsAdaptiveThinking: boolean;
8
+ }
9
+ export declare function resolveAnthropicModelSpec(model: string, explicitContextWindowTokens?: number): ResolvedAnthropicModelSpec;
10
+ //# sourceMappingURL=anthropic-model-spec.d.ts.map
@@ -0,0 +1,16 @@
1
+ export const ANTHROPIC_LONG_CONTEXT_BETA = 'context-1m-2025-08-07';
2
+ export function resolveAnthropicModelSpec(model, explicitContextWindowTokens) {
3
+ const longContext = model.endsWith('[1m]');
4
+ const wireModel = longContext ? model.slice(0, -'[1m]'.length) : model;
5
+ if (longContext && wireModel.trim().length === 0) {
6
+ throw new Error('Anthropic [1m] model spec must include a base model name');
7
+ }
8
+ return Object.freeze({
9
+ model,
10
+ wireModel,
11
+ contextWindowTokens: explicitContextWindowTokens ?? (longContext ? 1_000_000 : 200_000),
12
+ betas: Object.freeze(longContext ? [ANTHROPIC_LONG_CONTEXT_BETA] : []),
13
+ supportsAdaptiveThinking: wireModel === 'claude-sonnet-4-6' || wireModel === 'claude-opus-4-6',
14
+ });
15
+ }
16
+ //# sourceMappingURL=anthropic-model-spec.js.map
@@ -1,6 +1,6 @@
1
1
  import type { ModelProvider, ModelThinkingConfig } from '../core/runtime.js';
2
2
  import { type CodexOAuthVault } from './codex-oauth.js';
3
- import { type ProviderTarget } from './provider-settings.js';
3
+ import { type ProviderProtocol, type ProviderTarget } from './provider-settings.js';
4
4
  import type { ProviderCredentialSourceMetadata, ProviderCredentialReader, ResolvedProviderCredential } from './provider-auth.js';
5
5
  import { parseProviderEnvironment, type ContextEnvironment } from './environment.js';
6
6
  import { type AnthropicPromptCachePolicy } from './anthropic-prompt-cache.js';
@@ -47,6 +47,11 @@ export interface ProviderRegistry {
47
47
  readonly credentialSource: ProviderRegistrySourceMetadata;
48
48
  create(modelId?: string): ModelProvider;
49
49
  }
50
+ export declare function resolveProviderContextWindowTokens(options: {
51
+ protocol: ProviderProtocol;
52
+ modelId: string;
53
+ explicitContextWindowTokens?: number;
54
+ }): number | undefined;
50
55
  export declare function resolveProviderRegistry(options: ResolveProviderRegistryOptions): Promise<ProviderRegistry>;
51
56
  export declare function createProviderRegistry(options: ProviderRegistryOptions): ProviderRegistry;
52
57
  //# sourceMappingURL=provider-registry.d.ts.map
@@ -6,6 +6,7 @@ import { DeadlineModelProvider } from './deadline-provider.js';
6
6
  import { NonStreamingFallbackModelProvider } from './non-streaming-fallback-provider.js';
7
7
  import { CodexOAuthCredentialManager, } from './codex-oauth.js';
8
8
  import { resolveProviderTarget, } from './provider-settings.js';
9
+ import { resolveAnthropicModelSpec } from './anthropic-model-spec.js';
9
10
  import { ProviderAuthenticationError, resolveProviderCredential, } from './provider-auth.js';
10
11
  import { parseContextEnvironment, parseProviderEnvironment, } from './environment.js';
11
12
  import { createAnthropicPromptCachePolicyResolver, } from './anthropic-prompt-cache.js';
@@ -17,6 +18,14 @@ export class ProviderRegistryError extends Error {
17
18
  this.code = code;
18
19
  }
19
20
  }
21
+ export function resolveProviderContextWindowTokens(options) {
22
+ if (options.protocol === 'anthropic-messages')
23
+ return resolveAnthropicModelSpec(options.modelId, options.explicitContextWindowTokens).contextWindowTokens;
24
+ if (options.protocol === 'openai-compatible' ||
25
+ options.protocol === 'openai-responses')
26
+ return options.explicitContextWindowTokens;
27
+ return undefined;
28
+ }
20
29
  export async function resolveProviderRegistry(options) {
21
30
  const environment = options.environment ?? process.env;
22
31
  const target = await resolveProviderTarget({
@@ -175,10 +184,7 @@ class NativeProviderRegistry {
175
184
  ...(this.options.anthropicPromptCacheResolver === undefined
176
185
  ? {}
177
186
  : {
178
- promptCaching: this.options.anthropicPromptCacheResolver({
179
- baseUrl: target.baseUrl,
180
- model: target.modelId,
181
- }),
187
+ promptCacheResolver: this.options.anthropicPromptCacheResolver,
182
188
  }),
183
189
  ...(this.options.providerEnvironment?.maxOutputTokens === undefined
184
190
  ? {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "praxis-agent",
3
- "version": "0.62.0",
3
+ "version": "0.62.2",
4
4
  "description": "Local-first, single-user general agent for the command line.",
5
5
  "license": "MIT",
6
6
  "author": "wuqisen",