@oh-my-pi/pi-coding-agent 13.9.1 → 13.9.3

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.
Files changed (53) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/examples/sdk/02-custom-model.ts +2 -1
  3. package/package.json +7 -7
  4. package/src/cli/args.ts +6 -5
  5. package/src/cli/list-models.ts +2 -2
  6. package/src/commands/launch.ts +3 -3
  7. package/src/config/model-registry.ts +85 -39
  8. package/src/config/model-resolver.ts +47 -21
  9. package/src/config/settings-schema.ts +56 -2
  10. package/src/discovery/helpers.ts +2 -2
  11. package/src/export/html/template.generated.ts +1 -1
  12. package/src/export/html/template.js +37 -15
  13. package/src/extensibility/custom-tools/types.ts +2 -0
  14. package/src/extensibility/extensions/loader.ts +3 -2
  15. package/src/extensibility/extensions/types.ts +10 -7
  16. package/src/extensibility/hooks/types.ts +2 -0
  17. package/src/main.ts +5 -22
  18. package/src/memories/index.ts +7 -3
  19. package/src/modes/components/footer.ts +10 -8
  20. package/src/modes/components/model-selector.ts +33 -38
  21. package/src/modes/components/settings-defs.ts +31 -2
  22. package/src/modes/components/settings-selector.ts +16 -5
  23. package/src/modes/components/status-line/context-thresholds.ts +68 -0
  24. package/src/modes/components/status-line/segments.ts +11 -12
  25. package/src/modes/components/thinking-selector.ts +7 -7
  26. package/src/modes/components/tree-selector.ts +3 -2
  27. package/src/modes/controllers/command-controller.ts +11 -26
  28. package/src/modes/controllers/event-controller.ts +16 -3
  29. package/src/modes/controllers/input-controller.ts +4 -2
  30. package/src/modes/controllers/selector-controller.ts +20 -7
  31. package/src/modes/interactive-mode.ts +2 -2
  32. package/src/modes/rpc/rpc-client.ts +5 -10
  33. package/src/modes/rpc/rpc-types.ts +5 -5
  34. package/src/modes/theme/theme.ts +8 -3
  35. package/src/patch/hashline.ts +26 -3
  36. package/src/priority.json +1 -0
  37. package/src/prompts/system/auto-handoff-threshold-focus.md +1 -0
  38. package/src/prompts/system/system-prompt.md +18 -2
  39. package/src/prompts/tools/hashline.md +139 -83
  40. package/src/sdk.ts +22 -14
  41. package/src/session/agent-session.ts +259 -117
  42. package/src/session/agent-storage.ts +14 -14
  43. package/src/session/compaction/compaction.ts +500 -13
  44. package/src/session/messages.ts +12 -1
  45. package/src/session/session-manager.ts +77 -19
  46. package/src/slash-commands/builtin-registry.ts +48 -0
  47. package/src/task/agents.ts +3 -2
  48. package/src/task/executor.ts +2 -2
  49. package/src/task/types.ts +2 -1
  50. package/src/thinking.ts +87 -0
  51. package/src/tools/browser.ts +15 -6
  52. package/src/tools/fetch.ts +118 -100
  53. package/src/web/search/providers/exa.ts +74 -3
package/CHANGELOG.md CHANGED
@@ -2,6 +2,83 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [13.9.3] - 2026-03-07
6
+
7
+ ### Breaking Changes
8
+
9
+ - Changed `ThinkingLevel` type to be imported from `@oh-my-pi/pi-agent-core` instead of `@oh-my-pi/pi-ai`
10
+ - Changed thinking level representation from string literals to `Effort` enum values (e.g., `Effort.High` instead of `"high"`)
11
+ - Changed `getThinkingLevel()` return type to `ThinkingLevel | undefined` to support models without thinking support
12
+ - Changed model `reasoning` property to `thinking` property with `ThinkingConfig` for explicit effort level configuration
13
+ - Changed `thinkingLevel` in session context to be optional (`ThinkingLevel | undefined`) instead of always present
14
+
15
+ ### Added
16
+
17
+ - Added `thinking.ts` module with `getThinkingLevelMetadata()` and `resolveThinkingLevelForModel()` utilities for thinking level handling
18
+ - Added `ThinkingConfig` support to model definitions for specifying supported thinking effort levels per model
19
+ - Added `enrichModelThinking()` function to apply thinking configuration to models during registry initialization
20
+ - Added `clampThinkingLevelForModel()` function to constrain thinking levels to model-supported ranges
21
+ - Added `getSupportedEfforts()` function to retrieve available thinking efforts for a model
22
+ - Added `Effort` enum import from `@oh-my-pi/pi-ai` for type-safe thinking level representation
23
+ - Added `/fast` slash command to toggle OpenAI service tier priority mode for faster response processing
24
+ - Added `serviceTier` setting to control OpenAI processing priority (none, auto, default, flex, scale, priority)
25
+ - Added `compaction.remoteEnabled` setting to control use of remote compaction endpoints
26
+ - Added remote compaction support for OpenAI and OpenAI Codex models with encrypted reasoning preservation
27
+ - Added fast mode indicator (⚡) to model segment in status line when priority service tier is active
28
+ - Added context usage threshold levels (normal, warning, purple, error) with token-aware thresholds for better context awareness
29
+ - Added `isFastModeEnabled()`, `setFastMode()`, and `toggleFastMode()` methods to AgentSession for fast mode control
30
+
31
+ ### Changed
32
+
33
+ - Changed credential deletion to disable credentials with persisted cause instead of permanent deletion
34
+ - Added `disabledCause` parameter to credential deletion methods to track reason for disabling
35
+ - Changed thinking level parsing to use `parseEffort()` from local thinking module instead of `parseThinkingLevel()` from pi-ai
36
+ - Changed model list display to show supported thinking efforts (e.g., "low,medium,high") instead of yes/no reasoning indicator
37
+ - Changed footer and status line to check `model.thinking` instead of `model.reasoning` for thinking level display
38
+ - Changed thinking selector to work with `Effort` type instead of `ThinkingLevel` for available levels
39
+ - Changed model resolver to return `undefined` for thinking level instead of `"off"` when no thinking is specified
40
+ - Changed compaction reasoning parameters to use `Effort` enum values instead of string literals
41
+ - Changed RPC types to use `Effort` for cycling thinking levels and `ThinkingLevel | undefined` for session state
42
+ - Changed theme thinking border color function to accept both `ThinkingLevel` and `Effort` types
43
+ - Changed context usage coloring in footer and status line to use token-aware thresholds instead of fixed percentages
44
+ - Changed compaction to preserve OpenAI remote compaction state and encrypted reasoning across sessions
45
+ - Changed compaction to skip emitting kept messages when using OpenAI remote compaction with preserved history
46
+ - Changed session context to include `serviceTier` field for tracking active service tier across session branches
47
+ - Changed `compact()` function to accept `remoteInstructions` option for custom remote compaction prompts
48
+ - Changed model registry to apply hardcoded policies (gpt-5.4 context window) consistently across all model loading paths
49
+
50
+ ### Fixed
51
+
52
+ - Fixed OpenAI remote compaction to correctly append incremental responses instead of replacing entire history
53
+ - Fixed thinking level display logic in main.ts to correctly check for undefined instead of "off"
54
+ - Fixed model registry to preserve explicit thinking configuration on runtime-registered models
55
+ - Fixed usage limit reset time calculation to use absolute `resetsAt` timestamps instead of deprecated `resetInMs` field
56
+ - Fixed compaction summary message creation to no longer be automatically added to chat during compaction (now handled by session manager)
57
+
58
+ ## [13.9.2] - 2026-03-05
59
+
60
+ ### Added
61
+
62
+ - Support for Python code execution messages with output display and error handling
63
+ - Support for mode change entries in session exports
64
+ - Support for TTSR injection and session initialization entries in tree filtering
65
+
66
+ ### Changed
67
+
68
+ - Updated label lookup to use `targetId` field instead of `parentId` for label references
69
+ - Changed model change entry display to use `model` field instead of separate `provider` and `modelId` fields
70
+ - Simplified model change rendering by removing OpenAI Codex bridge prompt display
71
+ - Updated searchable text extraction to include Python code from `pythonExecution` messages
72
+
73
+ ### Removed
74
+
75
+ - Removed `codexInjectionInfo` from session data destructuring
76
+ - Removed OpenAI Codex-specific bridge prompt UI from model change entries
77
+
78
+ ### Fixed
79
+
80
+ - Auto-corrected off-by-one range start errors in hashline edits that would duplicate preceding lines
81
+
5
82
  ## [13.9.0] - 2026-03-05
6
83
  ### Added
7
84
 
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Shows how to select a specific model and thinking level.
5
5
  */
6
+ import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
6
7
  import { getModel } from "@oh-my-pi/pi-ai";
7
8
  import { createAgentSession, discoverAuthStorage, discoverModels } from "@oh-my-pi/pi-coding-agent";
8
9
 
@@ -32,7 +33,7 @@ console.log(
32
33
  if (available.length > 0) {
33
34
  const { session } = await createAgentSession({
34
35
  model: available[0],
35
- thinkingLevel: "medium", // off, low, medium, high
36
+ thinkingLevel: ThinkingLevel.Medium, // off, low, medium, high
36
37
  authStorage,
37
38
  modelRegistry,
38
39
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "13.9.1",
4
+ "version": "13.9.3",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -41,12 +41,12 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@mozilla/readability": "^0.6",
44
- "@oh-my-pi/omp-stats": "13.9.1",
45
- "@oh-my-pi/pi-agent-core": "13.9.1",
46
- "@oh-my-pi/pi-ai": "13.9.1",
47
- "@oh-my-pi/pi-natives": "13.9.1",
48
- "@oh-my-pi/pi-tui": "13.9.1",
49
- "@oh-my-pi/pi-utils": "13.9.1",
44
+ "@oh-my-pi/omp-stats": "13.9.3",
45
+ "@oh-my-pi/pi-agent-core": "13.9.3",
46
+ "@oh-my-pi/pi-ai": "13.9.3",
47
+ "@oh-my-pi/pi-natives": "13.9.3",
48
+ "@oh-my-pi/pi-tui": "13.9.3",
49
+ "@oh-my-pi/pi-utils": "13.9.3",
50
50
  "@sinclair/typebox": "^0.34",
51
51
  "@xterm/headless": "^6.0",
52
52
  "ajv": "^8.18",
package/src/cli/args.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * CLI argument parsing and help display
3
3
  */
4
- import { getAvailableThinkingLevels, parseThinkingLevel, type ThinkingLevel } from "@oh-my-pi/pi-ai";
4
+ import { type Effort, THINKING_EFFORTS } from "@oh-my-pi/pi-ai";
5
5
  import { APP_NAME, CONFIG_DIR_NAME, logger } from "@oh-my-pi/pi-utils";
6
6
  import chalk from "chalk";
7
+ import { parseEffort } from "../thinking";
7
8
  import { BUILTIN_TOOLS } from "../tools";
8
9
 
9
10
  export type Mode = "text" | "json" | "rpc";
@@ -19,7 +20,7 @@ export interface Args {
19
20
  apiKey?: string;
20
21
  systemPrompt?: string;
21
22
  appendSystemPrompt?: string;
22
- thinking?: ThinkingLevel;
23
+ thinking?: Effort;
23
24
  continue?: boolean;
24
25
  resume?: string | true;
25
26
  help?: boolean;
@@ -122,13 +123,13 @@ export function parseArgs(args: string[], extensionFlags?: Map<string, { type: "
122
123
  result.tools = validTools;
123
124
  } else if (arg === "--thinking" && i + 1 < args.length) {
124
125
  const rawThinking = args[++i];
125
- const thinking = parseThinkingLevel(rawThinking);
126
+ const thinking = parseEffort(rawThinking);
126
127
  if (thinking !== undefined) {
127
128
  result.thinking = thinking;
128
129
  } else {
129
130
  logger.warn("Invalid thinking level passed to --thinking", {
130
131
  level: rawThinking,
131
- validThinkingLevels: getAvailableThinkingLevels(),
132
+ validThinkingLevels: THINKING_EFFORTS,
132
133
  });
133
134
  }
134
135
  } else if (arg === "--print" || arg === "-p") {
@@ -207,7 +208,7 @@ export function getExtraHelpText(): string {
207
208
  MISTRAL_API_KEY - Mistral models
208
209
  ZAI_API_KEY - z.ai models (ZhipuAI/GLM)
209
210
  MINIMAX_API_KEY - MiniMax models
210
- OPENCODE_API_KEY - OpenCode models
211
+ OPENCODE_API_KEY - OpenCode Zen/OpenCode Go models
211
212
  CURSOR_ACCESS_TOKEN - Cursor AI models
212
213
  AI_GATEWAY_API_KEY - Vercel AI Gateway
213
214
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * List available models with optional fuzzy search
3
3
  */
4
- import type { Api, Model } from "@oh-my-pi/pi-ai";
4
+ import { type Api, getSupportedEfforts, type Model } from "@oh-my-pi/pi-ai";
5
5
  import { formatNumber } from "@oh-my-pi/pi-utils";
6
6
  import type { ModelRegistry } from "../config/model-registry";
7
7
  import { fuzzyFilter } from "../utils/fuzzy";
@@ -41,7 +41,7 @@ export async function listModels(modelRegistry: ModelRegistry, searchPattern?: s
41
41
  model: m.id,
42
42
  context: formatNumber(m.contextWindow),
43
43
  maxOut: formatNumber(m.maxTokens),
44
- thinking: m.reasoning ? "yes" : "no",
44
+ thinking: m.thinking ? getSupportedEfforts(m).join(",") : m.reasoning ? "yes" : "-",
45
45
  images: m.input.includes("image") ? "yes" : "no",
46
46
  }));
47
47
 
@@ -2,7 +2,7 @@
2
2
  * Root command for the coding agent CLI.
3
3
  */
4
4
 
5
- import { getAvailableThinkingLevels } from "@oh-my-pi/pi-ai";
5
+ import { THINKING_EFFORTS } from "@oh-my-pi/pi-ai";
6
6
  import { APP_NAME } from "@oh-my-pi/pi-utils";
7
7
  import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
8
8
  import { parseArgs } from "../cli/args";
@@ -86,8 +86,8 @@ export default class Index extends Command {
86
86
  description: "Comma-separated list of tools to enable (default: all)",
87
87
  }),
88
88
  thinking: Flags.string({
89
- description: `Set thinking level: ${getAvailableThinkingLevels().join(", ")}`,
90
- options: getAvailableThinkingLevels(),
89
+ description: `Set thinking level: ${THINKING_EFFORTS.join(", ")}`,
90
+ options: [...THINKING_EFFORTS],
91
91
  }),
92
92
  hook: Flags.string({
93
93
  description: "Load a hook/extension file (can be used multiple times)",
@@ -4,6 +4,7 @@ import {
4
4
  type Context,
5
5
  createModelManager,
6
6
  DEFAULT_LOCAL_TOKEN,
7
+ enrichModelThinking,
7
8
  getBundledModels,
8
9
  getBundledProviders,
9
10
  googleAntigravityModelManagerOptions,
@@ -18,6 +19,7 @@ import {
18
19
  registerCustomApi,
19
20
  registerOAuthProvider,
20
21
  type SimpleStreamOptions,
22
+ type ThinkingConfig,
21
23
  unregisterCustomApis,
22
24
  unregisterOAuthProviders,
23
25
  } from "@oh-my-pi/pi-ai";
@@ -72,6 +74,28 @@ const OpenAICompatSchema = Type.Object({
72
74
  vercelGatewayRouting: Type.Optional(VercelGatewayRoutingSchema),
73
75
  });
74
76
 
77
+ const EffortSchema = Type.Union([
78
+ Type.Literal("minimal"),
79
+ Type.Literal("low"),
80
+ Type.Literal("medium"),
81
+ Type.Literal("high"),
82
+ Type.Literal("xhigh"),
83
+ ]);
84
+
85
+ const ThinkingControlModeSchema = Type.Union([
86
+ Type.Literal("effort"),
87
+ Type.Literal("budget"),
88
+ Type.Literal("google-level"),
89
+ Type.Literal("anthropic-adaptive"),
90
+ Type.Literal("anthropic-budget-effort"),
91
+ ]);
92
+
93
+ const ModelThinkingSchema = Type.Object({
94
+ minLevel: EffortSchema,
95
+ maxLevel: EffortSchema,
96
+ mode: ThinkingControlModeSchema,
97
+ });
98
+
75
99
  // Schema for custom model definition
76
100
  // Most fields are optional with sensible defaults for local models (Ollama, LM Studio, etc.)
77
101
  const ModelDefinitionSchema = Type.Object({
@@ -88,7 +112,9 @@ const ModelDefinitionSchema = Type.Object({
88
112
  Type.Literal("google-vertex"),
89
113
  ]),
90
114
  ),
115
+ baseUrl: Type.Optional(Type.String({ minLength: 1 })),
91
116
  reasoning: Type.Optional(Type.Boolean()),
117
+ thinking: Type.Optional(ModelThinkingSchema),
92
118
  input: Type.Optional(Type.Array(Type.Union([Type.Literal("text"), Type.Literal("image")]))),
93
119
  cost: Type.Optional(
94
120
  Type.Object({
@@ -110,6 +136,7 @@ const ModelDefinitionSchema = Type.Object({
110
136
  const ModelOverrideSchema = Type.Object({
111
137
  name: Type.Optional(Type.String({ minLength: 1 })),
112
138
  reasoning: Type.Optional(Type.Boolean()),
139
+ thinking: Type.Optional(ModelThinkingSchema),
113
140
  input: Type.Optional(Type.Array(Type.Union([Type.Literal("text"), Type.Literal("image")]))),
114
141
  cost: Type.Optional(
115
142
  Type.Object({
@@ -375,6 +402,7 @@ function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<A
375
402
  const result = { ...model };
376
403
  if (override.name !== undefined) result.name = override.name;
377
404
  if (override.reasoning !== undefined) result.reasoning = override.reasoning;
405
+ if (override.thinking !== undefined) result.thinking = override.thinking as ThinkingConfig;
378
406
  if (override.input !== undefined) result.input = override.input as ("text" | "image")[];
379
407
  if (override.contextWindow !== undefined) result.contextWindow = override.contextWindow;
380
408
  if (override.maxTokens !== undefined) result.maxTokens = override.maxTokens;
@@ -392,14 +420,16 @@ function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<A
392
420
  result.headers = { ...model.headers, ...override.headers };
393
421
  }
394
422
  result.compat = mergeCompat(model.compat, override.compat);
395
- return result;
423
+ return enrichModelThinking(result);
396
424
  }
397
425
 
398
426
  interface CustomModelDefinitionLike {
399
427
  id: string;
400
428
  name?: string;
401
429
  api?: Api;
430
+ baseUrl?: string;
402
431
  reasoning?: boolean;
432
+ thinking?: ThinkingConfig;
403
433
  input?: ("text" | "image")[];
404
434
  cost?: { input: number; output: number; cacheRead: number; cacheWrite: number };
405
435
  contextWindow?: number;
@@ -445,13 +475,14 @@ function buildCustomModel(
445
475
  const withDefaults = options.useDefaults;
446
476
  const cost = modelDef.cost ?? (withDefaults ? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } : undefined);
447
477
  const input = modelDef.input ?? (withDefaults ? ["text"] : undefined);
448
- return {
478
+ return enrichModelThinking({
449
479
  id: modelDef.id,
450
480
  name: modelDef.name ?? (withDefaults ? modelDef.id : undefined),
451
481
  api,
452
482
  provider: providerName,
453
- baseUrl: providerBaseUrl,
483
+ baseUrl: modelDef.baseUrl ?? providerBaseUrl,
454
484
  reasoning: modelDef.reasoning ?? (withDefaults ? false : undefined),
485
+ thinking: modelDef.thinking as ThinkingConfig | undefined,
455
486
  input: input as ("text" | "image")[],
456
487
  cost,
457
488
  contextWindow: modelDef.contextWindow ?? (withDefaults ? 128000 : undefined),
@@ -460,7 +491,7 @@ function buildCustomModel(
460
491
  compat: modelDef.compat,
461
492
  contextPromotionTarget: modelDef.contextPromotionTarget,
462
493
  premiumMultiplier: modelDef.premiumMultiplier,
463
- } as Model<Api>;
494
+ } as Model<Api>);
464
495
  }
465
496
 
466
497
  /**
@@ -537,7 +568,7 @@ export class ModelRegistry {
537
568
  const builtInModels = this.#loadBuiltInModels(overrides, modelOverrides);
538
569
  const combined = this.#mergeCustomModels(builtInModels, customModels);
539
570
 
540
- this.#models = combined;
571
+ this.#models = this.#applyHardcodedModelPolicies(combined);
541
572
  }
542
573
 
543
574
  /** Load built-in models, applying provider and per-model overrides */
@@ -716,7 +747,7 @@ export class ModelRegistry {
716
747
  : model;
717
748
  }),
718
749
  );
719
- this.#models = this.#applyModelOverrides(merged, this.#modelOverrides);
750
+ this.#models = this.#applyHardcodedModelPolicies(this.#applyModelOverrides(merged, this.#modelOverrides));
720
751
  }
721
752
 
722
753
  async #discoverProviderModels(providerConfig: DiscoveryProviderConfig): Promise<Model<Api>[]> {
@@ -853,19 +884,21 @@ export class ModelRegistry {
853
884
  for (const item of models) {
854
885
  const id = item.model || item.name;
855
886
  if (!id) continue;
856
- discovered.push({
857
- id,
858
- name: item.name || id,
859
- api: providerConfig.api,
860
- provider: providerConfig.provider,
861
- baseUrl: `${endpoint}/v1`,
862
- reasoning: false,
863
- input: ["text"],
864
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
865
- contextWindow: 128000,
866
- maxTokens: 8192,
867
- headers: providerConfig.headers,
868
- });
887
+ discovered.push(
888
+ enrichModelThinking({
889
+ id,
890
+ name: item.name || id,
891
+ api: providerConfig.api,
892
+ provider: providerConfig.provider,
893
+ baseUrl: `${endpoint}/v1`,
894
+ reasoning: false,
895
+ input: ["text"],
896
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
897
+ contextWindow: 128000,
898
+ maxTokens: 8192,
899
+ headers: providerConfig.headers,
900
+ }),
901
+ );
869
902
  }
870
903
  return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
871
904
  } catch (error) {
@@ -907,24 +940,26 @@ export class ModelRegistry {
907
940
  for (const item of models) {
908
941
  const id = item.id;
909
942
  if (!id) continue;
910
- discovered.push({
911
- id,
912
- name: id,
913
- api: providerConfig.api,
914
- provider: providerConfig.provider,
915
- baseUrl,
916
- reasoning: false,
917
- input: ["text"],
918
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
919
- contextWindow: 128000,
920
- maxTokens: 8192,
921
- headers,
922
- compat: {
923
- supportsStore: false,
924
- supportsDeveloperRole: false,
925
- supportsReasoningEffort: false,
926
- },
927
- });
943
+ discovered.push(
944
+ enrichModelThinking({
945
+ id,
946
+ name: id,
947
+ api: providerConfig.api,
948
+ provider: providerConfig.provider,
949
+ baseUrl,
950
+ reasoning: false,
951
+ input: ["text"],
952
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
953
+ contextWindow: 128000,
954
+ maxTokens: 8192,
955
+ headers,
956
+ compat: {
957
+ supportsStore: false,
958
+ supportsDeveloperRole: false,
959
+ supportsReasoningEffort: false,
960
+ },
961
+ }),
962
+ );
928
963
  }
929
964
  return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
930
965
  } catch (error) {
@@ -980,6 +1015,15 @@ export class ModelRegistry {
980
1015
  });
981
1016
  }
982
1017
 
1018
+ #applyHardcodedModelPolicies(models: Model<Api>[]): Model<Api>[] {
1019
+ return models.map(model => {
1020
+ if (model.id === "gpt-5.4") {
1021
+ return { ...model, contextWindow: 1_000_000 };
1022
+ }
1023
+ return model;
1024
+ });
1025
+ }
1026
+
983
1027
  #parseModels(config: ModelsConfig): Model<Api>[] {
984
1028
  const models: Model<Api>[] = [];
985
1029
 
@@ -997,7 +1041,7 @@ export class ModelRegistry {
997
1041
  providerConfig.headers,
998
1042
  providerConfig.apiKey,
999
1043
  providerConfig.authHeader,
1000
- modelDef,
1044
+ modelDef as CustomModelDefinitionLike,
1001
1045
  { useDefaults: true },
1002
1046
  );
1003
1047
  if (!model) continue;
@@ -1150,7 +1194,7 @@ export class ModelRegistry {
1150
1194
  config.headers,
1151
1195
  config.apiKey,
1152
1196
  config.authHeader,
1153
- modelDef,
1197
+ modelDef as CustomModelDefinitionLike,
1154
1198
  { useDefaults: false },
1155
1199
  );
1156
1200
  if (!model) {
@@ -1205,7 +1249,9 @@ export interface ProviderConfigInput {
1205
1249
  id: string;
1206
1250
  name: string;
1207
1251
  api?: Api;
1252
+ baseUrl?: string;
1208
1253
  reasoning: boolean;
1254
+ thinking?: ThinkingConfig;
1209
1255
  input: ("text" | "image")[];
1210
1256
  cost: { input: number; output: number; cacheRead: number; cacheWrite: number };
1211
1257
  contextWindow: number;
@@ -1,17 +1,20 @@
1
1
  /**
2
2
  * Model resolution, scoping, and initial selection
3
3
  */
4
+
5
+ import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
4
6
  import {
5
7
  type Api,
8
+ clampThinkingLevelForModel,
6
9
  DEFAULT_MODEL_PER_PROVIDER,
10
+ type Effort,
7
11
  type KnownProvider,
8
12
  type Model,
9
13
  modelsAreEqual,
10
- parseThinkingLevel,
11
- type ThinkingLevel,
12
14
  } from "@oh-my-pi/pi-ai";
13
15
  import chalk from "chalk";
14
16
  import MODEL_PRIO from "../priority.json" with { type: "json" };
17
+ import { parseThinkingLevel, resolveThinkingLevelForModel } from "../thinking";
15
18
  import { fuzzyMatch } from "../utils/fuzzy";
16
19
  import { MODEL_ROLE_IDS, type ModelRegistry, type ModelRole } from "./model-registry";
17
20
  import type { Settings } from "./settings";
@@ -377,7 +380,14 @@ export function resolveModelRoleValue(
377
380
  options?.matchPreferences,
378
381
  );
379
382
 
380
- return { model, thinkingLevel, explicitThinkingLevel, warning };
383
+ return {
384
+ model,
385
+ thinkingLevel: explicitThinkingLevel
386
+ ? (resolveThinkingLevelForModel(model, thinkingLevel) ?? thinkingLevel)
387
+ : thinkingLevel,
388
+ explicitThinkingLevel,
389
+ warning,
390
+ };
381
391
  }
382
392
 
383
393
  export function extractExplicitThinkingSelector(
@@ -393,10 +403,10 @@ export function extractExplicitThinkingSelector(
393
403
  while (!visited.has(current)) {
394
404
  visited.add(current);
395
405
  const lastColonIndex = current.lastIndexOf(":");
396
- const hasThinkingSuffix =
397
- lastColonIndex > PREFIX_MODEL_ROLE.length && parseThinkingLevel(current.slice(lastColonIndex + 1));
398
- if (hasThinkingSuffix) {
399
- return current.slice(lastColonIndex + 1) as ThinkingLevel;
406
+ const thinkingSelector =
407
+ lastColonIndex > PREFIX_MODEL_ROLE.length ? parseThinkingLevel(current.slice(lastColonIndex + 1)) : undefined;
408
+ if (thinkingSelector) {
409
+ return thinkingSelector;
400
410
  }
401
411
  const expanded = expandRoleAlias(current, settings).trim();
402
412
  if (!expanded || expanded === current) break;
@@ -520,7 +530,13 @@ export async function resolveModelScope(
520
530
 
521
531
  for (const model of matchingModels) {
522
532
  if (!scopedModels.find(sm => modelsAreEqual(sm.model, model))) {
523
- scopedModels.push({ model, thinkingLevel, explicitThinkingLevel });
533
+ scopedModels.push({
534
+ model,
535
+ thinkingLevel: explicitThinkingLevel
536
+ ? (resolveThinkingLevelForModel(model, thinkingLevel) ?? thinkingLevel)
537
+ : thinkingLevel,
538
+ explicitThinkingLevel,
539
+ });
524
540
  }
525
541
  }
526
542
  continue;
@@ -543,7 +559,13 @@ export async function resolveModelScope(
543
559
 
544
560
  // Avoid duplicates
545
561
  if (!scopedModels.find(sm => modelsAreEqual(sm.model, model))) {
546
- scopedModels.push({ model, thinkingLevel, explicitThinkingLevel });
562
+ scopedModels.push({
563
+ model,
564
+ thinkingLevel: explicitThinkingLevel
565
+ ? (resolveThinkingLevelForModel(model, thinkingLevel) ?? thinkingLevel)
566
+ : thinkingLevel,
567
+ explicitThinkingLevel,
568
+ });
547
569
  }
548
570
  }
549
571
 
@@ -644,7 +666,7 @@ export function resolveCliModel(options: {
644
666
 
645
667
  export interface InitialModelResult {
646
668
  model: Model<Api> | undefined;
647
- thinkingLevel: ThinkingLevel;
669
+ thinkingLevel?: ThinkingLevel;
648
670
  fallbackMessage: string | undefined;
649
671
  }
650
672
 
@@ -663,7 +685,7 @@ export async function findInitialModel(options: {
663
685
  isContinuing: boolean;
664
686
  defaultProvider?: string;
665
687
  defaultModelId?: string;
666
- defaultThinkingSelector?: ThinkingLevel;
688
+ defaultThinkingSelector?: Effort;
667
689
  modelRegistry: ModelRegistry;
668
690
  }): Promise<InitialModelResult> {
669
691
  const {
@@ -678,7 +700,7 @@ export async function findInitialModel(options: {
678
700
  } = options;
679
701
 
680
702
  let model: Model<Api> | undefined;
681
- let thinkingLevel: ThinkingLevel = "off";
703
+ let thinkingLevel: Effort | undefined;
682
704
 
683
705
  // 1. CLI args take priority
684
706
  if (cliProvider && cliModel) {
@@ -687,16 +709,22 @@ export async function findInitialModel(options: {
687
709
  console.error(chalk.red(`Model ${cliProvider}/${cliModel} not found`));
688
710
  process.exit(1);
689
711
  }
690
- return { model: found, thinkingLevel: "off", fallbackMessage: undefined };
712
+ return { model: found, thinkingLevel: undefined, fallbackMessage: undefined };
691
713
  }
692
714
 
693
715
  // 2. Use first model from scoped models (skip if continuing/resuming)
694
716
  if (scopedModels.length > 0 && !isContinuing) {
695
717
  const scoped = scopedModels[0];
696
- const scopedThinkingSelector = scoped.thinkingLevel ?? defaultThinkingSelector ?? "off";
718
+ const scopedThinkingSelector =
719
+ scoped.thinkingLevel === ThinkingLevel.Inherit
720
+ ? defaultThinkingSelector
721
+ : (scoped.thinkingLevel ?? defaultThinkingSelector);
697
722
  return {
698
723
  model: scoped.model,
699
- thinkingLevel: scopedThinkingSelector,
724
+ thinkingLevel:
725
+ scopedThinkingSelector === ThinkingLevel.Off
726
+ ? ThinkingLevel.Off
727
+ : clampThinkingLevelForModel(scoped.model, scopedThinkingSelector),
700
728
  fallbackMessage: undefined,
701
729
  };
702
730
  }
@@ -706,9 +734,7 @@ export async function findInitialModel(options: {
706
734
  const found = modelRegistry.find(defaultProvider, defaultModelId);
707
735
  if (found) {
708
736
  model = found;
709
- if (defaultThinkingSelector) {
710
- thinkingLevel = defaultThinkingSelector;
711
- }
737
+ thinkingLevel = clampThinkingLevelForModel(found, defaultThinkingSelector);
712
738
  return { model, thinkingLevel, fallbackMessage: undefined };
713
739
  }
714
740
  }
@@ -722,16 +748,16 @@ export async function findInitialModel(options: {
722
748
  const defaultId = defaultModelPerProvider[provider];
723
749
  const match = availableModels.find(m => m.provider === provider && m.id === defaultId);
724
750
  if (match) {
725
- return { model: match, thinkingLevel: "off", fallbackMessage: undefined };
751
+ return { model: match, thinkingLevel: undefined, fallbackMessage: undefined };
726
752
  }
727
753
  }
728
754
 
729
755
  // If no default found, use first available
730
- return { model: availableModels[0], thinkingLevel: "off", fallbackMessage: undefined };
756
+ return { model: availableModels[0], thinkingLevel: undefined, fallbackMessage: undefined };
731
757
  }
732
758
 
733
759
  // 5. No model found
734
- return { model: undefined, thinkingLevel: "off", fallbackMessage: undefined };
760
+ return { model: undefined, thinkingLevel: undefined, fallbackMessage: undefined };
735
761
  }
736
762
 
737
763
  /**