feat: introduced Effort enum and ThinkingConfig for model-aware reasoning
- Introduced Effort enum and ThinkingConfig metadata for per-model reasoning capabilities with min/max effort levels. - Migrated thinking level API from string-based ThinkingLevel to structured Effort enum across agent and AI packages. - Added model-thinking module with effort mapping, policy application, and semantic versioning utilities for provider-specific thinking modes. - Removed supportsXhigh() function and replaced effort clamping with model-aware validation using ThinkingConfig metadata. - Expanded models.json with thinking configuration objects for 50+ models including Claude, Gemini, and OpenAI variants. - Added Python analysis scripts for edit tool usage patterns and tool invocation stream processing.
This commit is contained in:
@@ -187,59 +187,59 @@
|
||||
|
||||
"@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
|
||||
|
||||
"@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.1002.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.17", "@aws-sdk/credential-provider-node": "^3.972.16", "@aws-sdk/eventstream-handler-node": "^3.972.9", "@aws-sdk/middleware-eventstream": "^3.972.6", "@aws-sdk/middleware-host-header": "^3.972.6", "@aws-sdk/middleware-logger": "^3.972.6", "@aws-sdk/middleware-recursion-detection": "^3.972.6", "@aws-sdk/middleware-user-agent": "^3.972.17", "@aws-sdk/middleware-websocket": "^3.972.11", "@aws-sdk/region-config-resolver": "^3.972.6", "@aws-sdk/token-providers": "3.1002.0", "@aws-sdk/types": "^3.973.4", "@aws-sdk/util-endpoints": "^3.996.3", "@aws-sdk/util-user-agent-browser": "^3.972.6", "@aws-sdk/util-user-agent-node": "^3.973.2", "@smithy/config-resolver": "^4.4.9", "@smithy/core": "^3.23.7", "@smithy/eventstream-serde-browser": "^4.2.10", "@smithy/eventstream-serde-config-resolver": "^4.3.10", "@smithy/eventstream-serde-node": "^4.2.10", "@smithy/fetch-http-handler": "^5.3.12", "@smithy/hash-node": "^4.2.10", "@smithy/invalid-dependency": "^4.2.10", "@smithy/middleware-content-length": "^4.2.10", "@smithy/middleware-endpoint": "^4.4.21", "@smithy/middleware-retry": "^4.4.38", "@smithy/middleware-serde": "^4.2.11", "@smithy/middleware-stack": "^4.2.10", "@smithy/node-config-provider": "^4.3.10", "@smithy/node-http-handler": "^4.4.13", "@smithy/protocol-http": "^5.3.10", "@smithy/smithy-client": "^4.12.1", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.10", "@smithy/util-base64": "^4.3.1", "@smithy/util-body-length-browser": "^4.2.1", "@smithy/util-body-length-node": "^4.2.2", "@smithy/util-defaults-mode-browser": "^4.3.37", "@smithy/util-defaults-mode-node": "^4.2.40", "@smithy/util-endpoints": "^3.3.1", "@smithy/util-middleware": "^4.2.10", "@smithy/util-retry": "^4.2.10", "@smithy/util-stream": "^4.5.16", "@smithy/util-utf8": "^4.2.1", "tslib": "^2.6.2" } }, "sha512-xUmzgTvTeQFVxBqla8U4nXpZNXLcZ0xszfZ4yxdTUNyChQQb7JLaH4E8pAbl7ulg0RoJ4ChNWtOqMJC/N3+qcQ=="],
|
||||
"@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.1003.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.18", "@aws-sdk/credential-provider-node": "^3.972.17", "@aws-sdk/eventstream-handler-node": "^3.972.10", "@aws-sdk/middleware-eventstream": "^3.972.7", "@aws-sdk/middleware-host-header": "^3.972.7", "@aws-sdk/middleware-logger": "^3.972.7", "@aws-sdk/middleware-recursion-detection": "^3.972.7", "@aws-sdk/middleware-user-agent": "^3.972.18", "@aws-sdk/middleware-websocket": "^3.972.12", "@aws-sdk/region-config-resolver": "^3.972.7", "@aws-sdk/token-providers": "3.1003.0", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@aws-sdk/util-user-agent-browser": "^3.972.7", "@aws-sdk/util-user-agent-node": "^3.973.3", "@smithy/config-resolver": "^4.4.10", "@smithy/core": "^3.23.8", "@smithy/eventstream-serde-browser": "^4.2.11", "@smithy/eventstream-serde-config-resolver": "^4.3.11", "@smithy/eventstream-serde-node": "^4.2.11", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/hash-node": "^4.2.11", "@smithy/invalid-dependency": "^4.2.11", "@smithy/middleware-content-length": "^4.2.11", "@smithy/middleware-endpoint": "^4.4.22", "@smithy/middleware-retry": "^4.4.39", "@smithy/middleware-serde": "^4.2.12", "@smithy/middleware-stack": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/node-http-handler": "^4.4.14", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.38", "@smithy/util-defaults-mode-node": "^4.2.41", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/util-stream": "^4.5.17", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-b39kYrFC3dGFQ7S5UiHKD8aGCFr0/k+QXDzqnT8N2zi8JILEvdxBhMWNqCIpZAbCCK2Jp9S8jK5/Vh0TfLUIPQ=="],
|
||||
|
||||
"@aws-sdk/core": ["@aws-sdk/core@3.973.17", "", { "dependencies": { "@aws-sdk/types": "^3.973.4", "@aws-sdk/xml-builder": "^3.972.9", "@smithy/core": "^3.23.7", "@smithy/node-config-provider": "^4.3.10", "@smithy/property-provider": "^4.2.10", "@smithy/protocol-http": "^5.3.10", "@smithy/signature-v4": "^5.3.10", "@smithy/smithy-client": "^4.12.1", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.1", "@smithy/util-middleware": "^4.2.10", "@smithy/util-utf8": "^4.2.1", "tslib": "^2.6.2" } }, "sha512-VtgGP0TjbCeyp6DQpiBqJKbemTSIaN2bZc3UbeTDCani3lBCyxn75ouJYD6koSSp0bh7rKLEbUpiFsNCI7tr0w=="],
|
||||
"@aws-sdk/core": ["@aws-sdk/core@3.973.18", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws-sdk/xml-builder": "^3.972.10", "@smithy/core": "^3.23.8", "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-GUIlegfcK2LO1J2Y98sCJy63rQSiLiDOgVw7HiHPRqfI2vb3XozTVqemwO0VSGXp54ngCnAQz0Lf0YPCBINNxA=="],
|
||||
|
||||
"@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.15", "", { "dependencies": { "@aws-sdk/core": "^3.973.17", "@aws-sdk/types": "^3.973.4", "@smithy/property-provider": "^4.2.10", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-RhHQG1lhkWHL4tK1C/KDjaOeis+9U0tAMnWDiwiSVQZMC7CsST9Xin+sK89XywJ5g/tyABtb7TvFePJ4Te5XSQ=="],
|
||||
"@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.16", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-HrdtnadvTGAQUr18sPzGlE5El3ICphnH6SU7UQOMOWFgRKbTRNN8msTxM4emzguUso9CzaHU2xy5ctSrmK5YNA=="],
|
||||
|
||||
"@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.17", "", { "dependencies": { "@aws-sdk/core": "^3.973.17", "@aws-sdk/types": "^3.973.4", "@smithy/fetch-http-handler": "^5.3.12", "@smithy/node-http-handler": "^4.4.13", "@smithy/property-provider": "^4.2.10", "@smithy/protocol-http": "^5.3.10", "@smithy/smithy-client": "^4.12.1", "@smithy/types": "^4.13.0", "@smithy/util-stream": "^4.5.16", "tslib": "^2.6.2" } }, "sha512-b/bDL76p51+yQ+0O9ZDH5nw/ioE0sRYkjwjOwFWAWZXo6it2kQZUOXhVpjohx3ldKyUxt/SwAivjUu1Nr/PWlQ=="],
|
||||
"@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.18", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/types": "^3.973.5", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/node-http-handler": "^4.4.14", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/util-stream": "^4.5.17", "tslib": "^2.6.2" } }, "sha512-NyB6smuZAixND5jZumkpkunQ0voc4Mwgkd+SZ6cvAzIB7gK8HV8Zd4rS8Kn5MmoGgusyNfVGG+RLoYc4yFiw+A=="],
|
||||
|
||||
"@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.15", "", { "dependencies": { "@aws-sdk/core": "^3.973.17", "@aws-sdk/credential-provider-env": "^3.972.15", "@aws-sdk/credential-provider-http": "^3.972.17", "@aws-sdk/credential-provider-login": "^3.972.15", "@aws-sdk/credential-provider-process": "^3.972.15", "@aws-sdk/credential-provider-sso": "^3.972.15", "@aws-sdk/credential-provider-web-identity": "^3.972.15", "@aws-sdk/nested-clients": "^3.996.5", "@aws-sdk/types": "^3.973.4", "@smithy/credential-provider-imds": "^4.2.10", "@smithy/property-provider": "^4.2.10", "@smithy/shared-ini-file-loader": "^4.4.5", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-qWnM+wB8MmU2kKY7f4KowKjOjkwRosaFxrtseEEIefwoXn1SjN+CbHzXBVdTAQxxkbBiqhPgJ/WHiPtES4grRQ=="],
|
||||
"@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.16", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/credential-provider-env": "^3.972.16", "@aws-sdk/credential-provider-http": "^3.972.18", "@aws-sdk/credential-provider-login": "^3.972.16", "@aws-sdk/credential-provider-process": "^3.972.16", "@aws-sdk/credential-provider-sso": "^3.972.16", "@aws-sdk/credential-provider-web-identity": "^3.972.16", "@aws-sdk/nested-clients": "^3.996.6", "@aws-sdk/types": "^3.973.5", "@smithy/credential-provider-imds": "^4.2.11", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hzAnzNXKV0A4knFRWGu2NCt72P4WWxpEGnOc6H3DptUjC4oX3hGw846oN76M1rTHAOwDdbhjU0GAOWR4OUfTZg=="],
|
||||
|
||||
"@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.15", "", { "dependencies": { "@aws-sdk/core": "^3.973.17", "@aws-sdk/nested-clients": "^3.996.5", "@aws-sdk/types": "^3.973.4", "@smithy/property-provider": "^4.2.10", "@smithy/protocol-http": "^5.3.10", "@smithy/shared-ini-file-loader": "^4.4.5", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-x92FJy34/95wgu+qOGD8SHcgh1hZ9Qx2uFtQEGn4m9Ljou8ICIv3Ybq5yxdB7A60S8ZGCQB0mIopmjJwiLbh5g=="],
|
||||
"@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.16", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/nested-clients": "^3.996.6", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-VI0kXTlr0o1FTay+Jvx6AKqx5ECBgp7X4VevGBEbuXdCXnNp7SPU0KvjsOLVhIz3OoPK4/lTXphk43t0IVk65w=="],
|
||||
|
||||
"@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.16", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.15", "@aws-sdk/credential-provider-http": "^3.972.17", "@aws-sdk/credential-provider-ini": "^3.972.15", "@aws-sdk/credential-provider-process": "^3.972.15", "@aws-sdk/credential-provider-sso": "^3.972.15", "@aws-sdk/credential-provider-web-identity": "^3.972.15", "@aws-sdk/types": "^3.973.4", "@smithy/credential-provider-imds": "^4.2.10", "@smithy/property-provider": "^4.2.10", "@smithy/shared-ini-file-loader": "^4.4.5", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-7mlt14Ee4rPFAFUVgpWE7+0CBhetJJyzVFqfIsMp7sgyOSm9Y/+qHZOWAuK5I4JNc+Y5PltvJ9kssTzRo92iXQ=="],
|
||||
"@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.17", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.16", "@aws-sdk/credential-provider-http": "^3.972.18", "@aws-sdk/credential-provider-ini": "^3.972.16", "@aws-sdk/credential-provider-process": "^3.972.16", "@aws-sdk/credential-provider-sso": "^3.972.16", "@aws-sdk/credential-provider-web-identity": "^3.972.16", "@aws-sdk/types": "^3.973.5", "@smithy/credential-provider-imds": "^4.2.11", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-98MAcQ2Dk7zkvgwZ5f6fLX2lTyptC3gTSDx4EpvTdJWET8qs9lBPYggoYx7GmKp/5uk0OwVl0hxIDZsDNS/Y9g=="],
|
||||
|
||||
"@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.15", "", { "dependencies": { "@aws-sdk/core": "^3.973.17", "@aws-sdk/types": "^3.973.4", "@smithy/property-provider": "^4.2.10", "@smithy/shared-ini-file-loader": "^4.4.5", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-PrH3iTeD18y/8uJvQD2s/T87BTGhsdS/1KZU7ReWHXsplBwvCqi7AbnnNbML1pFlQwRWCE2RdSZFWDVId3CvkA=="],
|
||||
"@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.16", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-n89ibATwnLEg0ZdZmUds5bq8AfBAdoYEDpqP3uzPLaRuGelsKlIvCYSNNvfgGLi8NaHPNNhs1HjJZYbqkW9b+g=="],
|
||||
|
||||
"@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.15", "", { "dependencies": { "@aws-sdk/core": "^3.973.17", "@aws-sdk/nested-clients": "^3.996.5", "@aws-sdk/token-providers": "3.1002.0", "@aws-sdk/types": "^3.973.4", "@smithy/property-provider": "^4.2.10", "@smithy/shared-ini-file-loader": "^4.4.5", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-M/+LBHTPKZxxXckM6m4dnJeR+jlm9NynH9b2YDswN4Zj2St05SK/crdL3Wy3WfJTZootnnhm3oTh87Usl7PS7w=="],
|
||||
"@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.16", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/nested-clients": "^3.996.6", "@aws-sdk/token-providers": "3.1003.0", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-b9of7tQgERxgcEcwAFWvRe84ivw+Kw6b3jVuz/6LQzonkomiY5UoWfprkbjc8FSCQ2VjDqKTvIRA9F0KSQ025w=="],
|
||||
|
||||
"@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.15", "", { "dependencies": { "@aws-sdk/core": "^3.973.17", "@aws-sdk/nested-clients": "^3.996.5", "@aws-sdk/types": "^3.973.4", "@smithy/property-provider": "^4.2.10", "@smithy/shared-ini-file-loader": "^4.4.5", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-QTH6k93v+UOfFam/ado8zc71tH+enTVyuvLy9uEWXX1x894dN5ovtf/MdBDgFwq3g6c9mbtgVJ4B+yBqDtXvdA=="],
|
||||
"@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.16", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/nested-clients": "^3.996.6", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-PaOH5jFoPQX4WkqpKzKh9cM7rieKtbgEGqrZ+ybGmotJhcvhI/xl69yCwMbHGnpQJJmHZIX9q2zaPB7HTBn/4w=="],
|
||||
|
||||
"@aws-sdk/eventstream-handler-node": ["@aws-sdk/eventstream-handler-node@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.4", "@smithy/eventstream-codec": "^4.2.10", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-mKPiiVssgFDWkAXdEDh8+wpr2pFSX/fBn2onXXnrfIAYbdZhYb4WilKbZ3SJMUnQi+Y48jZMam5J0RrgARluaA=="],
|
||||
"@aws-sdk/eventstream-handler-node": ["@aws-sdk/eventstream-handler-node@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/eventstream-codec": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-g2Z9s6Y4iNh0wICaEqutgYgt/Pmhv5Ev9G3eKGFe2w9VuZDhc76vYdop6I5OocmpHV79d4TuLG+JWg5rQIVDVA=="],
|
||||
|
||||
"@aws-sdk/middleware-eventstream": ["@aws-sdk/middleware-eventstream@3.972.6", "", { "dependencies": { "@aws-sdk/types": "^3.973.4", "@smithy/protocol-http": "^5.3.10", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-mB2+3G/oxRC+y9WRk0KCdradE2rSfxxJpcOSmAm+vDh3ex3WQHVLZ1catNIe1j5NQ+3FLBsNMRPVGkZ43PRpjw=="],
|
||||
"@aws-sdk/middleware-eventstream": ["@aws-sdk/middleware-eventstream@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-VWndapHYCfwLgPpCb/xwlMKG4imhFzKJzZcKOEioGn7OHY+6gdr0K7oqy1HZgbLa3ACznZ9fku+DzmAi8fUC0g=="],
|
||||
|
||||
"@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.6", "", { "dependencies": { "@aws-sdk/types": "^3.973.4", "@smithy/protocol-http": "^5.3.10", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-5XHwjPH1lHB+1q4bfC7T8Z5zZrZXfaLcjSMwTd1HPSPrCmPFMbg3UQ5vgNWcVj0xoX4HWqTGkSf2byrjlnRg5w=="],
|
||||
"@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-aHQZgztBFEpDU1BB00VWCIIm85JjGjQW1OG9+98BdmaOpguJvzmXBGbnAiYcciCd+IS4e9BEq664lhzGnWJHgQ=="],
|
||||
|
||||
"@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.6", "", { "dependencies": { "@aws-sdk/types": "^3.973.4", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-iFnaMFMQdljAPrvsCVKYltPt2j40LQqukAbXvW7v0aL5I+1GO7bZ/W8m12WxW3gwyK5p5u1WlHg8TSAizC5cZw=="],
|
||||
"@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-LXhiWlWb26txCU1vcI9PneESSeRp/RYY/McuM4SpdrimQR5NgwaPb4VJCadVeuGWgh6QmqZ6rAKSoL1ob16W6w=="],
|
||||
|
||||
"@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.6", "", { "dependencies": { "@aws-sdk/types": "^3.973.4", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.10", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-dY4v3of5EEMvik6+UDwQ96KfUFDk8m1oZDdkSc5lwi4o7rFrjnv0A+yTV+gu230iybQZnKgDLg/rt2P3H+Vscw=="],
|
||||
"@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-l2VQdcBcYLzIzykCHtXlbpiVCZ94/xniLIkAj0jpnpjY4xlgZx7f56Ypn+uV1y3gG0tNVytJqo3K9bfMFee7SQ=="],
|
||||
|
||||
"@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.17", "", { "dependencies": { "@aws-sdk/core": "^3.973.17", "@aws-sdk/types": "^3.973.4", "@aws-sdk/util-endpoints": "^3.996.3", "@smithy/core": "^3.23.7", "@smithy/protocol-http": "^5.3.10", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-HHArkgWzomuwufXwheQqkddu763PWCpoNTq1dGjqXzJT/lojX3VlOqjNSR2Xvb6/T9ISfwYcMOcbFgUp4EWxXA=="],
|
||||
"@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.18", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@smithy/core": "^3.23.8", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-KcqQDs/7WtoEnp52+879f8/i1XAJkgka5i4arOtOCPR10o4wWo3VRecDI9Gxoh6oghmLCnIiOSKyRcXI/50E+w=="],
|
||||
|
||||
"@aws-sdk/middleware-websocket": ["@aws-sdk/middleware-websocket@3.972.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.4", "@aws-sdk/util-format-url": "^3.972.6", "@smithy/eventstream-codec": "^4.2.10", "@smithy/eventstream-serde-browser": "^4.2.10", "@smithy/fetch-http-handler": "^5.3.12", "@smithy/protocol-http": "^5.3.10", "@smithy/signature-v4": "^5.3.10", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.1", "@smithy/util-hex-encoding": "^4.2.1", "@smithy/util-utf8": "^4.2.1", "tslib": "^2.6.2" } }, "sha512-cWf+8iUUnitgFuUu/ryK2uVfx7f5ezdhGwsjLLEEC1Nk716Ld2Hw4LA8iipyVcQI3EarvK6ExY2dSBET/0PYng=="],
|
||||
"@aws-sdk/middleware-websocket": ["@aws-sdk/middleware-websocket@3.972.12", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-format-url": "^3.972.7", "@smithy/eventstream-codec": "^4.2.11", "@smithy/eventstream-serde-browser": "^4.2.11", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-iyPP6FVDKe/5wy5ojC0akpDFG1vX3FeCUU47JuwN8xfvT66xlEI8qUJZPtN55TJVFzzWZJpWL78eqUE31md08Q=="],
|
||||
|
||||
"@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.5", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.17", "@aws-sdk/middleware-host-header": "^3.972.6", "@aws-sdk/middleware-logger": "^3.972.6", "@aws-sdk/middleware-recursion-detection": "^3.972.6", "@aws-sdk/middleware-user-agent": "^3.972.17", "@aws-sdk/region-config-resolver": "^3.972.6", "@aws-sdk/types": "^3.973.4", "@aws-sdk/util-endpoints": "^3.996.3", "@aws-sdk/util-user-agent-browser": "^3.972.6", "@aws-sdk/util-user-agent-node": "^3.973.2", "@smithy/config-resolver": "^4.4.9", "@smithy/core": "^3.23.7", "@smithy/fetch-http-handler": "^5.3.12", "@smithy/hash-node": "^4.2.10", "@smithy/invalid-dependency": "^4.2.10", "@smithy/middleware-content-length": "^4.2.10", "@smithy/middleware-endpoint": "^4.4.21", "@smithy/middleware-retry": "^4.4.38", "@smithy/middleware-serde": "^4.2.11", "@smithy/middleware-stack": "^4.2.10", "@smithy/node-config-provider": "^4.3.10", "@smithy/node-http-handler": "^4.4.13", "@smithy/protocol-http": "^5.3.10", "@smithy/smithy-client": "^4.12.1", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.10", "@smithy/util-base64": "^4.3.1", "@smithy/util-body-length-browser": "^4.2.1", "@smithy/util-body-length-node": "^4.2.2", "@smithy/util-defaults-mode-browser": "^4.3.37", "@smithy/util-defaults-mode-node": "^4.2.40", "@smithy/util-endpoints": "^3.3.1", "@smithy/util-middleware": "^4.2.10", "@smithy/util-retry": "^4.2.10", "@smithy/util-utf8": "^4.2.1", "tslib": "^2.6.2" } }, "sha512-zn0WApcULn7Rtl6T+KP2CQTZo/7wOa2YV1yHQnbijTQoi4YXQHM8s21JcJzt33/mqPh8AdvWX1f+83KvKuxlZw=="],
|
||||
"@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.6", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.18", "@aws-sdk/middleware-host-header": "^3.972.7", "@aws-sdk/middleware-logger": "^3.972.7", "@aws-sdk/middleware-recursion-detection": "^3.972.7", "@aws-sdk/middleware-user-agent": "^3.972.18", "@aws-sdk/region-config-resolver": "^3.972.7", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@aws-sdk/util-user-agent-browser": "^3.972.7", "@aws-sdk/util-user-agent-node": "^3.973.3", "@smithy/config-resolver": "^4.4.10", "@smithy/core": "^3.23.8", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/hash-node": "^4.2.11", "@smithy/invalid-dependency": "^4.2.11", "@smithy/middleware-content-length": "^4.2.11", "@smithy/middleware-endpoint": "^4.4.22", "@smithy/middleware-retry": "^4.4.39", "@smithy/middleware-serde": "^4.2.12", "@smithy/middleware-stack": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/node-http-handler": "^4.4.14", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.38", "@smithy/util-defaults-mode-node": "^4.2.41", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-blNJ3ugn4gCQ9ZSZi/firzKCvVl5LvPFVxv24LprENeWI4R8UApG006UQkF4SkmLygKq2BQXRad2/anQ13Te4Q=="],
|
||||
|
||||
"@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.6", "", { "dependencies": { "@aws-sdk/types": "^3.973.4", "@smithy/config-resolver": "^4.4.9", "@smithy/node-config-provider": "^4.3.10", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-Aa5PusHLXAqLTX1UKDvI3pHQJtIsF7Q+3turCHqfz/1F61/zDMWfbTC8evjhrrYVAtz9Vsv3SJ/waSUeu7B6gw=="],
|
||||
"@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/config-resolver": "^4.4.10", "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-/Ev/6AI8bvt4HAAptzSjThGUMjcWaX3GX8oERkB0F0F9x2dLSBdgFDiyrRz3i0u0ZFZFQ1b28is4QhyqXTUsVA=="],
|
||||
|
||||
"@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1002.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.17", "@aws-sdk/nested-clients": "^3.996.5", "@aws-sdk/types": "^3.973.4", "@smithy/property-provider": "^4.2.10", "@smithy/shared-ini-file-loader": "^4.4.5", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-x972uKOydFn4Rb0PZJzLdNW59rH0KWC78Q2JbQzZpGlGt0DxjYdDRwBG6F42B1MyaEwHGqO/tkGc4r3/PRFfMw=="],
|
||||
"@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1003.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/nested-clients": "^3.996.6", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-SOyyWNdT7njKRwtZ1JhwHlH1csv6Pkgf305X96/OIfnhq1pU/EjmT6W6por57rVrjrKuHBuEIXgpWv8OgoMHpg=="],
|
||||
|
||||
"@aws-sdk/types": ["@aws-sdk/types@3.973.4", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-RW60aH26Bsc016Y9B98hC0Plx6fK5P2v/iQYwMzrSjiDh1qRMUCP6KrXHYEHe3uFvKiOC93Z9zk4BJsUi6Tj1Q=="],
|
||||
"@aws-sdk/types": ["@aws-sdk/types@3.973.5", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ=="],
|
||||
|
||||
"@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.4", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.10", "@smithy/util-endpoints": "^3.3.1", "tslib": "^2.6.2" } }, "sha512-yWIQSNiCjykLL+ezN5A+DfBb1gfXTytBxm57e64lYmwxDHNmInYHRJYYRAGWG1o77vKEiWaw4ui28e3yb1k5aQ=="],
|
||||
"@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.4", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-endpoints": "^3.3.2", "tslib": "^2.6.2" } }, "sha512-Hek90FBmd4joCFj+Vc98KLJh73Zqj3s2W56gjAcTkrNLMDI5nIFkG9YpfcJiVI1YlE2Ne1uOQNe+IgQ/Vz2XRA=="],
|
||||
|
||||
"@aws-sdk/util-format-url": ["@aws-sdk/util-format-url@3.972.6", "", { "dependencies": { "@aws-sdk/types": "^3.973.4", "@smithy/querystring-builder": "^4.2.10", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-0YNVNgFyziCejXJx0rzxPiD2rkxTWco4c9wiMF6n37Tb9aQvIF8+t7GyEyIFCwQHZ0VMQaAl+nCZHOYz5I5EKw=="],
|
||||
"@aws-sdk/util-format-url": ["@aws-sdk/util-format-url@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/querystring-builder": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-V+PbnWfUl93GuFwsOHsAq7hY/fnm9kElRqR8IexIJr5Rvif9e614X5sGSyz3mVSf1YAZ+VTy63W1/pGdA55zyA=="],
|
||||
|
||||
"@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.4", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-H1onv5SkgPBK2P6JR2MjGgbOnttoNzSPIRoeZTNPZYyaplwGg50zS3amXvXqF0/qfXpWEC9rLWU564QTB9bSog=="],
|
||||
"@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.5", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ=="],
|
||||
|
||||
"@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.6", "", { "dependencies": { "@aws-sdk/types": "^3.973.4", "@smithy/types": "^4.13.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-Fwr/llD6GOrFgQnKaI2glhohdGuBDfHfora6iG9qsBBBR8xv1SdCSwbtf5CWlUdCw5X7g76G/9Hf0Inh0EmoxA=="],
|
||||
"@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7SJVuvhKhMF/BkNS1n0QAJYgvEwYbK2QLKBrzDiwQGiTRU6Yf1f3nehTzm/l21xdAOtWSfp2uWSddPnP2ZtsVw=="],
|
||||
|
||||
"@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.2", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.17", "@aws-sdk/types": "^3.973.4", "@smithy/node-config-provider": "^4.3.10", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-lpaIuekdkpw7VRiik0IZmd6TyvEUcuLgKZ5fKRGpCA3I4PjrD/XH15sSwW+OptxQjNU4DEzSxag70spC9SluvA=="],
|
||||
"@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.3", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.18", "@aws-sdk/types": "^3.973.5", "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-8s2cQmTUOwcBlIJyI9PAZNnnnF+cGtdhHc1yzMMsSD/GR/Hxj7m0IGUE92CslXXb8/p5Q76iqOCjN1GFwyf+1A=="],
|
||||
|
||||
"@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.9", "", { "dependencies": { "@smithy/types": "^4.13.0", "fast-xml-parser": "5.4.1", "tslib": "^2.6.2" } }, "sha512-ItnlMgSqkPrUfJs7EsvU/01zw5UeIb2tNPhD09LBLHbg+g+HDiKibSLwpkuz/ZIlz4F2IMn+5XgE4AK/pfPuog=="],
|
||||
"@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.10", "", { "dependencies": { "@smithy/types": "^4.13.0", "fast-xml-parser": "5.4.1", "tslib": "^2.6.2" } }, "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA=="],
|
||||
|
||||
"@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.3", "", {}, "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw=="],
|
||||
|
||||
@@ -263,23 +263,23 @@
|
||||
|
||||
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||
|
||||
"@biomejs/biome": ["@biomejs/biome@2.4.5", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.5", "@biomejs/cli-darwin-x64": "2.4.5", "@biomejs/cli-linux-arm64": "2.4.5", "@biomejs/cli-linux-arm64-musl": "2.4.5", "@biomejs/cli-linux-x64": "2.4.5", "@biomejs/cli-linux-x64-musl": "2.4.5", "@biomejs/cli-win32-arm64": "2.4.5", "@biomejs/cli-win32-x64": "2.4.5" }, "bin": { "biome": "bin/biome" } }, "sha512-OWNCyMS0Q011R6YifXNOg6qsOg64IVc7XX6SqGsrGszPbkVCoaO7Sr/lISFnXZ9hjQhDewwZ40789QmrG0GYgQ=="],
|
||||
"@biomejs/biome": ["@biomejs/biome@2.4.6", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.6", "@biomejs/cli-darwin-x64": "2.4.6", "@biomejs/cli-linux-arm64": "2.4.6", "@biomejs/cli-linux-arm64-musl": "2.4.6", "@biomejs/cli-linux-x64": "2.4.6", "@biomejs/cli-linux-x64-musl": "2.4.6", "@biomejs/cli-win32-arm64": "2.4.6", "@biomejs/cli-win32-x64": "2.4.6" }, "bin": { "biome": "bin/biome" } }, "sha512-QnHe81PMslpy3mnpL8DnO2M4S4ZnYPkjlGCLWBZT/3R9M6b5daArWMMtEfP52/n174RKnwRIf3oT8+wc9ihSfQ=="],
|
||||
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lGS4Nd5O3KQJ6TeWv10mElnx1phERhBxqGP/IKq0SvZl78kcWDFMaTtVK+w3v3lusRFxJY78n07PbKplirsU5g=="],
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NW18GSyxr+8sJIqgoGwVp5Zqm4SALH4b4gftIA0n62PTuBs6G2tHlwNAOj0Vq0KKSs7Sf88VjjmHh0O36EnzrQ=="],
|
||||
|
||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-6MoH4tyISIBNkZ2Q5T1R7dLd5BsITb2yhhhrU9jHZxnNSNMWl+s2Mxu7NBF8Y3a7JJcqq9nsk8i637z4gqkJxQ=="],
|
||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-4uiE/9tuI7cnjtY9b07RgS7gGyYOAfIAGeVJWEfeCnAarOAS7qVmuRyX6d7JTKw28/mt+rUzMasYeZ+0R/U1Mw=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-U1GAG6FTjhAO04MyH4xn23wRNBkT6H7NentHh+8UxD6ShXKBm5SY4RedKJzkUThANxb9rUKIPc7B8ew9Xo/cWg=="],
|
||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-kMLaI7OF5GN1Q8Doymjro1P8rVEoy7BKQALNz6fiR8IC1WKduoNyteBtJlHT7ASIL0Cx2jR6VUOBIbcB1B8pew=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-iqLDgpzobG7gpBF0fwEVS/LT8kmN7+S0E2YKFDtqliJfzNLnAiV2Nnyb+ehCDCJgAZBASkYHR2o60VQWikpqIg=="],
|
||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-F/JdB7eN22txiTqHM5KhIVt0jVkzZwVYrdTR1O3Y4auBOQcXxHK4dxULf4z43QyZI5tsnQJrRBHZy7wwtL+B3A=="],
|
||||
|
||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-NdODlSugMzTlENPTa4z0xB82dTUlCpsrOxc43///aNkTLblIYH4XpYflBbf5ySlQuP8AA4AZd1qXhV07IdrHdQ=="],
|
||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.6", "", { "os": "linux", "cpu": "x64" }, "sha512-oHXmUFEoH8Lql1xfc3QkFLiC1hGR7qedv5eKNlC185or+o4/4HiaU7vYODAH3peRCfsuLr1g6v2fK9dFFOYdyw=="],
|
||||
|
||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-NlKa7GpbQmNhZf9kakQeddqZyT7itN7jjWdakELeXyTU3pg/83fTysRRDPJD0akTfKDl6vZYNT9Zqn4MYZVBOA=="],
|
||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.6", "", { "os": "linux", "cpu": "x64" }, "sha512-C9s98IPDu7DYarjlZNuzJKTjVHN03RUnmHV5htvqsx6vEUXCDSJ59DNwjKVD5XYoSS4N+BYhq3RTBAL8X6svEg=="],
|
||||
|
||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-EBfrTqRIWOFSd7CQb/0ttjHMR88zm3hGravnDwUA9wHAaCAYsULKDebWcN5RmrEo1KBtl/gDVJMrFjNR0pdGUw=="],
|
||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-xzThn87Pf3YrOGTEODFGONmqXpTwUNxovQb72iaUOdcw8sBSY3+3WD8Hm9IhMYLnPi0n32s3L3NWU6+eSjfqFg=="],
|
||||
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.5", "", { "os": "win32", "cpu": "x64" }, "sha512-Pmhv9zT95YzECfjEHNl3mN9Vhusw9VA5KHY0ZvlGsxsjwS5cb7vpRnHzJIv0vG7jB0JI7xEaMH9ddfZm/RozBw=="],
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.6", "", { "os": "win32", "cpu": "x64" }, "sha512-7++XhnsPlr1HDbor5amovPjOH6vsrFOCdp93iKXhFn6bcMUI6soodj3WWKfgEO6JosKU1W5n3uky3WW9RlRjTg=="],
|
||||
|
||||
"@bufbuild/protobuf": ["@bufbuild/protobuf@2.11.0", "", {}, "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ=="],
|
||||
|
||||
@@ -455,7 +455,7 @@
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
|
||||
|
||||
"@types/node": ["@types/node@25.3.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ=="],
|
||||
"@types/node": ["@types/node@25.3.5", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
|
||||
|
||||
@@ -467,21 +467,21 @@
|
||||
|
||||
"@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
|
||||
|
||||
"@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260304.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260304.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260304.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260304.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260304.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260304.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260304.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260304.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-Xj0ZeHEy+yJ/bIg6psPwl0POvBf1j5u7IZAXsUqgvgWbMIvdM9JOGmhpifcj6j28LcXM6GTvXUoXwlatxJ73Qg=="],
|
||||
"@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260306.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260306.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260306.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260306.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260306.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260306.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260306.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260306.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-4m7cOjtKu+iLazWW5MuJuI2ZZMkQkS42+GxN6FVdja1nL0t47l1wpaTnzUa1Ny9Xa0opIJ7psPAMBKYAPKbCKA=="],
|
||||
|
||||
"@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260304.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TnTUxYt+dShRSoeOldx7VlKoEG+bvPHnyPEBImlNc7c3WP0AHYyNHrNg6EbLbzkOorARtd06J3Vk+XYzkrRzZg=="],
|
||||
"@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260306.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4vuh4VlPydMS/nymDzjJIKDk3dntnEEB5UzyJV9mM4kxF5+geFgJih1DTtZS3qVafhHLB3e4l8omtvGftMnb8g=="],
|
||||
|
||||
"@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260304.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1nwXX1zbyYI3sDKdaR8NsBdM7LmE0J6OzVtlWgEJ/8YR7oC2/HY6/SfShF3DHHcEOHOFxRLbkJ9zVTJJspWLCw=="],
|
||||
"@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260306.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-qxYfv0aM4KCZPEe584KIjT5sO4uR+xdyuQXX5tXbnH1UoksIz7bvJ9KUgRloS/q/ww0f8UjPS2+27LnRA4y7ig=="],
|
||||
|
||||
"@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260304.1", "", { "os": "linux", "cpu": "arm" }, "sha512-TXZClCJVteK2f9gcI+I7o1Sxgq3qdMtraXOP9GZF8o0sKCLdDWENN8uORfZSeQv2qOJohcKvrrEz6LLSSngvEg=="],
|
||||
"@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260306.1", "", { "os": "linux", "cpu": "arm" }, "sha512-8gRAFx0ExDWHOmphl8mzBrSoGWnLWDU4VpxkPRsWqaJpHVbjr9Yk2QkuJNIaDmF6q44eJmW/huSiObmHTbZ1UQ=="],
|
||||
|
||||
"@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260304.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-cw+xqroXtsk/yVTKbelcPWMd6oZdET9kNWmigyc189KWwzOu2eq2EPXPQsrhEigq8O3j0xW0z3q2oqG+smOiXg=="],
|
||||
"@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260306.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-8G0BKvTkE+eKX1tSnyKeDaf3bWPWY7OI77SMipagCAyYi06v4gxx+IVE3Px7W7kLX2Wqp1MjWDXu2N76wfJtXQ=="],
|
||||
|
||||
"@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260304.1", "", { "os": "linux", "cpu": "x64" }, "sha512-EXufnN4PG0HYBHYbHXQXXRXtaQKuKBT3e6nxPhKnwpBBgy2MgWDIxzroTLvI9+SllhbJQzHNZOWiB+SU+KdCNw=="],
|
||||
"@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260306.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rsJV3Z9J/zYCEtcqvm+WfLAml3i1OAyMEUn0hja7i8C0kzE+tXKXzsJ0+I1TrSU5O7hHvqlLTvueBoCoM4aL4g=="],
|
||||
|
||||
"@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260304.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-Be9yyDDbT/PEdNlhG+NXT47fwuiIeN0+/9BkeRKkiLgzY8DqQIC9w5FRWmwAJ+9PVa2sKr5cjD1SpJDHGrPIrA=="],
|
||||
"@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260306.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-US1WsIu9IukaFzM+w8wt0fIAkmk2WtxeVuk8nkbrnH9S3ax39r0J4ikMNZSXEJE0VMxhXJoymzfWxhj3s9yW/Q=="],
|
||||
|
||||
"@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260304.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lg/w+rZ9NIUoqSsk2TbtDsqyD9nW0/rhTMYd14RFP7vuNijLrTbl7GPiMhFtMxaqCSOFapwbql7/3lU4BKHB6g=="],
|
||||
"@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260306.1", "", { "os": "win32", "cpu": "x64" }, "sha512-MlneT0RWS9Zdb8XoWvHsUgmnMJu6K3S0BXRu5ZgUYjcbQKlkz+Z87aUB8eX8qnDFd9csJcMp3+ZrgQ/LKVGP1g=="],
|
||||
|
||||
"@typescript/vfs": ["@typescript/vfs@1.6.4", "", { "dependencies": { "debug": "^4.4.3" }, "peerDependencies": { "typescript": "*" } }, "sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ=="],
|
||||
|
||||
@@ -821,7 +821,7 @@
|
||||
|
||||
"onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
|
||||
|
||||
"openai": ["openai@6.25.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-mEh6VZ2ds2AGGokWARo18aPISI1OhlgdEIC1ewhkZr8pSIT31dec0ecr9Nhxx0JlybyOgoAT1sWeKtwPZzJyww=="],
|
||||
"openai": ["openai@6.27.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-osTKySlrdYrLYTt0zjhY8yp0JUBmWDCN+Q+QxsV4xMQnnoVFpylgKGgxwN8sSdTNw0G4y+WUXs4eCMWpyDNWZQ=="],
|
||||
|
||||
"p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="],
|
||||
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Exported `ThinkingLevel` selector constants and types for configuring agent reasoning behavior
|
||||
- Added `inherit` thinking level option to defer reasoning configuration to higher-level selectors
|
||||
- Added `serviceTier` option to configure service tier for agent requests
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed `thinkingLevel` from required string to optional `Effort` type, allowing undefined state
|
||||
- Updated `setThinkingLevel()` method to accept `Effort | undefined` instead of `ThinkingLevel` string
|
||||
|
||||
## [13.4.0] - 2026-03-01
|
||||
### Added
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
type AssistantMessage,
|
||||
type CursorExecHandlers,
|
||||
type CursorToolResultHandler,
|
||||
type Effort,
|
||||
getBundledModel,
|
||||
type ImageContent,
|
||||
type Message,
|
||||
@@ -14,7 +15,6 @@ import {
|
||||
streamSimple,
|
||||
type TextContent,
|
||||
type ThinkingBudgets,
|
||||
type ThinkingLevel,
|
||||
type ToolChoice,
|
||||
type ToolResultMessage,
|
||||
} from "@oh-my-pi/pi-ai";
|
||||
@@ -175,7 +175,7 @@ export class Agent {
|
||||
#state: AgentState = {
|
||||
systemPrompt: "",
|
||||
model: getBundledModel("google", "gemini-2.5-flash-lite-preview-06-17"),
|
||||
thinkingLevel: "off",
|
||||
thinkingLevel: undefined,
|
||||
tools: [],
|
||||
messages: [],
|
||||
isStreaming: false,
|
||||
@@ -416,7 +416,7 @@ export class Agent {
|
||||
this.#state.model = m;
|
||||
}
|
||||
|
||||
setThinkingLevel(l: ThinkingLevel) {
|
||||
setThinkingLevel(l: Effort | undefined) {
|
||||
this.#state.thinkingLevel = l;
|
||||
}
|
||||
|
||||
@@ -669,7 +669,7 @@ export class Agent {
|
||||
// Clear Cursor tool result buffer at start of each run
|
||||
this.#cursorToolResultBuffer = [];
|
||||
|
||||
const reasoning = this.#state.thinkingLevel === "off" ? undefined : this.#state.thinkingLevel;
|
||||
const reasoning = this.#state.thinkingLevel;
|
||||
|
||||
const context: AgentContext = {
|
||||
systemPrompt: this.#state.systemPrompt,
|
||||
|
||||
@@ -4,5 +4,7 @@ export * from "./agent";
|
||||
export * from "./agent-loop";
|
||||
// Proxy utilities
|
||||
export * from "./proxy";
|
||||
// Thinking selectors
|
||||
export * from "./thinking";
|
||||
// Types
|
||||
export * from "./types";
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Effort } from "@oh-my-pi/pi-ai";
|
||||
|
||||
/**
|
||||
* Agent-local thinking selector.
|
||||
*
|
||||
* `off` disables reasoning, while `inherit` defers to a higher-level selector.
|
||||
*/
|
||||
export const ThinkingLevel = {
|
||||
Inherit: "inherit",
|
||||
Off: "off",
|
||||
Minimal: Effort.Minimal,
|
||||
Low: Effort.Low,
|
||||
Medium: Effort.Medium,
|
||||
High: Effort.High,
|
||||
XHigh: Effort.XHigh,
|
||||
} as const;
|
||||
|
||||
export type ThinkingLevel = (typeof ThinkingLevel)[keyof typeof ThinkingLevel];
|
||||
export type ResolvedThinkingLevel = Exclude<ThinkingLevel, "inherit">;
|
||||
@@ -1,13 +1,13 @@
|
||||
import type {
|
||||
AssistantMessageEvent,
|
||||
AssistantMessageEventStream,
|
||||
Effort,
|
||||
ImageContent,
|
||||
Message,
|
||||
Model,
|
||||
SimpleStreamOptions,
|
||||
streamSimple,
|
||||
TextContent,
|
||||
ThinkingLevel,
|
||||
Tool,
|
||||
ToolChoice,
|
||||
ToolResultMessage,
|
||||
@@ -171,7 +171,7 @@ export type AgentMessage = Message | CustomAgentMessages[keyof CustomAgentMessag
|
||||
export interface AgentState {
|
||||
systemPrompt: string;
|
||||
model: Model;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
thinkingLevel?: Effort;
|
||||
tools: AgentTool<any>[];
|
||||
messages: AgentMessage[]; // Can include attachments + custom message types
|
||||
isStreaming: boolean;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { Agent } from "@oh-my-pi/pi-agent-core";
|
||||
import { Agent, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import { type AssistantMessage, getBundledModel, type ThinkingBudgets, type Usage } from "@oh-my-pi/pi-ai";
|
||||
import { AssistantMessageEventStream } from "@oh-my-pi/pi-ai/utils/event-stream";
|
||||
|
||||
@@ -39,7 +39,7 @@ describe("Agent", () => {
|
||||
expect(agent.state).toBeDefined();
|
||||
expect(agent.state.systemPrompt).toBe("");
|
||||
expect(agent.state.model).toBeDefined();
|
||||
expect(agent.state.thinkingLevel).toBe("off");
|
||||
expect(agent.state.thinkingLevel).toBeUndefined();
|
||||
expect(agent.state.tools).toEqual([]);
|
||||
expect(agent.state.messages).toEqual([]);
|
||||
expect(agent.state.isStreaming).toBe(false);
|
||||
@@ -54,13 +54,13 @@ describe("Agent", () => {
|
||||
initialState: {
|
||||
systemPrompt: "You are a helpful assistant.",
|
||||
model: customModel,
|
||||
thinkingLevel: "low",
|
||||
thinkingLevel: ThinkingLevel.Low,
|
||||
},
|
||||
});
|
||||
|
||||
expect(agent.state.systemPrompt).toBe("You are a helpful assistant.");
|
||||
expect(agent.state.model).toBe(customModel);
|
||||
expect(agent.state.thinkingLevel).toBe("low");
|
||||
expect(agent.state.thinkingLevel).toBe(ThinkingLevel.Low);
|
||||
});
|
||||
|
||||
it("should subscribe to events", () => {
|
||||
@@ -98,8 +98,8 @@ describe("Agent", () => {
|
||||
expect(agent.state.model).toBe(newModel);
|
||||
|
||||
// Test setThinkingLevel
|
||||
agent.setThinkingLevel("high");
|
||||
expect(agent.state.thinkingLevel).toBe("high");
|
||||
agent.setThinkingLevel(ThinkingLevel.High);
|
||||
expect(agent.state.thinkingLevel).toBe(ThinkingLevel.High);
|
||||
|
||||
// Test setTools
|
||||
const tools = [{ name: "test", description: "test tool" } as any];
|
||||
|
||||
@@ -1,8 +1,26 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
### Breaking Changes
|
||||
|
||||
- Changed `reasoning` parameter from `ThinkingLevel | undefined` to `Effort | undefined` in `SimpleStreamOptions`; 'off' is no longer valid (omit the field instead)
|
||||
- Removed `supportsXhigh()` function; check `model.thinking?.maxLevel` instead
|
||||
- Removed `ThinkingLevel` and `ThinkingEffort` types; use `Effort` enum
|
||||
- Removed `getAvailableThinkingLevels()` and `getAvailableThinkingEfforts()` functions
|
||||
- Changed `transformRequestBody()` signature to require `Model` parameter as second argument for effort validation
|
||||
- Removed `thinking.ts` module export; import from `model-thinking.ts` instead
|
||||
|
||||
### Added
|
||||
|
||||
- Added `ThinkingConfig` interface to models for canonical thinking transport metadata with min/max effort levels and provider-specific mode
|
||||
- Added `thinking` field to `Model` type containing per-model thinking capabilities used to clamp and map user-facing effort levels
|
||||
- Added `Effort` enum (minimal, low, medium, high, xhigh) as canonical user-facing thinking levels replacing `ThinkingLevel`
|
||||
- Added `enrichModelThinking()` function to automatically populate thinking metadata on models based on their capabilities
|
||||
- Added `mapEffortToAnthropicAdaptiveEffort()` function to map user effort levels to Anthropic adaptive thinking effort
|
||||
- Added `mapEffortToGoogleThinkingLevel()` function to map user effort levels to Google thinking levels
|
||||
- Added `requireSupportedEffort()` function to validate and clamp effort levels per model, throwing errors for unsupported combinations
|
||||
- Added `clampThinkingLevelForModel()` function to clamp thinking levels to model-supported range
|
||||
- Added `applyGeneratedModelPolicies()` and `linkSparkPromotionTargets()` exports from model-thinking module
|
||||
- Added `serviceTier` option to control OpenAI processing priority and cost (auto, default, flex, scale, priority)
|
||||
- Added `providerPayload` field to messages and responses for reconstructing transport-native history
|
||||
- Added Gemini usage provider for tracking quota and tier information
|
||||
@@ -11,6 +29,14 @@
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed `reasoning` parameter type from `ThinkingLevel` to `Effort` in `SimpleStreamOptions`, removing 'off' value (callers should omit the field instead)
|
||||
- Changed thinking configuration to use model-specific metadata instead of hardcoded provider logic for effort mapping
|
||||
- Changed OpenAI Codex request transformer to accept `Model` parameter for effort validation instead of string model ID
|
||||
- Changed Anthropic provider to use model thinking metadata for determining adaptive thinking support instead of model ID pattern matching
|
||||
- Changed Google Vertex and Google providers to use shorter variable names for thinking config construction
|
||||
- Moved thinking-related utilities from `thinking.ts` to new `model-thinking.ts` module with expanded functionality
|
||||
- Moved model policy functions from `provider-models/model-policies.ts` to `model-thinking.ts`
|
||||
- Moved `googleGeminiCliUsageProvider` from `providers/google-gemini-cli-usage.ts` to `usage/gemini.ts`
|
||||
- Changed default OpenAI model from gpt-5.1-codex to gpt-5.4 across all providers
|
||||
- Changed `UsageFetchContext` to remove cache and now() dependencies—usage fetchers now use Date.now() directly
|
||||
- Removed `resetInMs` field from usage windows; consumers should calculate from `resetsAt` timestamp
|
||||
@@ -19,6 +45,13 @@
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed `thinking.ts` module; use `model-thinking.ts` instead
|
||||
- Removed `provider-models/model-policies.ts` module; functionality moved to `model-thinking.ts`
|
||||
- Removed `supportsXhigh()` function from models.ts; use model.thinking metadata instead
|
||||
- Removed `ThinkingLevel` and `ThinkingEffort` types; use `Effort` enum instead
|
||||
- Removed `getAvailableThinkingLevels()` and `getAvailableThinkingEfforts()` functions
|
||||
- Removed `model-policies` export from `provider-models/index.ts`
|
||||
- Removed hardcoded thinking level clamping logic from OpenAI Codex request transformer; now uses model metadata
|
||||
- Removed `UsageCache` and `UsageCacheEntry` interfaces—caching is now handled internally by AuthStorage
|
||||
- Removed `google-gemini-cli-usage` export; use new `gemini` usage provider instead
|
||||
- Removed `resetInMs` computation from all usage providers
|
||||
@@ -26,6 +59,9 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed OpenAI Codex to reject unsupported effort levels instead of silently clamping them, providing clear error messages about supported efforts
|
||||
- Fixed model cache normalization to properly apply thinking enrichment when loading cached models
|
||||
- Fixed dynamic model merging to apply thinking enrichment to merged model results
|
||||
- Fixed OpenAI Codex streaming to properly include service_tier in SSE payloads
|
||||
- Fixed type safety in OpenAI responses by removing unsafe type casts on image content blocks
|
||||
- Fixed credential purging to respect disabled credentials when deduplicating by email
|
||||
@@ -47,31 +83,33 @@
|
||||
- Fixed Unicode normalization to consistently apply `toWellFormed()` to all text content, including thinking blocks, ensuring proper handling of malformed UTF-16 sequences
|
||||
|
||||
## [13.9.1] - 2026-03-05
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Removed `THINKING_LEVELS`, `ALL_THINKING_LEVELS`, `ALL_THINKING_MODES`, `THINKING_MODE_DESCRIPTIONS`, and `THINKING_MODE_LABELS` exports
|
||||
- Renamed `formatThinking()` to `getThinkingMetadata()` with changed return type from string to `ThinkingMetadata` object
|
||||
- Renamed `getAvailableThinkingLevel()` to `getAvailableThinkingLevels()` and added default parameter
|
||||
- Renamed `getAvailableThinkingEffort()` to `getAvailableThinkingEfforts()` and added default parameter
|
||||
- Renamed `getAvailableEffort()` to `getAvailableEfforts()` and added default parameter
|
||||
|
||||
### Added
|
||||
|
||||
- Added `ThinkingMetadata` type to provide structured access to thinking mode information (value, label, description)
|
||||
|
||||
## [13.9.0] - 2026-03-05
|
||||
|
||||
### Added
|
||||
|
||||
- Exported new thinking module with `ThinkingEffort`, `ThinkingLevel`, and `ThinkingMode` types for managing reasoning effort levels
|
||||
- Added `getAvailableThinkingEffort()` function to determine supported thinking effort levels based on model capabilities
|
||||
- Added `parseThinkingEffort()`, `parseThinkingLevel()`, and `parseThinkingMode()` functions for parsing thinking configuration strings
|
||||
- Exported new thinking module with `Effort`, `ThinkingLevel`, and `ThinkingMode` types for managing reasoning effort levels
|
||||
- Added `getAvailableEffort()` function to determine supported thinking effort levels based on model capabilities
|
||||
- Added `parseEffort()`, `parseThinkingLevel()`, and `parseThinkingMode()` functions for parsing thinking configuration strings
|
||||
- Added `THINKING_LEVELS`, `ALL_THINKING_LEVELS`, and `ALL_THINKING_MODES` constants for iterating over available thinking options
|
||||
- Added `THINKING_MODE_DESCRIPTIONS` and `THINKING_MODE_LABELS` for displaying thinking modes in user interfaces
|
||||
- Added `formatThinking()` function to format thinking modes as compact display labels
|
||||
|
||||
### Changed
|
||||
|
||||
- Refactored thinking level handling to distinguish between `ThinkingEffort` (provider-level, no "off") and `ThinkingLevel` (user-facing, includes "off")
|
||||
- Updated `ThinkingBudgets` type to use `ThinkingEffort` instead of `ThinkingLevel` for more precise token budget configuration
|
||||
- Refactored thinking level handling to distinguish between `Effort` (provider-level, no "off") and `ThinkingLevel` (user-facing, includes "off")
|
||||
- Updated `ThinkingBudgets` type to use `Effort` instead of `ThinkingLevel` for more precise token budget configuration
|
||||
- Improved reasoning option handling to explicitly support "off" value for disabling reasoning across all providers
|
||||
- Simplified thinking effort mapping logic by centralizing provider-specific clamping behavior
|
||||
|
||||
|
||||
@@ -12,6 +12,11 @@ import * as path from "node:path";
|
||||
import { $env } from "@oh-my-pi/pi-utils";
|
||||
import { AuthCredentialStore } from "../src/auth-storage";
|
||||
import { createModelManager } from "../src/model-manager";
|
||||
import {
|
||||
applyGeneratedModelPolicies,
|
||||
CLOUDFLARE_FALLBACK_MODEL,
|
||||
linkSparkPromotionTargets,
|
||||
} from "../src/model-thinking";
|
||||
import prevModelsJson from "../src/models.json" with { type: "json" };
|
||||
import {
|
||||
allowsUnauthenticatedCatalogDiscovery,
|
||||
@@ -20,11 +25,6 @@ import {
|
||||
isCatalogDescriptor,
|
||||
PROVIDER_DESCRIPTORS,
|
||||
} from "../src/provider-models/descriptors";
|
||||
import {
|
||||
applyGeneratedModelPolicies,
|
||||
CLOUDFLARE_FALLBACK_MODEL,
|
||||
linkSparkPromotionTargets,
|
||||
} from "../src/provider-models/model-policies";
|
||||
import { MODELS_DEV_PROVIDER_DESCRIPTORS, mapModelsDevToModels } from "../src/provider-models/openai-compat";
|
||||
import { getGitLabDuoModels } from "../src/providers/gitlab-duo";
|
||||
import { JWT_CLAIM_PATH } from "../src/providers/openai-codex/constants";
|
||||
|
||||
@@ -11,7 +11,6 @@ import { Database, type Statement } from "bun:sqlite";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
import { getAgentDir, logger } from "@oh-my-pi/pi-utils";
|
||||
import { googleGeminiCliUsageProvider } from "./providers/google-gemini-cli-usage";
|
||||
import { getEnvApiKey } from "./stream";
|
||||
import type { Provider } from "./types";
|
||||
import type {
|
||||
@@ -23,6 +22,7 @@ import type {
|
||||
UsageReport,
|
||||
} from "./usage";
|
||||
import { claudeRankingStrategy, claudeUsageProvider } from "./usage/claude";
|
||||
import { googleGeminiCliUsageProvider } from "./usage/gemini";
|
||||
import { githubCopilotUsageProvider } from "./usage/github-copilot";
|
||||
import { antigravityUsageProvider } from "./usage/google-antigravity";
|
||||
import { kimiUsageProvider } from "./usage/kimi";
|
||||
|
||||
@@ -4,6 +4,7 @@ export * from "./api-registry";
|
||||
export * from "./auth-storage";
|
||||
export * from "./model-cache";
|
||||
export * from "./model-manager";
|
||||
export * from "./model-thinking";
|
||||
export * from "./models";
|
||||
export * from "./provider-details";
|
||||
export * from "./provider-models";
|
||||
@@ -20,7 +21,6 @@ export * from "./providers/openai-responses";
|
||||
export * from "./providers/synthetic";
|
||||
export * from "./rate-limit-utils";
|
||||
export * from "./stream";
|
||||
export * from "./thinking";
|
||||
export * from "./types";
|
||||
export * from "./usage";
|
||||
export * from "./usage/claude";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { readModelCache, writeModelCache } from "./model-cache";
|
||||
import { enrichModelThinking } from "./model-thinking";
|
||||
import { type GeneratedProvider, getBundledModels } from "./models";
|
||||
import type { Api, Model, Provider } from "./types";
|
||||
import { isRecord } from "./utils";
|
||||
@@ -108,7 +109,7 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
||||
const shouldUseFreshCacheAsAuthoritative =
|
||||
strategy === "online-if-uncached" && (cache?.fresh ?? false) && hasAuthoritativeCache;
|
||||
const dynamicFetchSucceeded = fetchedDynamicModels !== null;
|
||||
const cacheModels = dynamicFetchSucceeded ? [] : (cache?.models ?? []);
|
||||
const cacheModels = dynamicFetchSucceeded ? [] : normalizeModelList<TApi>(cache?.models ?? []);
|
||||
const dynamicModels = fetchedDynamicModels ?? [];
|
||||
const mergedWithoutDynamic = mergeModelSources(staticModels, modelsDevModels, cacheModels);
|
||||
const models = mergeDynamicModels(mergedWithoutDynamic, dynamicModels);
|
||||
@@ -223,7 +224,7 @@ function mergeDynamicModels<TApi extends Api>(
|
||||
|
||||
function mergeDynamicModel<TApi extends Api>(existingModel: Model<TApi>, dynamicModel: Model<TApi>): Model<TApi> {
|
||||
const supportsImage = existingModel.input.includes("image") || dynamicModel.input.includes("image");
|
||||
return {
|
||||
return enrichModelThinking({
|
||||
...existingModel,
|
||||
...dynamicModel,
|
||||
name: preferDiscoveryName(dynamicModel.name, existingModel.name, dynamicModel.id),
|
||||
@@ -240,7 +241,7 @@ function mergeDynamicModel<TApi extends Api>(existingModel: Model<TApi>, dynamic
|
||||
headers: dynamicModel.headers ? { ...existingModel.headers, ...dynamicModel.headers } : existingModel.headers,
|
||||
compat: dynamicModel.compat ?? existingModel.compat,
|
||||
contextPromotionTarget: dynamicModel.contextPromotionTarget ?? existingModel.contextPromotionTarget,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function preferDiscoveryCost(discoveryCost: number, fallbackCost: number): number {
|
||||
@@ -278,7 +279,7 @@ function normalizeModelList<TApi extends Api>(value: unknown): Model<TApi>[] {
|
||||
const models: Model<TApi>[] = [];
|
||||
for (const item of value) {
|
||||
if (isModelLike(item)) {
|
||||
models.push(item as Model<TApi>);
|
||||
models.push(enrichModelThinking(item as Model<TApi>));
|
||||
}
|
||||
}
|
||||
return models;
|
||||
|
||||
@@ -0,0 +1,526 @@
|
||||
import type { Api, Model as ApiModel, ThinkingConfig } from "./types";
|
||||
|
||||
/** User-facing thinking levels, ordered least to most intensive. */
|
||||
export const enum Effort {
|
||||
Minimal = "minimal",
|
||||
Low = "low",
|
||||
Medium = "medium",
|
||||
High = "high",
|
||||
XHigh = "xhigh",
|
||||
}
|
||||
|
||||
export const THINKING_EFFORTS: readonly Effort[] = [
|
||||
Effort.Minimal,
|
||||
Effort.Low,
|
||||
Effort.Medium,
|
||||
Effort.High,
|
||||
Effort.XHigh,
|
||||
];
|
||||
|
||||
const DEFAULT_REASONING_EFFORTS: readonly Effort[] = [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High];
|
||||
const DEFAULT_REASONING_EFFORTS_WITH_XHIGH: readonly Effort[] = [
|
||||
Effort.Minimal,
|
||||
Effort.Low,
|
||||
Effort.Medium,
|
||||
Effort.High,
|
||||
Effort.XHigh,
|
||||
];
|
||||
const GEMINI_3_PRO_EFFORTS: readonly Effort[] = [Effort.Low, Effort.High];
|
||||
const GEMINI_3_FLASH_EFFORTS: readonly Effort[] = [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High];
|
||||
const GPT_5_2_PLUS_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh];
|
||||
const GPT_5_1_CODEX_MINI_EFFORTS: readonly Effort[] = [Effort.Medium, Effort.High];
|
||||
const CLOUDFLARE_AI_GATEWAY_BASE_URL = "https://gateway.ai.cloudflare.com/v1/<account>/<gateway>/anthropic";
|
||||
|
||||
type SemVer = {
|
||||
major: number;
|
||||
minor: number;
|
||||
patch: number;
|
||||
};
|
||||
|
||||
type GeminiKind = "pro" | "flash";
|
||||
type AnthropicKind = "opus" | "sonnet";
|
||||
type OpenAIVariant = "base" | "codex" | "codex-max" | "codex-mini" | "codex-spark" | "max" | "nano";
|
||||
|
||||
interface GeminiModel {
|
||||
family: "gemini";
|
||||
kind: GeminiKind;
|
||||
version: SemVer;
|
||||
}
|
||||
|
||||
interface AnthropicModel {
|
||||
family: "anthropic";
|
||||
kind: AnthropicKind;
|
||||
version: SemVer;
|
||||
}
|
||||
|
||||
interface OpenAIModel {
|
||||
family: "openai";
|
||||
variant: OpenAIVariant;
|
||||
version: SemVer;
|
||||
}
|
||||
|
||||
interface UnknownModel {
|
||||
family: "unknown";
|
||||
id: string;
|
||||
}
|
||||
|
||||
type ParsedModel = GeminiModel | AnthropicModel | OpenAIModel | UnknownModel;
|
||||
|
||||
/**
|
||||
* Static fallback model injected when Cloudflare AI Gateway discovery
|
||||
* returns no results. Ensures the provider always has at least one usable
|
||||
* model entry in the catalog.
|
||||
*/
|
||||
export const CLOUDFLARE_FALLBACK_MODEL: ApiModel<"anthropic-messages"> = {
|
||||
id: "claude-sonnet-4-5",
|
||||
name: "Claude Sonnet 4.5",
|
||||
api: "anthropic-messages",
|
||||
provider: "cloudflare-ai-gateway",
|
||||
baseUrl: CLOUDFLARE_AI_GATEWAY_BASE_URL,
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 3,
|
||||
output: 15,
|
||||
cacheRead: 0.3,
|
||||
cacheWrite: 3.75,
|
||||
},
|
||||
contextWindow: 200000,
|
||||
maxTokens: 64000,
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a copy of the model with canonical thinking metadata attached.
|
||||
*
|
||||
* This helper belongs to catalog enrichment only. Runtime consumers should
|
||||
* trust `model.thinking` and avoid inferring capabilities on demand.
|
||||
*/
|
||||
export function enrichModelThinking<TApi extends Api>(model: ApiModel<TApi>): ApiModel<TApi> {
|
||||
const normalizedThinking = normalizeThinkingConfig(model.thinking);
|
||||
if (!model.reasoning) {
|
||||
return normalizedThinking === undefined && model.thinking === undefined
|
||||
? model
|
||||
: { ...model, thinking: undefined };
|
||||
}
|
||||
|
||||
const thinking = normalizedThinking ?? inferModelThinking(model);
|
||||
if (thinkingsEqual(normalizedThinking, thinking)) {
|
||||
return model;
|
||||
}
|
||||
return { ...model, thinking };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of the model with thinking metadata recomputed from the
|
||||
* canonical rules, replacing any existing `thinking`.
|
||||
*/
|
||||
export function refreshModelThinking<TApi extends Api>(model: ApiModel<TApi>): ApiModel<TApi> {
|
||||
if (!model.reasoning) {
|
||||
const normalizedThinking = normalizeThinkingConfig(model.thinking);
|
||||
return normalizedThinking === undefined && model.thinking === undefined
|
||||
? model
|
||||
: { ...model, thinking: undefined };
|
||||
}
|
||||
return { ...model, thinking: inferModelThinking(model) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply upstream metadata corrections to a mutable array of models.
|
||||
*
|
||||
* Each model is first normalized through `refreshModelThinking()` so generated
|
||||
* catalogs keep canonical thinking metadata and policy fixes in one pass.
|
||||
*/
|
||||
export function applyGeneratedModelPolicies(models: ApiModel<Api>[]): void {
|
||||
for (let index = 0; index < models.length; index++) {
|
||||
const model = refreshModelThinking(models[index]!);
|
||||
applyGeneratedModelPolicy(model);
|
||||
models[index] = model;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Link `-spark` model variants to their base models for context promotion.
|
||||
*
|
||||
* When a spark model's context is exhausted, the agent can promote to the
|
||||
* corresponding full model. This sets `contextPromotionTarget` on each
|
||||
* spark variant that has a matching base model.
|
||||
*/
|
||||
export function linkSparkPromotionTargets(models: ApiModel<Api>[]): void {
|
||||
for (const candidate of models) {
|
||||
const parsedCandidate = parseKnownModel(candidate.id);
|
||||
if (parsedCandidate.family !== "openai" || parsedCandidate.variant !== "codex-spark") continue;
|
||||
const baseId = candidate.id.slice(0, -"-spark".length);
|
||||
const fallback = models.find(
|
||||
model => model.provider === candidate.provider && model.api === candidate.api && model.id === baseId,
|
||||
);
|
||||
if (!fallback) continue;
|
||||
candidate.contextPromotionTarget = `${fallback.provider}/${fallback.id}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns supported thinking efforts from canonical model rules constrained by
|
||||
* explicit model metadata.
|
||||
*
|
||||
* @throws Error when a reasoning-capable model is missing thinking metadata
|
||||
*/
|
||||
export function getSupportedEfforts<TApi extends Api>(model: ApiModel<TApi>): readonly Effort[] {
|
||||
if (!model.reasoning) {
|
||||
return [];
|
||||
}
|
||||
if (!model.thinking) {
|
||||
throw new Error(`Model ${model.provider}/${model.id} is missing thinking metadata`);
|
||||
}
|
||||
const configuredEfforts = expandEffortRange(model.thinking);
|
||||
const parsedModel = parseKnownModel(model.id);
|
||||
if (parsedModel.family === "unknown") {
|
||||
return configuredEfforts;
|
||||
}
|
||||
return intersectEfforts(configuredEfforts, inferSupportedEfforts(parsedModel, model));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamps a requested thinking level against explicit model metadata.
|
||||
*
|
||||
* Non-reasoning models always resolve to `undefined`.
|
||||
*/
|
||||
export function clampThinkingLevelForModel<TApi extends Api>(
|
||||
model: ApiModel<TApi> | undefined,
|
||||
requested: Effort | undefined,
|
||||
): Effort | undefined {
|
||||
if (!model) {
|
||||
return requested;
|
||||
}
|
||||
if (!model.reasoning || requested === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const levels = getSupportedEfforts(model);
|
||||
if (levels.includes(requested)) {
|
||||
return requested;
|
||||
}
|
||||
|
||||
const requestedIndex = THINKING_EFFORTS.indexOf(requested);
|
||||
if (requestedIndex === -1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let clamped: Effort | undefined;
|
||||
for (const effort of levels) {
|
||||
if (THINKING_EFFORTS.indexOf(effort) > requestedIndex) {
|
||||
break;
|
||||
}
|
||||
clamped = effort;
|
||||
}
|
||||
|
||||
return clamped ?? levels[0];
|
||||
}
|
||||
|
||||
export function requireSupportedEffort<TApi extends Api>(model: ApiModel<TApi>, effort: Effort): Effort {
|
||||
if (!model.reasoning) {
|
||||
throw new Error(`Model ${model.provider}/${model.id} does not support thinking`);
|
||||
}
|
||||
const levels = getSupportedEfforts(model);
|
||||
if (!levels.includes(effort)) {
|
||||
throw new Error(
|
||||
`Thinking effort ${effort} is not supported by ${model.provider}/${model.id}. Supported efforts: ${levels.join(", ")}`,
|
||||
);
|
||||
}
|
||||
return effort;
|
||||
}
|
||||
|
||||
/** Maps a normalized thinking effort to Google's `thinkingLevel` enum values. */
|
||||
export function mapEffortToGoogleThinkingLevel<TApi extends Api>(
|
||||
model: ApiModel<TApi>,
|
||||
effort: Effort,
|
||||
): "MINIMAL" | "LOW" | "MEDIUM" | "HIGH" {
|
||||
switch (requireSupportedEffort(model, effort)) {
|
||||
case Effort.Minimal:
|
||||
return "MINIMAL";
|
||||
case Effort.Low:
|
||||
return "LOW";
|
||||
case Effort.Medium:
|
||||
return "MEDIUM";
|
||||
case Effort.High:
|
||||
case Effort.XHigh:
|
||||
return "HIGH";
|
||||
}
|
||||
}
|
||||
|
||||
/** Maps a normalized thinking effort to Anthropic adaptive effort values. */
|
||||
export function mapEffortToAnthropicAdaptiveEffort<TApi extends Api>(
|
||||
model: ApiModel<TApi>,
|
||||
effort: Effort,
|
||||
): "low" | "medium" | "high" | "max" {
|
||||
switch (requireSupportedEffort(model, effort)) {
|
||||
case Effort.Minimal:
|
||||
case Effort.Low:
|
||||
return "low";
|
||||
case Effort.Medium:
|
||||
return "medium";
|
||||
case Effort.High:
|
||||
return "high";
|
||||
case Effort.XHigh:
|
||||
return "max";
|
||||
}
|
||||
}
|
||||
|
||||
function applyGeneratedModelPolicy(model: ApiModel<Api>): void {
|
||||
const parsedModel = parseKnownModel(model.id);
|
||||
if (parsedModel.family === "anthropic") {
|
||||
applyAnthropicCatalogPolicy(model, parsedModel);
|
||||
}
|
||||
if (parsedModel.family === "openai") {
|
||||
applyOpenAICatalogPolicy(model, parsedModel);
|
||||
}
|
||||
}
|
||||
|
||||
function applyAnthropicCatalogPolicy(model: ApiModel<Api>, parsedModel: AnthropicModel): void {
|
||||
// Claude Opus 4.5: models.dev reports 3x the correct cache pricing.
|
||||
if (model.provider === "anthropic" && parsedModel.kind === "opus" && semverEqual(parsedModel.version, "4.5")) {
|
||||
model.cost.cacheRead = 0.5;
|
||||
model.cost.cacheWrite = 6.25;
|
||||
}
|
||||
|
||||
// Bedrock Opus 4.6: upstream cache pricing is incorrect.
|
||||
if (model.provider === "amazon-bedrock" && parsedModel.kind === "opus" && semverEqual(parsedModel.version, "4.6")) {
|
||||
model.cost.cacheRead = 0.5;
|
||||
model.cost.cacheWrite = 6.25;
|
||||
}
|
||||
|
||||
// Opus 4.6 / Sonnet 4.6: 1M context is beta; clamp to 200K.
|
||||
if (semverEqual(parsedModel.version, "4.6")) {
|
||||
model.contextWindow = 200000;
|
||||
}
|
||||
|
||||
// OpenCode variants: Claude Sonnet 4/4.5 listed with 1M context, actual limit is 200K.
|
||||
if (
|
||||
(model.provider === "opencode-zen" || model.provider === "opencode-go") &&
|
||||
parsedModel.kind === "sonnet" &&
|
||||
(semverEqual(parsedModel.version, "4.0") || semverEqual(parsedModel.version, "4.5"))
|
||||
) {
|
||||
model.contextWindow = 200000;
|
||||
}
|
||||
}
|
||||
|
||||
function applyOpenAICatalogPolicy(model: ApiModel<Api>, parsedModel: OpenAIModel): void {
|
||||
// Codex models: 400K figure includes output budget; input window is 272K.
|
||||
if (parsedModel.variant.startsWith("codex") && parsedModel.variant !== "codex-spark") {
|
||||
model.contextWindow = 272000;
|
||||
}
|
||||
}
|
||||
|
||||
function inferModelThinking<TApi extends Api>(model: ApiModel<TApi>): ThinkingConfig {
|
||||
const parsedModel = parseKnownModel(model.id);
|
||||
const efforts = inferSupportedEfforts(parsedModel, model);
|
||||
const minLevel = efforts[0];
|
||||
const maxLevel = efforts.at(-1);
|
||||
if (!minLevel || !maxLevel) {
|
||||
throw new Error(`Model ${model.provider}/${model.id} resolved to an empty thinking range`);
|
||||
}
|
||||
return {
|
||||
mode: inferThinkingControlMode(model, parsedModel),
|
||||
minLevel,
|
||||
maxLevel,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeThinkingConfig(thinking: ThinkingConfig | undefined): ThinkingConfig | undefined {
|
||||
if (!thinking || expandEffortRange(thinking).length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return thinking;
|
||||
}
|
||||
|
||||
function thinkingsEqual(left: ThinkingConfig | undefined, right: ThinkingConfig | undefined): boolean {
|
||||
if (left === right) return true;
|
||||
if (!left || !right) return false;
|
||||
return left.mode === right.mode && left.minLevel === right.minLevel && left.maxLevel === right.maxLevel;
|
||||
}
|
||||
|
||||
function expandEffortRange(thinking: ThinkingConfig): readonly Effort[] {
|
||||
const minIndex = THINKING_EFFORTS.indexOf(thinking.minLevel);
|
||||
const maxIndex = THINKING_EFFORTS.indexOf(thinking.maxLevel);
|
||||
if (minIndex === -1 || maxIndex === -1 || minIndex > maxIndex) {
|
||||
return [];
|
||||
}
|
||||
return THINKING_EFFORTS.slice(minIndex, maxIndex + 1);
|
||||
}
|
||||
|
||||
function intersectEfforts(left: readonly Effort[], right: readonly Effort[]): readonly Effort[] {
|
||||
return left.filter(effort => right.includes(effort));
|
||||
}
|
||||
|
||||
function inferSupportedEfforts<TApi extends Api>(parsedModel: ParsedModel, model: ApiModel<TApi>): readonly Effort[] {
|
||||
switch (parsedModel.family) {
|
||||
case "openai":
|
||||
return inferOpenAISupportedEfforts(parsedModel);
|
||||
case "gemini":
|
||||
return inferGeminiSupportedEfforts(parsedModel);
|
||||
case "anthropic":
|
||||
return inferAnthropicSupportedEfforts(parsedModel, model);
|
||||
case "unknown":
|
||||
return inferFallbackEfforts(model);
|
||||
}
|
||||
}
|
||||
|
||||
function inferOpenAISupportedEfforts(model: OpenAIModel): readonly Effort[] {
|
||||
if (model.variant === "codex-mini" && semverEqual(model.version, "5.1")) {
|
||||
return GPT_5_1_CODEX_MINI_EFFORTS;
|
||||
}
|
||||
if (semverGte(model.version, "5.2")) {
|
||||
return GPT_5_2_PLUS_EFFORTS;
|
||||
}
|
||||
return DEFAULT_REASONING_EFFORTS;
|
||||
}
|
||||
|
||||
function inferGeminiSupportedEfforts(model: GeminiModel): readonly Effort[] {
|
||||
if (!semverGte(model.version, "3.0")) {
|
||||
return DEFAULT_REASONING_EFFORTS;
|
||||
}
|
||||
return model.kind === "pro" ? GEMINI_3_PRO_EFFORTS : GEMINI_3_FLASH_EFFORTS;
|
||||
}
|
||||
|
||||
function inferAnthropicSupportedEfforts<TApi extends Api>(
|
||||
parsedModel: AnthropicModel,
|
||||
model: ApiModel<TApi>,
|
||||
): readonly Effort[] {
|
||||
if (model.api === "anthropic-messages" && semverGte(parsedModel.version, "4.6")) {
|
||||
return parsedModel.kind === "opus" ? DEFAULT_REASONING_EFFORTS_WITH_XHIGH : DEFAULT_REASONING_EFFORTS;
|
||||
}
|
||||
return inferFallbackEfforts(model);
|
||||
}
|
||||
|
||||
function inferFallbackEfforts<TApi extends Api>(model: ApiModel<TApi>): readonly Effort[] {
|
||||
if (model.api === "anthropic-messages") {
|
||||
return DEFAULT_REASONING_EFFORTS_WITH_XHIGH;
|
||||
}
|
||||
if (model.api === "bedrock-converse-stream") {
|
||||
return DEFAULT_REASONING_EFFORTS;
|
||||
}
|
||||
return DEFAULT_REASONING_EFFORTS;
|
||||
}
|
||||
|
||||
function inferThinkingControlMode<TApi extends Api>(
|
||||
model: ApiModel<TApi>,
|
||||
parsedModel: ParsedModel,
|
||||
): ThinkingConfig["mode"] {
|
||||
switch (model.api) {
|
||||
case "google-generative-ai":
|
||||
case "google-gemini-cli":
|
||||
case "google-vertex":
|
||||
return parsedModel.family === "gemini" &&
|
||||
semverGte(parsedModel.version, "3.0") &&
|
||||
parsedModel.version.major === 3
|
||||
? "google-level"
|
||||
: "budget";
|
||||
|
||||
case "anthropic-messages":
|
||||
if (parsedModel.family === "anthropic") {
|
||||
if (semverGte(parsedModel.version, "4.6")) {
|
||||
return "anthropic-adaptive";
|
||||
}
|
||||
if (semverGte(parsedModel.version, "4.5")) {
|
||||
return "anthropic-budget-effort";
|
||||
}
|
||||
}
|
||||
return "budget";
|
||||
|
||||
case "bedrock-converse-stream":
|
||||
return "budget";
|
||||
|
||||
default:
|
||||
return "effort";
|
||||
}
|
||||
}
|
||||
|
||||
function parseKnownModel(modelId: string): ParsedModel {
|
||||
const canonicalId = getCanonicalModelId(modelId);
|
||||
return (
|
||||
parseGeminiModel(canonicalId) ??
|
||||
parseAnthropicModel(canonicalId) ??
|
||||
parseOpenAIModel(canonicalId) ?? { family: "unknown", id: canonicalId }
|
||||
);
|
||||
}
|
||||
|
||||
function parseGeminiModel(modelId: string): GeminiModel | null {
|
||||
const match = /gemini-(\d+(?:\.\d+){0,2})-(pro|flash)\b/.exec(modelId);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const version = parseSemVer(match[1]);
|
||||
if (!version) {
|
||||
return null;
|
||||
}
|
||||
return { family: "gemini", kind: match[2] as GeminiKind, version };
|
||||
}
|
||||
|
||||
function parseAnthropicModel(modelId: string): AnthropicModel | null {
|
||||
const match = /claude-(opus|sonnet)-(\d+(?:[.-]\d+){0,2})\b/.exec(modelId);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const version = parseSemVer(match[2]);
|
||||
if (!version) {
|
||||
return null;
|
||||
}
|
||||
return { family: "anthropic", kind: match[1] as AnthropicKind, version };
|
||||
}
|
||||
|
||||
function parseOpenAIModel(modelId: string): OpenAIModel | null {
|
||||
const match = /gpt-(\d+(?:\.\d+){0,2})(?:-(codex-spark|codex-mini|codex-max|codex|max|nano))?\b/.exec(modelId);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const version = parseSemVer(match[1]);
|
||||
if (!version) {
|
||||
return null;
|
||||
}
|
||||
return { family: "openai", variant: (match[2] as OpenAIVariant | undefined) ?? "base", version };
|
||||
}
|
||||
|
||||
function createSemVer(major: number, minor: number, patch = 0): SemVer {
|
||||
return { major, minor, patch };
|
||||
}
|
||||
|
||||
// extend this table if we need anything more than 9.10
|
||||
const precomputeTable: Record<string, SemVer> = {};
|
||||
for (let major = 0; major <= 9; major++) {
|
||||
for (let minor = 0; minor <= 10; minor++) {
|
||||
const version = createSemVer(major, minor, 0);
|
||||
precomputeTable[`${major}.${minor}`] = version;
|
||||
precomputeTable[`${major}-${minor}`] = version;
|
||||
}
|
||||
precomputeTable[`${major}`] = createSemVer(major, 0, 0);
|
||||
}
|
||||
|
||||
function parseSemVer(version: string): SemVer | null {
|
||||
return precomputeTable[version] ?? null;
|
||||
}
|
||||
|
||||
function semverGte(left: SemVer | string, right: SemVer | string): boolean {
|
||||
return compareSemVer(left, right) >= 0;
|
||||
}
|
||||
|
||||
function semverEqual(left: SemVer | string, right: SemVer | string): boolean {
|
||||
return compareSemVer(left, right) === 0;
|
||||
}
|
||||
|
||||
function compareSemVer(left: SemVer | string | null, right: SemVer | string | null): number {
|
||||
left = typeof left === "string" ? parseSemVer(left) : left;
|
||||
right = typeof right === "string" ? parseSemVer(right) : right;
|
||||
if (!left || !right) return (left ? 1 : 0) - (right ? 1 : 0);
|
||||
|
||||
if (left.major !== right.major) {
|
||||
return left.major - right.major;
|
||||
}
|
||||
if (left.minor !== right.minor) {
|
||||
return left.minor - right.minor;
|
||||
}
|
||||
return left.patch - right.patch;
|
||||
}
|
||||
|
||||
function getCanonicalModelId(modelId: string): string {
|
||||
const p = modelId.lastIndexOf("/");
|
||||
return p !== -1 ? modelId.slice(p + 1) : modelId;
|
||||
}
|
||||
+5684
-871
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
||||
import { enrichModelThinking } from "./model-thinking";
|
||||
import MODELS from "./models.json" with { type: "json" };
|
||||
import type { Api, KnownProvider, Model, Usage } from "./types";
|
||||
|
||||
@@ -13,7 +14,7 @@ const modelRegistry: Map<string, Map<string, Model<Api>>> = new Map();
|
||||
for (const [provider, models] of Object.entries(MODELS)) {
|
||||
const providerModels = new Map<string, Model<Api>>();
|
||||
for (const [id, model] of Object.entries(models)) {
|
||||
providerModels.set(id, model as Model<Api>);
|
||||
providerModels.set(id, enrichModelThinking(model as Model<Api>));
|
||||
}
|
||||
modelRegistry.set(provider, providerModels);
|
||||
}
|
||||
@@ -42,22 +43,6 @@ export function calculateCost<TApi extends Api>(model: Model<TApi>, usage: Usage
|
||||
usage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;
|
||||
return usage.cost;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a model supports xhigh thinking level.
|
||||
*
|
||||
* Supported today:
|
||||
* - GPT-5.1 Codex Max
|
||||
* - GPT-5.2 / GPT-5.3 model families
|
||||
* - Anthropic Messages API Opus 4.6 models (xhigh maps to adaptive effort "max"), or other models that support budget-based thinking
|
||||
*/
|
||||
export function supportsXhigh<TApi extends Api>(model: Model<TApi>): boolean {
|
||||
if (model.id.includes("gpt-5.2") || model.id.includes("gpt-5.3") || model.id.includes("gpt-5.1-codex-max")) {
|
||||
return true;
|
||||
}
|
||||
return model.api === "anthropic-messages";
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two models are equal by comparing both their id and provider.
|
||||
* Returns false if either model is null or undefined.
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export * from "./descriptors";
|
||||
export * from "./google";
|
||||
export * from "./model-policies";
|
||||
export * from "./openai-compat";
|
||||
export * from "./special";
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
/**
|
||||
* Post-processing policies applied to generated model catalogs.
|
||||
*
|
||||
* Each policy corrects known upstream metadata errors or normalizes model
|
||||
* properties that differ from the canonical values. Keeping these in a
|
||||
* dedicated module makes them explicit, isolated, and testable.
|
||||
*/
|
||||
import type { Api, Model } from "../types";
|
||||
|
||||
const CLOUDFLARE_AI_GATEWAY_BASE_URL = "https://gateway.ai.cloudflare.com/v1/<account>/<gateway>/anthropic";
|
||||
|
||||
/**
|
||||
* Static fallback model injected when Cloudflare AI Gateway discovery
|
||||
* returns no results. Ensures the provider always has at least one usable
|
||||
* model entry in the catalog.
|
||||
*/
|
||||
export const CLOUDFLARE_FALLBACK_MODEL: Model<"anthropic-messages"> = {
|
||||
id: "claude-sonnet-4-5",
|
||||
name: "Claude Sonnet 4.5",
|
||||
api: "anthropic-messages",
|
||||
provider: "cloudflare-ai-gateway",
|
||||
baseUrl: CLOUDFLARE_AI_GATEWAY_BASE_URL,
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 3,
|
||||
output: 15,
|
||||
cacheRead: 0.3,
|
||||
cacheWrite: 3.75,
|
||||
},
|
||||
contextWindow: 200000,
|
||||
maxTokens: 64000,
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply upstream metadata corrections to a mutable array of models.
|
||||
*
|
||||
* Corrections include cache-pricing fixes and context-window clamps where
|
||||
* provider APIs or models.dev report incorrect values.
|
||||
*/
|
||||
export function applyGeneratedModelPolicies(models: Model<Api>[]): void {
|
||||
for (const model of models) {
|
||||
// Claude Opus 4.5: models.dev reports 3x the correct cache pricing
|
||||
if (model.provider === "anthropic" && model.id === "claude-opus-4-5") {
|
||||
model.cost.cacheRead = 0.5;
|
||||
model.cost.cacheWrite = 6.25;
|
||||
}
|
||||
|
||||
// Bedrock Opus 4.6: upstream cache pricing is incorrect
|
||||
if (model.provider === "amazon-bedrock" && model.id.includes("anthropic.claude-opus-4-6-v1")) {
|
||||
model.cost.cacheRead = 0.5;
|
||||
model.cost.cacheWrite = 6.25;
|
||||
}
|
||||
|
||||
// Opus 4.6 / Sonnet 4.6: 1M context is beta; clamp to 200K
|
||||
if (
|
||||
model.id.includes("opus-4-6") ||
|
||||
model.id.includes("opus-4.6") ||
|
||||
model.id.includes("sonnet-4-6") ||
|
||||
model.id.includes("sonnet-4.6")
|
||||
) {
|
||||
model.contextWindow = 200000;
|
||||
}
|
||||
|
||||
// OpenCode variants: Claude Sonnet 4/4.5 listed with 1M context, actual limit is 200K
|
||||
if (
|
||||
(model.provider === "opencode-zen" || model.provider === "opencode-go") &&
|
||||
(model.id === "claude-sonnet-4-5" || model.id === "claude-sonnet-4")
|
||||
) {
|
||||
model.contextWindow = 200000;
|
||||
}
|
||||
|
||||
// Codex models: 400K figure includes output budget; input window is 272K
|
||||
if (model.id.includes("codex") && !model.id.includes("codex-spark")) {
|
||||
model.contextWindow = 272000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Link `-spark` model variants to their base models for context promotion.
|
||||
*
|
||||
* When a spark model's context is exhausted, the agent can promote to the
|
||||
* corresponding full model. This sets `contextPromotionTarget` on each
|
||||
* spark variant that has a matching base model.
|
||||
*/
|
||||
export function linkSparkPromotionTargets(models: Model<Api>[]): void {
|
||||
for (const candidate of models) {
|
||||
if (!candidate.id.endsWith("-spark")) continue;
|
||||
const baseId = candidate.id.slice(0, -"-spark".length);
|
||||
const fallback = models.find(
|
||||
model => model.provider === candidate.provider && model.api === candidate.api && model.id === baseId,
|
||||
);
|
||||
if (!fallback) continue;
|
||||
candidate.contextPromotionTarget = `${fallback.provider}/${fallback.id}`;
|
||||
}
|
||||
}
|
||||
@@ -21,15 +21,15 @@ import {
|
||||
} from "@aws-sdk/client-bedrock-runtime";
|
||||
import { $env } from "@oh-my-pi/pi-utils";
|
||||
import { NodeHttpHandler } from "@smithy/node-http-handler";
|
||||
import type { Effort } from "../model-thinking";
|
||||
import { mapEffortToAnthropicAdaptiveEffort, requireSupportedEffort } from "../model-thinking";
|
||||
import { calculateCost } from "../models";
|
||||
import type { ThinkingEffort, ThinkingLevel } from "../thinking";
|
||||
import type {
|
||||
Api,
|
||||
AssistantMessage,
|
||||
CacheRetention,
|
||||
Context,
|
||||
Model,
|
||||
SimpleStreamOptions,
|
||||
StopReason,
|
||||
StreamFunction,
|
||||
StreamOptions,
|
||||
@@ -51,7 +51,7 @@ export interface BedrockOptions extends StreamOptions {
|
||||
profile?: string;
|
||||
toolChoice?: "auto" | "any" | "none" | { type: "tool"; name: string };
|
||||
/* See https://docs.aws.amazon.com/bedrock/latest/userguide/inference-reasoning.html for supported models. */
|
||||
reasoning?: ThinkingLevel;
|
||||
reasoning?: Effort;
|
||||
/* Custom token budgets per thinking level. Overrides default budgets. */
|
||||
thinkingBudgets?: ThinkingBudgets;
|
||||
/* Only supported by Claude 4.x models, see https://docs.aws.amazon.com/bedrock/latest/userguide/claude-messages-extended-thinking.html#claude-messages-extended-thinking-tool-use-interleaved */
|
||||
@@ -591,85 +591,46 @@ function mapStopReason(reason: string | undefined): StopReason {
|
||||
}
|
||||
}
|
||||
|
||||
/** Check if the model supports adaptive thinking (Opus 4.6+ / Sonnet 4.6+). */
|
||||
function supportsAdaptiveThinking(modelId: string): boolean {
|
||||
return (
|
||||
modelId.includes("opus-4-6") ||
|
||||
modelId.includes("opus-4.6") ||
|
||||
modelId.includes("sonnet-4-6") ||
|
||||
modelId.includes("sonnet-4.6")
|
||||
);
|
||||
}
|
||||
|
||||
/** Map a thinking level to an adaptive effort value. */
|
||||
function mapThinkingLevelToEffort(level: SimpleStreamOptions["reasoning"]): "low" | "medium" | "high" | "max" {
|
||||
switch (level) {
|
||||
case "minimal":
|
||||
case "low":
|
||||
return "low";
|
||||
case "medium":
|
||||
return "medium";
|
||||
case "high":
|
||||
return "high";
|
||||
case "xhigh":
|
||||
return "max";
|
||||
default:
|
||||
return "high";
|
||||
}
|
||||
}
|
||||
|
||||
function buildAdditionalModelRequestFields(
|
||||
model: Model<"bedrock-converse-stream">,
|
||||
options: BedrockOptions,
|
||||
): Record<string, any> | undefined {
|
||||
const reasoning = options.reasoning;
|
||||
if (!reasoning || !model.reasoning || reasoning === "off") {
|
||||
if (!reasoning || !model.reasoning) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (model.id.includes("anthropic.claude")) {
|
||||
// Opus 4.6+ / Sonnet 4.6+ uses adaptive thinking with effort levels
|
||||
if (supportsAdaptiveThinking(model.id)) {
|
||||
let effort = mapThinkingLevelToEffort(reasoning);
|
||||
// "max" effort is only supported on Opus 4.6; clamp to "high" for Sonnet 4.6
|
||||
const supportsMax = model.id.includes("opus-4-6") || model.id.includes("opus-4.6");
|
||||
if (effort === "max" && !supportsMax) {
|
||||
effort = "high";
|
||||
}
|
||||
const result: Record<string, any> = {
|
||||
thinking: { type: "adaptive" },
|
||||
output_config: { effort },
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
const defaultBudgets: Record<ThinkingEffort, number> = {
|
||||
minimal: 1024,
|
||||
low: 2048,
|
||||
medium: 8192,
|
||||
high: 16384,
|
||||
xhigh: 16384, // Claude doesn't support xhigh, clamp to high
|
||||
const mode = model.thinking?.mode;
|
||||
if (mode === "anthropic-adaptive") {
|
||||
const effort = mapEffortToAnthropicAdaptiveEffort(model, reasoning);
|
||||
return {
|
||||
thinking: { type: "adaptive" },
|
||||
output_config: { effort },
|
||||
};
|
||||
|
||||
// Custom budgets override defaults (xhigh not in ThinkingBudgets, use high)
|
||||
const level = reasoning === "xhigh" ? "high" : reasoning;
|
||||
const budget = options.thinkingBudgets?.[level] ?? defaultBudgets[level];
|
||||
|
||||
const result: Record<string, any> = {
|
||||
thinking: {
|
||||
type: "enabled",
|
||||
budget_tokens: budget,
|
||||
},
|
||||
};
|
||||
|
||||
if (options.interleavedThinking && !supportsAdaptiveThinking(model.id)) {
|
||||
result.anthropic_beta = ["interleaved-thinking-2025-05-14"];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
const level = requireSupportedEffort(model, reasoning);
|
||||
const defaultBudgets: Record<Effort, number> = {
|
||||
minimal: 1024,
|
||||
low: 2048,
|
||||
medium: 8192,
|
||||
high: 16384,
|
||||
xhigh: 32768,
|
||||
};
|
||||
const budget = options.thinkingBudgets?.[level] ?? defaultBudgets[level];
|
||||
|
||||
const result: Record<string, any> = {
|
||||
thinking: {
|
||||
type: "enabled",
|
||||
budget_tokens: budget,
|
||||
},
|
||||
};
|
||||
|
||||
if (options.interleavedThinking) {
|
||||
result.anthropic_beta = ["interleaved-thinking-2025-05-14"];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function createImageBlock(mimeType: string, data: string) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
MessageParam,
|
||||
} from "@anthropic-ai/sdk/resources/messages";
|
||||
import { $env, abortableSleep, isEnoent } from "@oh-my-pi/pi-utils";
|
||||
import { mapEffortToAnthropicAdaptiveEffort } from "../model-thinking";
|
||||
import { calculateCost } from "../models";
|
||||
import { getEnvApiKey, OUTPUT_FALLBACK_BUFFER } from "../stream";
|
||||
import type {
|
||||
@@ -846,19 +847,6 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
||||
return stream;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a model supports adaptive thinking (Opus 4.6+)
|
||||
*/
|
||||
function supportsAdaptiveThinking(modelId: string): boolean {
|
||||
// Opus/Sonnet 4.6 model IDs (with or without date suffix)
|
||||
return (
|
||||
modelId.includes("opus-4-6") ||
|
||||
modelId.includes("opus-4.6") ||
|
||||
modelId.includes("sonnet-4-6") ||
|
||||
modelId.includes("sonnet-4.6")
|
||||
);
|
||||
}
|
||||
|
||||
export type AnthropicSystemBlock = {
|
||||
type: "text";
|
||||
text: string;
|
||||
@@ -914,26 +902,6 @@ export function buildAnthropicSystemBlocks(
|
||||
return blocks.length > 0 ? blocks : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map ThinkingLevel to Anthropic effort levels for adaptive thinking
|
||||
*/
|
||||
function mapThinkingLevelToEffort(level: SimpleStreamOptions["reasoning"]): AnthropicEffort {
|
||||
switch (level) {
|
||||
case "minimal":
|
||||
return "low";
|
||||
case "low":
|
||||
return "low";
|
||||
case "medium":
|
||||
return "medium";
|
||||
case "high":
|
||||
return "high";
|
||||
case "xhigh":
|
||||
return "max";
|
||||
default:
|
||||
return "high";
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeExtraBetas(betas?: string[] | string): string[] {
|
||||
if (!betas) return [];
|
||||
const raw = Array.isArray(betas) ? betas : betas.split(",");
|
||||
@@ -1303,9 +1271,13 @@ function buildParams(
|
||||
}
|
||||
|
||||
if (options?.thinkingEnabled && model.reasoning) {
|
||||
if (supportsAdaptiveThinking(model.id)) {
|
||||
const mode = model.thinking?.mode;
|
||||
const requestedEffort = options.reasoning;
|
||||
const effort =
|
||||
options.effort ?? (requestedEffort ? mapEffortToAnthropicAdaptiveEffort(model, requestedEffort) : undefined);
|
||||
|
||||
if (mode === "anthropic-adaptive") {
|
||||
params.thinking = { type: "adaptive" };
|
||||
const effort = options.effort ?? mapThinkingLevelToEffort(options.reasoning);
|
||||
if (effort) {
|
||||
params.output_config = { effort };
|
||||
}
|
||||
@@ -1314,12 +1286,8 @@ function buildParams(
|
||||
type: "enabled",
|
||||
budget_tokens: options.thinkingBudgetTokens || 1024,
|
||||
};
|
||||
// Opus 4.5 supports effort alongside budget-based thinking
|
||||
if (model.id.includes("opus-4-5") || model.id.includes("opus-4.5")) {
|
||||
const effort = options.effort ?? mapThinkingLevelToEffort(options.reasoning);
|
||||
if (effort) {
|
||||
params.output_config = { effort };
|
||||
}
|
||||
if (mode === "anthropic-budget-effort" && effort) {
|
||||
params.output_config = { effort };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,10 +267,7 @@ export function streamGitLabDuo(
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
const reasoningEffort =
|
||||
options.reasoning === "off"
|
||||
? undefined
|
||||
: (options.reasoning as "minimal" | "low" | "medium" | "high" | "xhigh" | undefined);
|
||||
const reasoningEffort = options.reasoning;
|
||||
|
||||
const inner =
|
||||
mapping.provider === "anthropic"
|
||||
|
||||
@@ -385,13 +385,13 @@ function buildParams(
|
||||
}
|
||||
|
||||
if (options.thinking?.enabled && model.reasoning) {
|
||||
const thinkingConfig: ThinkingConfig = { includeThoughts: true };
|
||||
const cfg: ThinkingConfig = { includeThoughts: true };
|
||||
if (options.thinking.level !== undefined) {
|
||||
thinkingConfig.thinkingLevel = THINKING_LEVEL_MAP[options.thinking.level];
|
||||
cfg.thinkingLevel = THINKING_LEVEL_MAP[options.thinking.level];
|
||||
} else if (options.thinking.budgetTokens !== undefined) {
|
||||
thinkingConfig.thinkingBudget = options.thinking.budgetTokens;
|
||||
cfg.thinkingBudget = options.thinking.budgetTokens;
|
||||
}
|
||||
config.thinkingConfig = thinkingConfig;
|
||||
config.thinkingConfig = cfg;
|
||||
}
|
||||
|
||||
if (options.signal) {
|
||||
|
||||
@@ -348,14 +348,14 @@ function buildParams(
|
||||
}
|
||||
|
||||
if (options.thinking?.enabled && model.reasoning) {
|
||||
const thinkingConfig: ThinkingConfig = { includeThoughts: true };
|
||||
const cfg: ThinkingConfig = { includeThoughts: true };
|
||||
if (options.thinking.level !== undefined) {
|
||||
// Cast to any since our GoogleThinkingLevel mirrors Google's ThinkingLevel enum values
|
||||
thinkingConfig.thinkingLevel = options.thinking.level as any;
|
||||
cfg.thinkingLevel = options.thinking.level as any;
|
||||
} else if (options.thinking.budgetTokens !== undefined) {
|
||||
thinkingConfig.thinkingBudget = options.thinking.budgetTokens;
|
||||
cfg.thinkingBudget = options.thinking.budgetTokens;
|
||||
}
|
||||
config.thinkingConfig = thinkingConfig;
|
||||
config.thinkingConfig = cfg;
|
||||
}
|
||||
|
||||
if (options.signal) {
|
||||
|
||||
@@ -62,7 +62,7 @@ export function streamKimi(
|
||||
|
||||
// Calculate thinking budget from reasoning level
|
||||
const reasoning = options?.reasoning;
|
||||
const reasoningEffort = reasoning === "off" ? undefined : reasoning;
|
||||
const reasoningEffort = reasoning;
|
||||
const thinkingEnabled = !!reasoningEffort && model.reasoning;
|
||||
const thinkingBudget = reasoningEffort
|
||||
? (options?.thinkingBudgets?.[reasoningEffort] ?? ANTHROPIC_THINKING[reasoningEffort])
|
||||
@@ -90,7 +90,7 @@ export function streamKimi(
|
||||
}
|
||||
} else {
|
||||
// OpenAI format - use original model with Kimi headers
|
||||
const reasoningEffort = options?.reasoning === "off" ? undefined : options?.reasoning;
|
||||
const reasoningEffort = options?.reasoning;
|
||||
const innerStream = streamOpenAICompletions(model, context, {
|
||||
apiKey: options?.apiKey,
|
||||
temperature: options?.temperature,
|
||||
|
||||
@@ -383,7 +383,7 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"
|
||||
include: options?.include,
|
||||
};
|
||||
|
||||
const transformedBody = await transformRequestBody(params, codexOptions, systemPrompt);
|
||||
const transformedBody = await transformRequestBody(params, model, codexOptions, systemPrompt);
|
||||
options?.onPayload?.(transformedBody);
|
||||
|
||||
const reasoningEffort = transformedBody.reasoning?.effort ?? null;
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import type { Effort } from "../../model-thinking";
|
||||
import { requireSupportedEffort } from "../../model-thinking";
|
||||
import type { Api, Model } from "../../types";
|
||||
|
||||
export interface ReasoningConfig {
|
||||
effort: "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
||||
summary: "auto" | "concise" | "detailed" | null;
|
||||
@@ -47,30 +51,10 @@ export interface RequestBody {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
function clampReasoningEffort(model: string, effort: ReasoningConfig["effort"]): ReasoningConfig["effort"] {
|
||||
// Codex backend expects exact model IDs. Do not normalize model names here.
|
||||
const modelId = model.includes("/") ? model.split("/").pop()! : model;
|
||||
|
||||
// gpt-5.1 does not support xhigh.
|
||||
if (modelId === "gpt-5.1" && effort === "xhigh") {
|
||||
return "high";
|
||||
}
|
||||
|
||||
if ((modelId.startsWith("gpt-5.2") || modelId.startsWith("gpt-5.3")) && effort === "minimal") {
|
||||
return "low";
|
||||
}
|
||||
|
||||
// gpt-5.1-codex-mini only supports medium/high.
|
||||
if (modelId === "gpt-5.1-codex-mini") {
|
||||
return effort === "high" || effort === "xhigh" ? "high" : "medium";
|
||||
}
|
||||
|
||||
return effort;
|
||||
}
|
||||
|
||||
function getReasoningConfig(model: string, options: CodexRequestOptions): ReasoningConfig {
|
||||
function getReasoningConfig(model: Model<Api>, options: CodexRequestOptions): ReasoningConfig {
|
||||
return {
|
||||
effort: clampReasoningEffort(model, options.reasoningEffort as ReasoningConfig["effort"]),
|
||||
effort:
|
||||
options.reasoningEffort === "none" ? "none" : requireSupportedEffort(model, options.reasoningEffort as Effort),
|
||||
summary: options.reasoningSummary ?? "detailed",
|
||||
};
|
||||
}
|
||||
@@ -91,6 +75,7 @@ function filterInput(input: InputItem[] | undefined): InputItem[] | undefined {
|
||||
|
||||
export async function transformRequestBody(
|
||||
body: RequestBody,
|
||||
model: Model<Api>,
|
||||
options: CodexRequestOptions = {},
|
||||
prompt?: { instructions: string; developerMessages: string[] },
|
||||
): Promise<RequestBody> {
|
||||
@@ -148,7 +133,7 @@ export async function transformRequestBody(
|
||||
}
|
||||
|
||||
if (options.reasoningEffort !== undefined) {
|
||||
const reasoningConfig = getReasoningConfig(body.model, options);
|
||||
const reasoningConfig = getReasoningConfig(model, options);
|
||||
body.reasoning = {
|
||||
...body.reasoning,
|
||||
...reasoningConfig,
|
||||
|
||||
@@ -59,7 +59,7 @@ export function streamSynthetic(
|
||||
|
||||
// Calculate thinking budget from reasoning level
|
||||
const reasoning = options?.reasoning;
|
||||
const reasoningEffort = reasoning === "off" ? undefined : reasoning;
|
||||
const reasoningEffort = reasoning;
|
||||
const thinkingEnabled = !!reasoningEffort && model.reasoning;
|
||||
const thinkingBudget = reasoningEffort
|
||||
? (options?.thinkingBudgets?.[reasoningEffort] ?? ANTHROPIC_THINKING[reasoningEffort])
|
||||
@@ -93,7 +93,7 @@ export function streamSynthetic(
|
||||
headers: mergedHeaders,
|
||||
};
|
||||
|
||||
const reasoningEffort = options?.reasoning === "off" ? undefined : options?.reasoning;
|
||||
const reasoningEffort = options?.reasoning;
|
||||
const innerStream = streamOpenAICompletions(syntheticModel, context, {
|
||||
apiKey: options?.apiKey,
|
||||
temperature: options?.temperature,
|
||||
|
||||
+34
-128
@@ -3,25 +3,25 @@ import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { $env, $pickenv } from "@oh-my-pi/pi-utils";
|
||||
import { getCustomApi } from "./api-registry";
|
||||
import { supportsXhigh } from "./models";
|
||||
import type { Effort } from "./model-thinking";
|
||||
import {
|
||||
mapEffortToAnthropicAdaptiveEffort,
|
||||
mapEffortToGoogleThinkingLevel,
|
||||
requireSupportedEffort,
|
||||
} from "./model-thinking";
|
||||
import { type BedrockOptions, streamBedrock } from "./providers/amazon-bedrock";
|
||||
import { type AnthropicOptions, streamAnthropic } from "./providers/anthropic";
|
||||
import { streamAzureOpenAIResponses } from "./providers/azure-openai-responses";
|
||||
import { type CursorOptions, streamCursor } from "./providers/cursor";
|
||||
import { isGitLabDuoModel, streamGitLabDuo } from "./providers/gitlab-duo";
|
||||
import { type GoogleOptions, streamGoogle } from "./providers/google";
|
||||
import {
|
||||
type GoogleGeminiCliOptions,
|
||||
type GoogleThinkingLevel,
|
||||
streamGoogleGeminiCli,
|
||||
} from "./providers/google-gemini-cli";
|
||||
import { type GoogleGeminiCliOptions, streamGoogleGeminiCli } from "./providers/google-gemini-cli";
|
||||
import { type GoogleVertexOptions, streamGoogleVertex } from "./providers/google-vertex";
|
||||
import { isKimiModel, streamKimi } from "./providers/kimi";
|
||||
import { streamOpenAICodexResponses } from "./providers/openai-codex-responses";
|
||||
import { type OpenAICompletionsOptions, streamOpenAICompletions } from "./providers/openai-completions";
|
||||
import { streamOpenAIResponses } from "./providers/openai-responses";
|
||||
import { isSyntheticModel, streamSynthetic } from "./providers/synthetic";
|
||||
import type { ThinkingEffort } from "./thinking";
|
||||
import type {
|
||||
Api,
|
||||
AssistantMessage,
|
||||
@@ -304,7 +304,7 @@ const MIN_OUTPUT_TOKENS = 1024;
|
||||
export const OUTPUT_FALLBACK_BUFFER = 4000;
|
||||
const ANTHROPIC_USE_INTERLEAVED_THINKING = Bun.env.PI_NO_INTERLEAVED_THINKING !== "1";
|
||||
|
||||
export const ANTHROPIC_THINKING: Record<ThinkingEffort, number> = {
|
||||
export const ANTHROPIC_THINKING: Record<Effort, number> = {
|
||||
minimal: 1024,
|
||||
low: 4096,
|
||||
medium: 8192,
|
||||
@@ -312,7 +312,7 @@ export const ANTHROPIC_THINKING: Record<ThinkingEffort, number> = {
|
||||
xhigh: 32768,
|
||||
};
|
||||
|
||||
const GOOGLE_THINKING: Record<ThinkingEffort, number> = {
|
||||
const GOOGLE_THINKING: Record<Effort, number> = {
|
||||
minimal: 1024,
|
||||
low: 4096,
|
||||
medium: 8192,
|
||||
@@ -320,7 +320,7 @@ const GOOGLE_THINKING: Record<ThinkingEffort, number> = {
|
||||
xhigh: 24575,
|
||||
};
|
||||
|
||||
const BEDROCK_CLAUDE_THINKING: Record<ThinkingEffort, number> = {
|
||||
const BEDROCK_CLAUDE_THINKING: Record<Effort, number> = {
|
||||
minimal: 1024,
|
||||
low: 2048,
|
||||
medium: 8192,
|
||||
@@ -331,10 +331,9 @@ const BEDROCK_CLAUDE_THINKING: Record<ThinkingEffort, number> = {
|
||||
function resolveBedrockThinkingBudget(
|
||||
model: Model<"bedrock-converse-stream">,
|
||||
options?: SimpleStreamOptions,
|
||||
): { budget: number; level: ThinkingEffort } | null {
|
||||
if (!options?.reasoning || !model.reasoning || options.reasoning === "off") return null;
|
||||
if (!model.id.includes("anthropic.claude")) return null;
|
||||
const level = options.reasoning === "xhigh" ? "high" : options.reasoning;
|
||||
): { budget: number; level: Effort } | null {
|
||||
if (!options?.reasoning || !model.reasoning) return null;
|
||||
const level = requireSupportedEffort(model, options.reasoning);
|
||||
const budget = options.thinkingBudgets?.[level] ?? BEDROCK_CLAUDE_THINKING[level];
|
||||
return { budget, level };
|
||||
}
|
||||
@@ -356,26 +355,6 @@ export function mapAnthropicToolChoice(choice?: ToolChoice): AnthropicOptions["t
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map ThinkingLevel to Anthropic effort levels for adaptive thinking (Opus 4.6+)
|
||||
*/
|
||||
function mapThinkingLevelToAnthropicEffort(level: ThinkingEffort, supportsXhigh: boolean): AnthropicOptions["effort"] {
|
||||
switch (level) {
|
||||
case "minimal":
|
||||
return "low";
|
||||
case "low":
|
||||
return "low";
|
||||
case "medium":
|
||||
return "medium";
|
||||
case "high":
|
||||
return "high";
|
||||
case "xhigh":
|
||||
return supportsXhigh ? "max" : "high";
|
||||
default:
|
||||
return "high";
|
||||
}
|
||||
}
|
||||
|
||||
function mapGoogleToolChoice(
|
||||
choice?: ToolChoice,
|
||||
): GoogleOptions["toolChoice"] | GoogleGeminiCliOptions["toolChoice"] | GoogleVertexOptions["toolChoice"] {
|
||||
@@ -408,11 +387,10 @@ function mapOpenAiToolChoice(choice?: ToolChoice): OpenAICompletionsOptions["too
|
||||
function resolveOpenAiReasoningEffort<TApi extends Api>(
|
||||
model: Model<TApi>,
|
||||
options?: SimpleStreamOptions,
|
||||
): ThinkingEffort | undefined {
|
||||
): Effort | undefined {
|
||||
const reasoning = options?.reasoning;
|
||||
if (!reasoning || reasoning === "off") return undefined;
|
||||
if (reasoning === "xhigh" && !supportsXhigh(model)) return "high";
|
||||
return reasoning;
|
||||
if (!reasoning) return undefined;
|
||||
return requireSupportedEffort(model, reasoning);
|
||||
}
|
||||
|
||||
const castApi = <TApi extends Api>(api: OptionsForApi<TApi>): OptionsForApi<Api> => api as OptionsForApi<Api>;
|
||||
@@ -446,7 +424,7 @@ function mapOptionsForApi<TApi extends Api>(
|
||||
case "anthropic-messages": {
|
||||
// Explicitly disable thinking when reasoning is not specified
|
||||
const reasoning = options?.reasoning;
|
||||
if (!reasoning || reasoning === "off") {
|
||||
if (!reasoning) {
|
||||
return castApi<"anthropic-messages">({
|
||||
...base,
|
||||
thinkingEnabled: false,
|
||||
@@ -465,14 +443,8 @@ function mapOptionsForApi<TApi extends Api>(
|
||||
|
||||
// For Opus 4.6+ and Sonnet 4.6+: use adaptive thinking with effort level
|
||||
// For older models: use budget-based thinking
|
||||
if (
|
||||
model.id.includes("opus-4-6") ||
|
||||
model.id.includes("opus-4.6") ||
|
||||
model.id.includes("sonnet-4-6") ||
|
||||
model.id.includes("sonnet-4.6")
|
||||
) {
|
||||
const supportsMaxEffort = model.id.includes("opus-4-6") || model.id.includes("opus-4.6");
|
||||
const effort = mapThinkingLevelToAnthropicEffort(reasoning, supportsMaxEffort);
|
||||
if (model.thinking?.mode === "anthropic-adaptive") {
|
||||
const effort = mapEffortToAnthropicAdaptiveEffort(model, reasoning);
|
||||
return castApi<"anthropic-messages">({
|
||||
...base,
|
||||
thinkingEnabled: true,
|
||||
@@ -577,7 +549,7 @@ function mapOptionsForApi<TApi extends Api>(
|
||||
// Explicitly disable thinking when reasoning is not specified
|
||||
// This is needed because Gemini has "dynamic thinking" enabled by default
|
||||
const reasoning = options?.reasoning;
|
||||
if (!reasoning || reasoning === "off") {
|
||||
if (!reasoning) {
|
||||
return castApi<"google-generative-ai">({
|
||||
...base,
|
||||
thinking: { enabled: false },
|
||||
@@ -586,16 +558,16 @@ function mapOptionsForApi<TApi extends Api>(
|
||||
}
|
||||
|
||||
const googleModel = model as Model<"google-generative-ai">;
|
||||
const effort = reasoning === "xhigh" ? "high" : reasoning;
|
||||
const effort = requireSupportedEffort(googleModel, reasoning);
|
||||
|
||||
// Gemini 3+ models use thinkingLevel exclusively instead of thinkingBudget.
|
||||
// https://ai.google.dev/gemini-api/docs/thinking#set-budget
|
||||
if (isGemini3ProModel(googleModel) || isGemini3FlashModel(googleModel)) {
|
||||
if (googleModel.thinking?.mode === "google-level") {
|
||||
return castApi<"google-generative-ai">({
|
||||
...base,
|
||||
thinking: {
|
||||
enabled: true,
|
||||
level: getGemini3ThinkingLevel(effort, googleModel),
|
||||
level: mapEffortToGoogleThinkingLevel(googleModel, effort),
|
||||
},
|
||||
toolChoice: mapGoogleToolChoice(options?.toolChoice),
|
||||
});
|
||||
@@ -613,7 +585,7 @@ function mapOptionsForApi<TApi extends Api>(
|
||||
|
||||
case "google-gemini-cli": {
|
||||
const reasoning = options?.reasoning;
|
||||
if (!reasoning || reasoning === "off") {
|
||||
if (!reasoning) {
|
||||
return castApi<"google-gemini-cli">({
|
||||
...base,
|
||||
thinking: { enabled: false },
|
||||
@@ -621,15 +593,15 @@ function mapOptionsForApi<TApi extends Api>(
|
||||
});
|
||||
}
|
||||
|
||||
const effort = reasoning === "xhigh" ? "high" : reasoning;
|
||||
const effort = requireSupportedEffort(model, reasoning);
|
||||
|
||||
// Gemini 3+ models use thinkingLevel instead of thinkingBudget
|
||||
if (isGemini3ProModelId(model.id) || isGemini3FlashModelId(model.id)) {
|
||||
return castApi<"google-vertex">({
|
||||
if (model.thinking?.mode === "google-level") {
|
||||
return castApi<"google-gemini-cli">({
|
||||
...base,
|
||||
thinking: {
|
||||
enabled: true,
|
||||
level: getGeminiCliThinkingLevel(effort, model.id),
|
||||
level: mapEffortToGoogleThinkingLevel(model, effort),
|
||||
},
|
||||
toolChoice: mapGoogleToolChoice(options?.toolChoice),
|
||||
});
|
||||
@@ -665,7 +637,7 @@ function mapOptionsForApi<TApi extends Api>(
|
||||
case "google-vertex": {
|
||||
// Explicitly disable thinking when reasoning is not specified
|
||||
const reasoning = options?.reasoning;
|
||||
if (!reasoning || reasoning === "off") {
|
||||
if (!reasoning) {
|
||||
return castApi<"google-vertex">({
|
||||
...base,
|
||||
thinking: { enabled: false },
|
||||
@@ -674,15 +646,15 @@ function mapOptionsForApi<TApi extends Api>(
|
||||
}
|
||||
|
||||
const vertexModel = model as Model<"google-vertex">;
|
||||
const effort = reasoning === "xhigh" ? "high" : reasoning;
|
||||
const effort = requireSupportedEffort(vertexModel, reasoning);
|
||||
const geminiModel = vertexModel as unknown as Model<"google-generative-ai">;
|
||||
|
||||
if (isGemini3ProModel(geminiModel) || isGemini3FlashModel(geminiModel)) {
|
||||
if (geminiModel.thinking?.mode === "google-level") {
|
||||
return castApi<"google-vertex">({
|
||||
...base,
|
||||
thinking: {
|
||||
enabled: true,
|
||||
level: getGemini3ThinkingLevel(effort, geminiModel),
|
||||
level: mapEffortToGoogleThinkingLevel(geminiModel, effort),
|
||||
},
|
||||
toolChoice: mapGoogleToolChoice(options?.toolChoice),
|
||||
});
|
||||
@@ -713,78 +685,12 @@ function mapOptionsForApi<TApi extends Api>(
|
||||
}
|
||||
}
|
||||
|
||||
function isGemini3ProModelId(modelId: string): boolean {
|
||||
return /3(?:\.\d+)?-pro/.test(modelId);
|
||||
}
|
||||
|
||||
function isGemini3FlashModelId(modelId: string): boolean {
|
||||
return /3(?:\.\d+)?-flash/.test(modelId);
|
||||
}
|
||||
|
||||
function isGemini3ProModel(model: Model<"google-generative-ai">): boolean {
|
||||
// Covers gemini-3-pro, gemini-3-pro-preview, gemini-3.1-pro-preview, and future 3.x variants
|
||||
return isGemini3ProModelId(model.id);
|
||||
}
|
||||
|
||||
function isGemini3FlashModel(model: Model<"google-generative-ai">): boolean {
|
||||
// Covers gemini-3-flash, gemini-3-flash-preview, gemini-3.1-flash, and future 3.x variants
|
||||
return isGemini3FlashModelId(model.id);
|
||||
}
|
||||
|
||||
function getGemini3ThinkingLevel(effort: ThinkingEffort, model: Model<"google-generative-ai">): GoogleThinkingLevel {
|
||||
if (isGemini3ProModel(model)) {
|
||||
// Gemini 3 Pro only supports LOW/HIGH (for now)
|
||||
switch (effort) {
|
||||
case "minimal":
|
||||
case "low":
|
||||
return "LOW";
|
||||
default:
|
||||
return "HIGH";
|
||||
}
|
||||
}
|
||||
// Gemini 3 Flash supports all four levels
|
||||
switch (effort) {
|
||||
case "minimal":
|
||||
return "MINIMAL";
|
||||
case "low":
|
||||
return "LOW";
|
||||
case "medium":
|
||||
return "MEDIUM";
|
||||
default:
|
||||
return "HIGH";
|
||||
}
|
||||
}
|
||||
|
||||
function getGeminiCliThinkingLevel(effort: ThinkingEffort, modelId: string): GoogleThinkingLevel {
|
||||
if (isGemini3ProModelId(modelId)) {
|
||||
// Gemini 3 Pro only supports LOW/HIGH (for now)
|
||||
switch (effort) {
|
||||
case "minimal":
|
||||
case "low":
|
||||
return "LOW";
|
||||
default:
|
||||
return "HIGH";
|
||||
}
|
||||
}
|
||||
// Gemini 3 Flash supports all four levels
|
||||
switch (effort) {
|
||||
case "minimal":
|
||||
return "MINIMAL";
|
||||
case "low":
|
||||
return "LOW";
|
||||
case "medium":
|
||||
return "MEDIUM";
|
||||
default:
|
||||
return "HIGH";
|
||||
}
|
||||
}
|
||||
|
||||
function getGoogleBudget(
|
||||
model: Model<"google-generative-ai">,
|
||||
effort: ThinkingEffort,
|
||||
effort: Effort,
|
||||
customBudgets?: ThinkingBudgets,
|
||||
): number {
|
||||
effort = effort === "xhigh" ? "high" : effort;
|
||||
requireSupportedEffort(model, effort);
|
||||
|
||||
// Custom budgets take precedence if provided for this level
|
||||
if (customBudgets?.[effort] !== undefined) {
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
/** Provider-level thinking levels (no "off"), ordered least to most. */
|
||||
export type ThinkingEffort = "minimal" | "low" | "medium" | "high" | "xhigh";
|
||||
|
||||
/**
|
||||
* ThinkingLevel extended with "off" to disable reasoning entirely.
|
||||
* Used in UI, config, session state, and CLI args.
|
||||
* "off" is never sent to providers — callers strip it before streaming.
|
||||
*/
|
||||
export type ThinkingLevel = ThinkingEffort | "off";
|
||||
|
||||
/**
|
||||
* ThinkingSelector extended with "inherit" to indicate the role should
|
||||
* use the session-level default rather than an explicit choice.
|
||||
* Used in per-role model assignment UI.
|
||||
*/
|
||||
export type ThinkingMode = ThinkingLevel | "inherit";
|
||||
|
||||
/** Metadata for a thinking mode. */
|
||||
export type ThinkingMetadata = {
|
||||
/** The value of the thinking mode. */
|
||||
value: ThinkingMode;
|
||||
/** The label to display for the thinking mode. */
|
||||
label: string;
|
||||
/** The description to display for the thinking mode. */
|
||||
description: string;
|
||||
};
|
||||
|
||||
const THINKING_META: Record<ThinkingMode, ThinkingMetadata> = {
|
||||
inherit: { value: "inherit", label: "inherit", description: "Inherit session default" },
|
||||
off: { value: "off", label: "off", description: "No reasoning" },
|
||||
minimal: { value: "minimal", label: "min", description: "Very brief reasoning (~1k tokens)" },
|
||||
low: { value: "low", label: "low", description: "Light reasoning (~2k tokens)" },
|
||||
medium: { value: "medium", label: "medium", description: "Moderate reasoning (~8k tokens)" },
|
||||
high: { value: "high", label: "high", description: "Deep reasoning (~16k tokens)" },
|
||||
xhigh: { value: "xhigh", label: "xhigh", description: "Maximum reasoning (~32k tokens)" },
|
||||
};
|
||||
|
||||
const F_LEVEL = 3;
|
||||
const F_SEL = 2;
|
||||
const F_MODE = 1;
|
||||
|
||||
const F_THINKING: Record<string, number> = {
|
||||
inherit: F_MODE,
|
||||
off: F_SEL,
|
||||
minimal: F_LEVEL,
|
||||
low: F_LEVEL,
|
||||
medium: F_LEVEL,
|
||||
high: F_LEVEL,
|
||||
xhigh: F_LEVEL,
|
||||
};
|
||||
|
||||
// Parses an unknown value and returns a ThinkingLevel if valid, otherwise undefined.
|
||||
export function parseThinkingEffort(level: string | null | undefined): ThinkingEffort | undefined {
|
||||
return level && (F_THINKING[level] ?? 0) >= F_LEVEL ? (level as ThinkingEffort) : undefined;
|
||||
}
|
||||
|
||||
// Parses an unknown value and returns a ThinkingSelector if valid, otherwise undefined.
|
||||
export function parseThinkingLevel(level: string | null | undefined): ThinkingLevel | undefined {
|
||||
return level && (F_THINKING[level] ?? 0) >= F_SEL ? (level as ThinkingLevel) : undefined;
|
||||
}
|
||||
|
||||
// Parses an unknown value and returns a ThinkingMode if valid, otherwise undefined.
|
||||
export function parseThinkingMode(level: string | null | undefined): ThinkingMode | undefined {
|
||||
return level && (F_THINKING[level] ?? 0) >= F_MODE ? (level as ThinkingMode) : undefined;
|
||||
}
|
||||
|
||||
/** Get the information for a thinking mode. */
|
||||
export function getThinkingMetadata(mode: ThinkingMode): ThinkingMetadata {
|
||||
return THINKING_META[mode];
|
||||
}
|
||||
|
||||
const REG_LVL: readonly ThinkingLevel[] = ["off", "minimal", "low", "medium", "high"];
|
||||
const XHI_LVL: readonly ThinkingLevel[] = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
||||
|
||||
/** Returns the available thinking modes for a model based on whether it supports xhigh. */
|
||||
export function getAvailableThinkingLevels(hasXhigh: boolean = true): ReadonlyArray<ThinkingLevel> {
|
||||
return hasXhigh ? XHI_LVL : REG_LVL;
|
||||
}
|
||||
|
||||
const REG_EFF: readonly ThinkingEffort[] = ["minimal", "low", "medium", "high"];
|
||||
const XHI_EFF: readonly ThinkingEffort[] = ["minimal", "low", "medium", "high", "xhigh"];
|
||||
|
||||
export function getAvailableThinkingEfforts(hasXhigh: boolean = true): ReadonlyArray<ThinkingEffort> {
|
||||
return hasXhigh ? XHI_EFF : REG_EFF;
|
||||
}
|
||||
@@ -66,6 +66,24 @@ export type OptionsForApi<TApi extends Api> =
|
||||
| StreamOptions
|
||||
| (TApi extends keyof ApiOptionsMap ? ApiOptionsMap[TApi] : never);
|
||||
|
||||
/** Canonical thinking transport used by a model. */
|
||||
export type ThinkingControlMode =
|
||||
| "effort"
|
||||
| "budget"
|
||||
| "google-level"
|
||||
| "anthropic-adaptive"
|
||||
| "anthropic-budget-effort";
|
||||
|
||||
/** Per-model thinking capabilities used to clamp and map user-facing effort levels. */
|
||||
export interface ThinkingConfig {
|
||||
/** Least intensive supported user-facing effort level. */
|
||||
minLevel: Effort;
|
||||
/** Most intensive supported user-facing effort level. */
|
||||
maxLevel: Effort;
|
||||
/** Provider-specific transport used to encode the selected effort. */
|
||||
mode: ThinkingControlMode;
|
||||
}
|
||||
|
||||
export type KnownProvider =
|
||||
| "amazon-bedrock"
|
||||
| "anthropic"
|
||||
@@ -110,10 +128,10 @@ export type KnownProvider =
|
||||
| "lm-studio";
|
||||
export type Provider = KnownProvider | string;
|
||||
|
||||
import type { ThinkingEffort, ThinkingLevel } from "./thinking";
|
||||
import type { Effort } from "./model-thinking";
|
||||
|
||||
/** Token budgets for each thinking level (token-based providers only) */
|
||||
export type ThinkingBudgets = { [key in ThinkingEffort]?: number };
|
||||
export type ThinkingBudgets = { [key in Effort]?: number };
|
||||
|
||||
export type MessageAttribution = "user" | "agent";
|
||||
|
||||
@@ -192,7 +210,7 @@ export interface StreamOptions {
|
||||
|
||||
// Unified options with reasoning passed to streamSimple() and completeSimple()
|
||||
export interface SimpleStreamOptions extends StreamOptions {
|
||||
reasoning?: ThinkingLevel;
|
||||
reasoning?: Effort;
|
||||
/** Custom token budgets for thinking levels (token-based providers only) */
|
||||
thinkingBudgets?: ThinkingBudgets;
|
||||
/** Cursor exec handlers for local tool execution */
|
||||
@@ -476,6 +494,8 @@ export interface Model<TApi extends Api = any> {
|
||||
contextPromotionTarget?: string;
|
||||
/** Provider-assigned priority value (lower = higher priority). */
|
||||
priority?: number;
|
||||
/** Canonical thinking capability metadata for this model. */
|
||||
thinking?: ThinkingConfig;
|
||||
/** Compatibility overrides for openai-completions API. If not set, auto-detected from baseUrl. */
|
||||
compat?: TApi extends "openai-completions" ? OpenAICompat : never;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { afterEach, describe, expect, it, vi } from "bun:test";
|
||||
import { Effort } from "@oh-my-pi/pi-ai";
|
||||
import { getBundledModel } from "../src/models";
|
||||
import { streamSimple } from "../src/stream";
|
||||
import type { Context, Model } from "../src/types";
|
||||
@@ -11,7 +12,7 @@ interface GeminiCliThinkingConfig {
|
||||
interface CapturedRequestBody {
|
||||
request?: {
|
||||
generationConfig?: {
|
||||
thinkingConfig?: GeminiCliThinkingConfig;
|
||||
thinking?: GeminiCliThinkingConfig;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -35,10 +36,10 @@ const context: Context = {
|
||||
messages: [{ role: "user", content: "hello", timestamp: Date.now() }],
|
||||
};
|
||||
|
||||
function extractThinkingConfig(bodyText: string | undefined): GeminiCliThinkingConfig | undefined {
|
||||
function extractThinking(bodyText: string | undefined): GeminiCliThinkingConfig | undefined {
|
||||
if (!bodyText) return undefined;
|
||||
const parsed = JSON.parse(bodyText) as CapturedRequestBody;
|
||||
return parsed.request?.generationConfig?.thinkingConfig;
|
||||
return parsed.request?.generationConfig?.thinking;
|
||||
}
|
||||
|
||||
describe("google-gemini-cli Gemini 3.x thinking mapping", () => {
|
||||
@@ -52,7 +53,7 @@ describe("google-gemini-cli Gemini 3.x thinking mapping", () => {
|
||||
it("includes gemini-3.1-pro-preview in bundled google-gemini-cli models", () => {
|
||||
expect(getBundledModel("google-gemini-cli", "gemini-3.1-pro-preview")?.id).toBe("gemini-3.1-pro-preview");
|
||||
});
|
||||
it("uses thinkingLevel for gemini-3.1-pro-preview", async () => {
|
||||
it("uses thinkingLevel for gemini-3.1-pro-preview when the effort is supported", async () => {
|
||||
let requestBody: string | undefined;
|
||||
globalThis.fetch = vi.fn(async (_input, init) => {
|
||||
requestBody = typeof init?.body === "string" ? init.body : undefined;
|
||||
@@ -61,13 +62,29 @@ describe("google-gemini-cli Gemini 3.x thinking mapping", () => {
|
||||
|
||||
const stream = streamSimple(createModel("gemini-3.1-pro-preview"), context, {
|
||||
apiKey: JSON.stringify({ token: "token", projectId: "proj-123" }),
|
||||
reasoning: "medium",
|
||||
reasoning: Effort.High,
|
||||
});
|
||||
await stream.result();
|
||||
|
||||
const thinkingConfig = extractThinkingConfig(requestBody);
|
||||
expect(thinkingConfig?.thinkingLevel).toBe("HIGH");
|
||||
expect(thinkingConfig?.thinkingBudget).toBeUndefined();
|
||||
const thinking = extractThinking(requestBody);
|
||||
expect(thinking?.thinkingLevel).toBe("HIGH");
|
||||
expect(thinking?.thinkingBudget).toBeUndefined();
|
||||
});
|
||||
|
||||
it("rejects unsupported gemini-3.1-pro-preview efforts instead of promoting them", () => {
|
||||
let requestBody: string | undefined;
|
||||
globalThis.fetch = vi.fn(async (_input, init) => {
|
||||
requestBody = typeof init?.body === "string" ? init.body : undefined;
|
||||
return new Response('{"error":{"message":"bad request"}}', { status: 400 });
|
||||
}) as unknown as typeof fetch;
|
||||
|
||||
expect(() =>
|
||||
streamSimple(createModel("gemini-3.1-pro-preview"), context, {
|
||||
apiKey: JSON.stringify({ token: "token", projectId: "proj-123" }),
|
||||
reasoning: Effort.Medium,
|
||||
}),
|
||||
).toThrow(/Supported efforts: low, high/);
|
||||
expect(requestBody).toBeUndefined();
|
||||
});
|
||||
|
||||
it("uses thinkingLevel for gemini-3.1-flash-preview", async () => {
|
||||
@@ -79,13 +96,13 @@ describe("google-gemini-cli Gemini 3.x thinking mapping", () => {
|
||||
|
||||
const stream = streamSimple(createModel("gemini-3.1-flash-preview"), context, {
|
||||
apiKey: JSON.stringify({ token: "token", projectId: "proj-123" }),
|
||||
reasoning: "medium",
|
||||
reasoning: Effort.Medium,
|
||||
});
|
||||
await stream.result();
|
||||
|
||||
const thinkingConfig = extractThinkingConfig(requestBody);
|
||||
expect(thinkingConfig?.thinkingLevel).toBe("MEDIUM");
|
||||
expect(thinkingConfig?.thinkingBudget).toBeUndefined();
|
||||
const thinking = extractThinking(requestBody);
|
||||
expect(thinking?.thinkingLevel).toBe("MEDIUM");
|
||||
expect(thinking?.thinkingBudget).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps thinkingBudget for gemini-2.5-pro", async () => {
|
||||
@@ -97,12 +114,12 @@ describe("google-gemini-cli Gemini 3.x thinking mapping", () => {
|
||||
|
||||
const stream = streamSimple(createModel("gemini-2.5-pro"), context, {
|
||||
apiKey: JSON.stringify({ token: "token", projectId: "proj-123" }),
|
||||
reasoning: "medium",
|
||||
reasoning: Effort.Medium,
|
||||
});
|
||||
await stream.result();
|
||||
|
||||
const thinkingConfig = extractThinkingConfig(requestBody);
|
||||
expect(thinkingConfig?.thinkingLevel).toBeUndefined();
|
||||
expect(thinkingConfig?.thinkingBudget).toBeDefined();
|
||||
const thinking = extractThinking(requestBody);
|
||||
expect(thinking?.thinkingLevel).toBeUndefined();
|
||||
expect(thinking?.thinkingBudget).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
applyGeneratedModelPolicies,
|
||||
clampThinkingLevelForModel,
|
||||
Effort,
|
||||
enrichModelThinking,
|
||||
linkSparkPromotionTargets,
|
||||
mapEffortToAnthropicAdaptiveEffort,
|
||||
mapEffortToGoogleThinkingLevel,
|
||||
requireSupportedEffort,
|
||||
} from "@oh-my-pi/pi-ai/model-thinking";
|
||||
import type { Api, Model, Provider } from "@oh-my-pi/pi-ai/types";
|
||||
|
||||
function createModel<TApi extends Api>(overrides: {
|
||||
id: string;
|
||||
api: TApi;
|
||||
provider: Provider;
|
||||
reasoning?: boolean;
|
||||
}): Model<TApi> {
|
||||
return enrichModelThinking({
|
||||
id: overrides.id,
|
||||
name: overrides.id,
|
||||
api: overrides.api,
|
||||
provider: overrides.provider,
|
||||
baseUrl: "",
|
||||
reasoning: overrides.reasoning ?? true,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 200000,
|
||||
maxTokens: 32000,
|
||||
});
|
||||
}
|
||||
|
||||
describe("model thinking metadata", () => {
|
||||
it("stores supported efforts for Codex mini in model metadata", () => {
|
||||
const model = createModel({
|
||||
id: "gpt-5.1-codex-mini",
|
||||
api: "openai-codex-responses",
|
||||
provider: "openai-codex",
|
||||
});
|
||||
|
||||
expect(model.thinking).toEqual({
|
||||
mode: "effort",
|
||||
minLevel: Effort.Medium,
|
||||
maxLevel: Effort.High,
|
||||
});
|
||||
expect(() => requireSupportedEffort(model, Effort.Low)).toThrow(/Supported efforts: medium, high/);
|
||||
expect(() => requireSupportedEffort(model, Effort.XHigh)).toThrow(/Supported efforts: medium, high/);
|
||||
});
|
||||
|
||||
it("stores xhigh support directly in metadata for GPT-5.2", () => {
|
||||
const model = createModel({
|
||||
id: "gpt-5.2-codex",
|
||||
api: "openai-codex-responses",
|
||||
provider: "openai-codex",
|
||||
});
|
||||
|
||||
expect(model.thinking).toEqual({
|
||||
mode: "effort",
|
||||
minLevel: Effort.Low,
|
||||
maxLevel: Effort.XHigh,
|
||||
});
|
||||
expect(requireSupportedEffort(model, Effort.XHigh)).toBe(Effort.XHigh);
|
||||
});
|
||||
|
||||
it("maps Gemini 3 Pro only for supported levels", () => {
|
||||
const model = createModel({
|
||||
id: "gemini-3-pro-preview",
|
||||
api: "google-generative-ai",
|
||||
provider: "google",
|
||||
});
|
||||
|
||||
expect(model.thinking).toEqual({
|
||||
mode: "google-level",
|
||||
minLevel: Effort.Low,
|
||||
maxLevel: Effort.High,
|
||||
});
|
||||
expect(mapEffortToGoogleThinkingLevel(model, Effort.Low)).toBe("LOW");
|
||||
expect(mapEffortToGoogleThinkingLevel(model, Effort.High)).toBe("HIGH");
|
||||
expect(() => mapEffortToGoogleThinkingLevel(model, Effort.Medium)).toThrow(/not supported/);
|
||||
});
|
||||
|
||||
it("encodes anthropic transport mode in metadata", () => {
|
||||
const opus45 = createModel({
|
||||
id: "claude-opus-4-5",
|
||||
api: "anthropic-messages",
|
||||
provider: "anthropic",
|
||||
});
|
||||
const opus46 = createModel({
|
||||
id: "claude-opus-4.6",
|
||||
api: "anthropic-messages",
|
||||
provider: "anthropic",
|
||||
});
|
||||
const sonnet46 = createModel({
|
||||
id: "claude-sonnet-4.6",
|
||||
api: "anthropic-messages",
|
||||
provider: "anthropic",
|
||||
});
|
||||
|
||||
expect(opus45.thinking?.mode).toBe("anthropic-budget-effort");
|
||||
expect(opus46.thinking?.mode).toBe("anthropic-adaptive");
|
||||
expect(sonnet46.thinking?.mode).toBe("anthropic-adaptive");
|
||||
expect(opus46.thinking).toEqual({
|
||||
mode: "anthropic-adaptive",
|
||||
minLevel: Effort.Minimal,
|
||||
maxLevel: Effort.XHigh,
|
||||
});
|
||||
expect(sonnet46.thinking).toEqual({
|
||||
mode: "anthropic-adaptive",
|
||||
minLevel: Effort.Minimal,
|
||||
maxLevel: Effort.High,
|
||||
});
|
||||
expect(mapEffortToAnthropicAdaptiveEffort(opus46, Effort.XHigh)).toBe("max");
|
||||
expect(() => mapEffortToAnthropicAdaptiveEffort(sonnet46, Effort.XHigh)).toThrow(/not supported/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("generated model policies", () => {
|
||||
it("refreshes thinking metadata and applies parsed catalog corrections", () => {
|
||||
const models: Model<Api>[] = [
|
||||
{
|
||||
id: "claude-opus-4-5",
|
||||
name: "Claude Opus 4.5",
|
||||
api: "anthropic-messages",
|
||||
provider: "anthropic",
|
||||
baseUrl: "https://example.com",
|
||||
reasoning: true,
|
||||
thinking: {
|
||||
mode: "budget",
|
||||
minLevel: Effort.High,
|
||||
maxLevel: Effort.High,
|
||||
},
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 1.5, cacheWrite: 18.75 },
|
||||
contextWindow: 1000000,
|
||||
maxTokens: 32000,
|
||||
},
|
||||
{
|
||||
id: "anthropic.claude-opus-4-6-v1:0",
|
||||
name: "Claude Opus 4.6",
|
||||
api: "bedrock-converse-stream",
|
||||
provider: "amazon-bedrock",
|
||||
baseUrl: "https://example.com",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 1.5, cacheWrite: 18.75 },
|
||||
contextWindow: 1000000,
|
||||
maxTokens: 32000,
|
||||
},
|
||||
{
|
||||
id: "gpt-5.2-codex",
|
||||
name: "GPT-5.2 Codex",
|
||||
api: "openai-codex-responses",
|
||||
provider: "openai-codex",
|
||||
baseUrl: "https://example.com",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 400000,
|
||||
maxTokens: 32000,
|
||||
},
|
||||
];
|
||||
|
||||
applyGeneratedModelPolicies(models);
|
||||
|
||||
expect(models[0]?.thinking).toEqual({
|
||||
mode: "anthropic-budget-effort",
|
||||
minLevel: Effort.Minimal,
|
||||
maxLevel: Effort.XHigh,
|
||||
});
|
||||
expect(models[0]?.cost.cacheRead).toBe(0.5);
|
||||
expect(models[0]?.cost.cacheWrite).toBe(6.25);
|
||||
expect(models[1]?.thinking).toEqual({
|
||||
mode: "budget",
|
||||
minLevel: Effort.Minimal,
|
||||
maxLevel: Effort.High,
|
||||
});
|
||||
expect(models[1]?.cost.cacheRead).toBe(0.5);
|
||||
expect(models[1]?.cost.cacheWrite).toBe(6.25);
|
||||
expect(models[1]?.contextWindow).toBe(200000);
|
||||
expect(models[2]?.contextWindow).toBe(272000);
|
||||
});
|
||||
|
||||
it("links spark variants to their base models", () => {
|
||||
const models = [
|
||||
createModel({
|
||||
id: "gpt-5.2-codex-spark",
|
||||
api: "openai-codex-responses",
|
||||
provider: "openai-codex",
|
||||
}),
|
||||
createModel({
|
||||
id: "gpt-5.2-codex",
|
||||
api: "openai-codex-responses",
|
||||
provider: "openai-codex",
|
||||
}),
|
||||
];
|
||||
|
||||
linkSparkPromotionTargets(models);
|
||||
|
||||
expect(models[0]?.contextPromotionTarget).toBe("openai-codex/gpt-5.2-codex");
|
||||
});
|
||||
});
|
||||
|
||||
describe("model thinking runtime helpers", () => {
|
||||
it("clamps from explicit metadata instead of inferring from model id", () => {
|
||||
const model: Model<"openai-codex-responses"> = {
|
||||
id: "custom-reasoner",
|
||||
name: "Custom Reasoner",
|
||||
api: "openai-codex-responses",
|
||||
provider: "custom",
|
||||
baseUrl: "https://example.com",
|
||||
reasoning: true,
|
||||
thinking: {
|
||||
mode: "effort",
|
||||
minLevel: Effort.Medium,
|
||||
maxLevel: Effort.High,
|
||||
},
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 200000,
|
||||
maxTokens: 32000,
|
||||
};
|
||||
|
||||
expect(clampThinkingLevelForModel(model, Effort.Minimal)).toBe(Effort.Medium);
|
||||
expect(clampThinkingLevelForModel(model, Effort.XHigh)).toBe(Effort.High);
|
||||
expect(clampThinkingLevelForModel(model, Effort.High)).toBe(Effort.High);
|
||||
});
|
||||
|
||||
it('forces "off" for non-reasoning models', () => {
|
||||
const model = createModel({
|
||||
id: "plain-model",
|
||||
api: "openai-responses",
|
||||
provider: "openai",
|
||||
reasoning: false,
|
||||
});
|
||||
|
||||
expect(clampThinkingLevelForModel(model, Effort.High)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("rejects reasoning models that are missing thinking metadata at runtime", () => {
|
||||
const model = {
|
||||
id: "broken-reasoner",
|
||||
name: "Broken Reasoner",
|
||||
api: "openai-responses",
|
||||
provider: "custom",
|
||||
baseUrl: "https://example.com",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 200000,
|
||||
maxTokens: 32000,
|
||||
} as Model<"openai-responses">;
|
||||
|
||||
expect(() => requireSupportedEffort(model, Effort.High)).toThrow(/missing thinking metadata/);
|
||||
});
|
||||
|
||||
it("drops empty thinking metadata so presence checks stay meaningful", () => {
|
||||
const model = enrichModelThinking({
|
||||
id: "plain-model",
|
||||
name: "Plain Model",
|
||||
api: "openai-responses",
|
||||
provider: "custom",
|
||||
baseUrl: "https://example.com",
|
||||
reasoning: false,
|
||||
thinking: {
|
||||
mode: "effort",
|
||||
minLevel: Effort.High,
|
||||
maxLevel: Effort.Low,
|
||||
},
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 200000,
|
||||
maxTokens: 32000,
|
||||
} satisfies Model<"openai-responses">);
|
||||
|
||||
expect(model.thinking).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,22 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { enrichModelThinking } from "@oh-my-pi/pi-ai/model-thinking";
|
||||
import { type RequestBody, transformRequestBody } from "@oh-my-pi/pi-ai/providers/openai-codex/request-transformer";
|
||||
import type { Model } from "@oh-my-pi/pi-ai/types";
|
||||
|
||||
function createCodexModel(id: string): Model<"openai-codex-responses"> {
|
||||
return enrichModelThinking({
|
||||
id,
|
||||
name: id,
|
||||
api: "openai-codex-responses",
|
||||
provider: "openai-codex",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 272000,
|
||||
maxTokens: 128000,
|
||||
});
|
||||
}
|
||||
|
||||
describe("openai-codex include handling", () => {
|
||||
it("always includes reasoning.encrypted_content when caller include is custom", async () => {
|
||||
@@ -7,7 +24,7 @@ describe("openai-codex include handling", () => {
|
||||
model: "gpt-5.1-codex",
|
||||
};
|
||||
|
||||
const transformed = await transformRequestBody(body, { include: ["foo"] });
|
||||
const transformed = await transformRequestBody(body, createCodexModel(body.model), { include: ["foo"] });
|
||||
expect(transformed.include).toEqual(["foo", "reasoning.encrypted_content"]);
|
||||
});
|
||||
|
||||
@@ -16,7 +33,7 @@ describe("openai-codex include handling", () => {
|
||||
model: "gpt-5.1-codex",
|
||||
};
|
||||
|
||||
const transformed = await transformRequestBody(body, {
|
||||
const transformed = await transformRequestBody(body, createCodexModel(body.model), {
|
||||
include: ["foo", "reasoning.encrypted_content"],
|
||||
});
|
||||
expect(transformed.include).toEqual(["foo", "reasoning.encrypted_content"]);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { afterEach, describe, expect, it, vi } from "bun:test";
|
||||
import { enrichModelThinking } from "@oh-my-pi/pi-ai/model-thinking";
|
||||
import {
|
||||
getOpenAICodexTransportDetails,
|
||||
prewarmOpenAICodexResponses,
|
||||
@@ -484,7 +485,7 @@ describe("openai-codex streaming", () => {
|
||||
await streamResult.result();
|
||||
});
|
||||
|
||||
it("clamps gpt-5.3-codex minimal reasoning effort to low", async () => {
|
||||
it("rejects gpt-5.3-codex minimal reasoning effort instead of clamping", async () => {
|
||||
const tempDir = TempDir.createSync("@pi-codex-stream-");
|
||||
setAgentDir(tempDir.path());
|
||||
|
||||
@@ -555,7 +556,7 @@ describe("openai-codex streaming", () => {
|
||||
|
||||
global.fetch = fetchMock as unknown as typeof fetch;
|
||||
|
||||
const model: Model<"openai-codex-responses"> = {
|
||||
const model = enrichModelThinking({
|
||||
id: "gpt-5.3-codex",
|
||||
name: "GPT-5.3 Codex",
|
||||
api: "openai-codex-responses",
|
||||
@@ -566,7 +567,7 @@ describe("openai-codex streaming", () => {
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 400000,
|
||||
maxTokens: 128000,
|
||||
};
|
||||
});
|
||||
|
||||
const context: Context = {
|
||||
systemPrompt: "You are a helpful assistant.",
|
||||
@@ -577,7 +578,9 @@ describe("openai-codex streaming", () => {
|
||||
apiKey: token,
|
||||
reasoning: "minimal",
|
||||
});
|
||||
await streamResult.result();
|
||||
const response = await streamResult.result();
|
||||
expect(response.stopReason).toBe("error");
|
||||
expect(response.errorMessage).toContain("Supported efforts: low, medium, high, xhigh");
|
||||
});
|
||||
|
||||
it("does not set conversation_id/session_id headers when sessionId is not provided", async () => {
|
||||
|
||||
@@ -1,10 +1,27 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { enrichModelThinking } from "@oh-my-pi/pi-ai/model-thinking";
|
||||
import { type RequestBody, transformRequestBody } from "@oh-my-pi/pi-ai/providers/openai-codex/request-transformer";
|
||||
import { parseCodexError } from "@oh-my-pi/pi-ai/providers/openai-codex/response-handler";
|
||||
import type { Model } from "@oh-my-pi/pi-ai/types";
|
||||
|
||||
const DEFAULT_PROMPT_PREFIX =
|
||||
"You are an expert coding assistant. You help users with coding tasks by reading files, executing commands";
|
||||
|
||||
function createCodexModel(id: string): Model<"openai-codex-responses"> {
|
||||
return enrichModelThinking({
|
||||
id,
|
||||
name: id,
|
||||
api: "openai-codex-responses",
|
||||
provider: "openai-codex",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 272000,
|
||||
maxTokens: 128000,
|
||||
});
|
||||
}
|
||||
|
||||
describe("openai-codex request transformer", () => {
|
||||
it("filters item_reference and strips ids", async () => {
|
||||
const body: RequestBody = {
|
||||
@@ -28,7 +45,7 @@ describe("openai-codex request transformer", () => {
|
||||
tools: [{ type: "function", name: "tool", description: "", parameters: {} }],
|
||||
};
|
||||
|
||||
const transformed = await transformRequestBody(body, {});
|
||||
const transformed = await transformRequestBody(body, createCodexModel(body.model), {});
|
||||
|
||||
expect(transformed.store).toBe(false);
|
||||
expect(transformed.stream).toBe(true);
|
||||
@@ -47,21 +64,24 @@ describe("openai-codex request transformer", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("openai-codex reasoning effort clamping", () => {
|
||||
it("clamps gpt-5.1 xhigh to high", async () => {
|
||||
describe("openai-codex reasoning effort validation", () => {
|
||||
it("rejects gpt-5.1 xhigh when metadata does not list it", async () => {
|
||||
const body: RequestBody = { model: "gpt-5.1", input: [] };
|
||||
const transformed = await transformRequestBody(body, { reasoningEffort: "xhigh" });
|
||||
expect(transformed.reasoning?.effort).toBe("high");
|
||||
await expect(
|
||||
transformRequestBody(body, createCodexModel(body.model), { reasoningEffort: "xhigh" }),
|
||||
).rejects.toThrow(/Supported efforts: minimal, low, medium, high/);
|
||||
});
|
||||
|
||||
it("clamps gpt-5.1-codex-mini to medium/high only", async () => {
|
||||
it("rejects unsupported Codex mini efforts instead of clamping", async () => {
|
||||
const body: RequestBody = { model: "gpt-5.1-codex-mini", input: [] };
|
||||
|
||||
const low = await transformRequestBody({ ...body }, { reasoningEffort: "low" });
|
||||
expect(low.reasoning?.effort).toBe("medium");
|
||||
await expect(
|
||||
transformRequestBody({ ...body }, createCodexModel(body.model), { reasoningEffort: "low" }),
|
||||
).rejects.toThrow(/Supported efforts: medium, high/);
|
||||
|
||||
const xhigh = await transformRequestBody({ ...body }, { reasoningEffort: "xhigh" });
|
||||
expect(xhigh.reasoning?.effort).toBe("high");
|
||||
await expect(
|
||||
transformRequestBody({ ...body }, createCodexModel(body.model), { reasoningEffort: "xhigh" }),
|
||||
).rejects.toThrow(/Supported efforts: medium, high/);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
||||
import { type ChildProcess, execSync, spawn } from "node:child_process";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
import { Effort } from "@oh-my-pi/pi-ai";
|
||||
import { getBundledModel } from "@oh-my-pi/pi-ai/models";
|
||||
import { complete, stream } from "@oh-my-pi/pi-ai/stream";
|
||||
import type { Api, Context, ImageContent, Model, OptionsForApi, Tool, ToolResultMessage } from "@oh-my-pi/pi-ai/types";
|
||||
@@ -534,7 +535,7 @@ describe("Generate E2E Tests", () => {
|
||||
it(
|
||||
"should handle thinking",
|
||||
async () => {
|
||||
await handleThinking(llm, { reasoning: "high" });
|
||||
await handleThinking(llm, { reasoning: Effort.High });
|
||||
},
|
||||
{ retry: 2 },
|
||||
);
|
||||
@@ -542,7 +543,7 @@ describe("Generate E2E Tests", () => {
|
||||
it(
|
||||
"should handle multi-turn with thinking and tools",
|
||||
async () => {
|
||||
await multiTurn(llm, { reasoning: "high" });
|
||||
await multiTurn(llm, { reasoning: Effort.High });
|
||||
},
|
||||
{ retry: 3 },
|
||||
);
|
||||
@@ -658,7 +659,7 @@ describe("Generate E2E Tests", () => {
|
||||
it(
|
||||
"should handle thinking mode",
|
||||
async () => {
|
||||
await handleThinking(llm, { reasoning: "medium" });
|
||||
await handleThinking(llm, { reasoning: Effort.Medium });
|
||||
},
|
||||
{ retry: 3 },
|
||||
);
|
||||
@@ -666,7 +667,7 @@ describe("Generate E2E Tests", () => {
|
||||
it(
|
||||
"should handle multi-turn with thinking and tools",
|
||||
async () => {
|
||||
await multiTurn(llm, { reasoning: "medium" });
|
||||
await multiTurn(llm, { reasoning: Effort.Medium });
|
||||
},
|
||||
{ retry: 3 },
|
||||
);
|
||||
@@ -702,7 +703,7 @@ describe("Generate E2E Tests", () => {
|
||||
it(
|
||||
"should handle thinking mode",
|
||||
async () => {
|
||||
await handleThinking(llm, { reasoning: "medium" });
|
||||
await handleThinking(llm, { reasoning: Effort.Medium });
|
||||
},
|
||||
{ retry: 3 },
|
||||
);
|
||||
@@ -710,7 +711,7 @@ describe("Generate E2E Tests", () => {
|
||||
it(
|
||||
"should handle multi-turn with thinking and tools",
|
||||
async () => {
|
||||
await multiTurn(llm, { reasoning: "medium" });
|
||||
await multiTurn(llm, { reasoning: Effort.Medium });
|
||||
},
|
||||
{ retry: 3 },
|
||||
);
|
||||
@@ -746,7 +747,7 @@ describe("Generate E2E Tests", () => {
|
||||
it(
|
||||
"should handle thinking mode",
|
||||
async () => {
|
||||
await handleThinking(llm, { reasoning: "medium" });
|
||||
await handleThinking(llm, { reasoning: Effort.Medium });
|
||||
},
|
||||
{ retry: 3 },
|
||||
);
|
||||
@@ -754,7 +755,7 @@ describe("Generate E2E Tests", () => {
|
||||
it(
|
||||
"should handle multi-turn with thinking and tools",
|
||||
async () => {
|
||||
await multiTurn(llm, { reasoning: "medium" });
|
||||
await multiTurn(llm, { reasoning: Effort.Medium });
|
||||
},
|
||||
{ retry: 3 },
|
||||
);
|
||||
@@ -790,7 +791,7 @@ describe("Generate E2E Tests", () => {
|
||||
it(
|
||||
"should handle thinking mode",
|
||||
async () => {
|
||||
await handleThinking(llm, { reasoning: "medium" });
|
||||
await handleThinking(llm, { reasoning: Effort.Medium });
|
||||
},
|
||||
{ retry: 3 },
|
||||
);
|
||||
@@ -798,7 +799,7 @@ describe("Generate E2E Tests", () => {
|
||||
it(
|
||||
"should handle multi-turn with thinking and tools",
|
||||
async () => {
|
||||
await multiTurn(llm, { reasoning: "medium" });
|
||||
await multiTurn(llm, { reasoning: Effort.Medium });
|
||||
},
|
||||
{ retry: 2 },
|
||||
);
|
||||
@@ -842,7 +843,7 @@ describe("Generate E2E Tests", () => {
|
||||
it.skip(
|
||||
"should handle thinking mode",
|
||||
async () => {
|
||||
await handleThinking(llm, { reasoning: "medium" });
|
||||
await handleThinking(llm, { reasoning: Effort.Medium });
|
||||
},
|
||||
{ retry: 3 },
|
||||
);
|
||||
@@ -850,7 +851,7 @@ describe("Generate E2E Tests", () => {
|
||||
it(
|
||||
"should handle multi-turn with thinking and tools",
|
||||
async () => {
|
||||
await multiTurn(llm, { reasoning: "medium" });
|
||||
await multiTurn(llm, { reasoning: Effort.Medium });
|
||||
},
|
||||
{ retry: 3 },
|
||||
);
|
||||
@@ -886,7 +887,7 @@ describe("Generate E2E Tests", () => {
|
||||
it(
|
||||
"should handle thinking mode",
|
||||
async () => {
|
||||
await handleThinking(llm, { reasoning: "medium" });
|
||||
await handleThinking(llm, { reasoning: Effort.Medium });
|
||||
},
|
||||
{ retry: 3 },
|
||||
);
|
||||
@@ -894,7 +895,7 @@ describe("Generate E2E Tests", () => {
|
||||
it(
|
||||
"should handle multi-turn with thinking and tools",
|
||||
async () => {
|
||||
await multiTurn(llm, { reasoning: "medium" });
|
||||
await multiTurn(llm, { reasoning: Effort.Medium });
|
||||
},
|
||||
{ retry: 3 },
|
||||
);
|
||||
@@ -950,7 +951,7 @@ describe("Generate E2E Tests", () => {
|
||||
it(
|
||||
"should handle multi-turn with thinking and tools",
|
||||
async () => {
|
||||
await multiTurn(llm, { reasoning: "medium" });
|
||||
await multiTurn(llm, { reasoning: Effort.Medium });
|
||||
},
|
||||
{ retry: 3 },
|
||||
);
|
||||
@@ -1076,7 +1077,7 @@ describe("Generate E2E Tests", () => {
|
||||
"should handle thinking",
|
||||
async () => {
|
||||
const thinkingModel = getBundledModel("github-copilot", "gpt-5-mini");
|
||||
await handleThinking(thinkingModel, { apiKey: githubCopilotToken, reasoning: "high" });
|
||||
await handleThinking(thinkingModel, { apiKey: githubCopilotToken, reasoning: Effort.High });
|
||||
},
|
||||
{ retry: 2 },
|
||||
);
|
||||
@@ -1085,7 +1086,7 @@ describe("Generate E2E Tests", () => {
|
||||
"should handle multi-turn with thinking and tools",
|
||||
async () => {
|
||||
const thinkingModel = getBundledModel("github-copilot", "gpt-5-mini");
|
||||
await multiTurn(thinkingModel, { apiKey: githubCopilotToken, reasoning: "high" });
|
||||
await multiTurn(thinkingModel, { apiKey: githubCopilotToken, reasoning: Effort.High });
|
||||
},
|
||||
{ retry: 3 },
|
||||
);
|
||||
@@ -1318,7 +1319,7 @@ describe("Generate E2E Tests", () => {
|
||||
it.skipIf(!openaiCodexToken)(
|
||||
"should handle thinking",
|
||||
async () => {
|
||||
await handleThinking(llm, { apiKey: openaiCodexToken, reasoning: "high" });
|
||||
await handleThinking(llm, { apiKey: openaiCodexToken, reasoning: Effort.High });
|
||||
},
|
||||
{ retry: 3 },
|
||||
);
|
||||
@@ -1361,7 +1362,7 @@ describe("Generate E2E Tests", () => {
|
||||
tools: [calculatorTool],
|
||||
},
|
||||
{
|
||||
reasoning: "xhigh",
|
||||
reasoning: Effort.XHigh,
|
||||
interleavedThinking: true,
|
||||
onPayload: payload => {
|
||||
capturedPayload = payload;
|
||||
@@ -1490,7 +1491,7 @@ describe("Generate E2E Tests", () => {
|
||||
"should handle thinking mode",
|
||||
async () => {
|
||||
if (!llm) return;
|
||||
await handleThinking(llm, { apiKey: "test", reasoning: "medium" });
|
||||
await handleThinking(llm, { apiKey: "test", reasoning: Effort.Medium });
|
||||
},
|
||||
{ retry: 3 },
|
||||
);
|
||||
@@ -1499,7 +1500,7 @@ describe("Generate E2E Tests", () => {
|
||||
"should handle multi-turn with thinking and tools",
|
||||
async () => {
|
||||
if (!llm) return;
|
||||
await multiTurn(llm, { apiKey: "test", reasoning: "medium" });
|
||||
await multiTurn(llm, { apiKey: "test", reasoning: Effort.Medium });
|
||||
},
|
||||
{ retry: 3 },
|
||||
);
|
||||
|
||||
@@ -1,8 +1,23 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Changed `ThinkingLevel` type to be imported from `@oh-my-pi/pi-agent-core` instead of `@oh-my-pi/pi-ai`
|
||||
- Changed thinking level representation from string literals to `Effort` enum values (e.g., `Effort.High` instead of `"high"`)
|
||||
- Changed `getThinkingLevel()` return type to `ThinkingLevel | undefined` to support models without thinking support
|
||||
- Changed model `reasoning` property to `thinking` property with `ThinkingConfig` for explicit effort level configuration
|
||||
- Changed `thinkingLevel` in session context to be optional (`ThinkingLevel | undefined`) instead of always present
|
||||
|
||||
### Added
|
||||
|
||||
- Added `thinking.ts` module with `getThinkingLevelMetadata()` and `resolveThinkingLevelForModel()` utilities for thinking level handling
|
||||
- Added `ThinkingConfig` support to model definitions for specifying supported thinking effort levels per model
|
||||
- Added `enrichModelThinking()` function to apply thinking configuration to models during registry initialization
|
||||
- Added `clampThinkingLevelForModel()` function to constrain thinking levels to model-supported ranges
|
||||
- Added `getSupportedEfforts()` function to retrieve available thinking efforts for a model
|
||||
- Added `Effort` enum import from `@oh-my-pi/pi-ai` for type-safe thinking level representation
|
||||
- Added `/fast` slash command to toggle OpenAI service tier priority mode for faster response processing
|
||||
- Added `serviceTier` setting to control OpenAI processing priority (none, auto, default, flex, scale, priority)
|
||||
- Added `compaction.remoteEnabled` setting to control use of remote compaction endpoints
|
||||
@@ -13,6 +28,14 @@
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed thinking level parsing to use `parseEffort()` from local thinking module instead of `parseThinkingLevel()` from pi-ai
|
||||
- Changed model list display to show supported thinking efforts (e.g., "low,medium,high") instead of yes/no reasoning indicator
|
||||
- Changed footer and status line to check `model.thinking` instead of `model.reasoning` for thinking level display
|
||||
- Changed thinking selector to work with `Effort` type instead of `ThinkingLevel` for available levels
|
||||
- Changed model resolver to return `undefined` for thinking level instead of `"off"` when no thinking is specified
|
||||
- Changed compaction reasoning parameters to use `Effort` enum values instead of string literals
|
||||
- Changed RPC types to use `Effort` for cycling thinking levels and `ThinkingLevel | undefined` for session state
|
||||
- Changed theme thinking border color function to accept both `ThinkingLevel` and `Effort` types
|
||||
- Changed context usage coloring in footer and status line to use token-aware thresholds instead of fixed percentages
|
||||
- Changed compaction to preserve OpenAI remote compaction state and encrypted reasoning across sessions
|
||||
- Changed compaction to skip emitting kept messages when using OpenAI remote compaction with preserved history
|
||||
@@ -22,6 +45,8 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed thinking level display logic in main.ts to correctly check for undefined instead of "off"
|
||||
- Fixed model registry to preserve explicit thinking configuration on runtime-registered models
|
||||
- Fixed usage limit reset time calculation to use absolute `resetsAt` timestamps instead of deprecated `resetInMs` field
|
||||
- Fixed compaction summary message creation to no longer be automatically added to chat during compaction (now handled by session manager)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*
|
||||
* Shows how to select a specific model and thinking level.
|
||||
*/
|
||||
import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import { getModel } from "@oh-my-pi/pi-ai";
|
||||
import { createAgentSession, discoverAuthStorage, discoverModels } from "@oh-my-pi/pi-coding-agent";
|
||||
|
||||
@@ -32,7 +33,7 @@ console.log(
|
||||
if (available.length > 0) {
|
||||
const { session } = await createAgentSession({
|
||||
model: available[0],
|
||||
thinkingLevel: "medium", // off, low, medium, high
|
||||
thinkingLevel: ThinkingLevel.Medium, // off, low, medium, high
|
||||
authStorage,
|
||||
modelRegistry,
|
||||
});
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
/**
|
||||
* CLI argument parsing and help display
|
||||
*/
|
||||
import { getAvailableThinkingLevels, parseThinkingLevel, type ThinkingLevel } from "@oh-my-pi/pi-ai";
|
||||
import { type Effort, THINKING_EFFORTS } from "@oh-my-pi/pi-ai";
|
||||
import { APP_NAME, CONFIG_DIR_NAME, logger } from "@oh-my-pi/pi-utils";
|
||||
import chalk from "chalk";
|
||||
import { parseEffort } from "../thinking";
|
||||
import { BUILTIN_TOOLS } from "../tools";
|
||||
|
||||
export type Mode = "text" | "json" | "rpc";
|
||||
@@ -19,7 +20,7 @@ export interface Args {
|
||||
apiKey?: string;
|
||||
systemPrompt?: string;
|
||||
appendSystemPrompt?: string;
|
||||
thinking?: ThinkingLevel;
|
||||
thinking?: Effort;
|
||||
continue?: boolean;
|
||||
resume?: string | true;
|
||||
help?: boolean;
|
||||
@@ -122,13 +123,13 @@ export function parseArgs(args: string[], extensionFlags?: Map<string, { type: "
|
||||
result.tools = validTools;
|
||||
} else if (arg === "--thinking" && i + 1 < args.length) {
|
||||
const rawThinking = args[++i];
|
||||
const thinking = parseThinkingLevel(rawThinking);
|
||||
const thinking = parseEffort(rawThinking);
|
||||
if (thinking !== undefined) {
|
||||
result.thinking = thinking;
|
||||
} else {
|
||||
logger.warn("Invalid thinking level passed to --thinking", {
|
||||
level: rawThinking,
|
||||
validThinkingLevels: getAvailableThinkingLevels(),
|
||||
validThinkingLevels: THINKING_EFFORTS,
|
||||
});
|
||||
}
|
||||
} else if (arg === "--print" || arg === "-p") {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* List available models with optional fuzzy search
|
||||
*/
|
||||
import type { Api, Model } from "@oh-my-pi/pi-ai";
|
||||
import { type Api, getSupportedEfforts, type Model } from "@oh-my-pi/pi-ai";
|
||||
import { formatNumber } from "@oh-my-pi/pi-utils";
|
||||
import type { ModelRegistry } from "../config/model-registry";
|
||||
import { fuzzyFilter } from "../utils/fuzzy";
|
||||
@@ -41,7 +41,7 @@ export async function listModels(modelRegistry: ModelRegistry, searchPattern?: s
|
||||
model: m.id,
|
||||
context: formatNumber(m.contextWindow),
|
||||
maxOut: formatNumber(m.maxTokens),
|
||||
thinking: m.reasoning ? "yes" : "no",
|
||||
thinking: m.thinking ? getSupportedEfforts(m).join(",") : m.reasoning ? "yes" : "-",
|
||||
images: m.input.includes("image") ? "yes" : "no",
|
||||
}));
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Root command for the coding agent CLI.
|
||||
*/
|
||||
|
||||
import { getAvailableThinkingLevels } from "@oh-my-pi/pi-ai";
|
||||
import { THINKING_EFFORTS } from "@oh-my-pi/pi-ai";
|
||||
import { APP_NAME } from "@oh-my-pi/pi-utils";
|
||||
import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
|
||||
import { parseArgs } from "../cli/args";
|
||||
@@ -86,8 +86,8 @@ export default class Index extends Command {
|
||||
description: "Comma-separated list of tools to enable (default: all)",
|
||||
}),
|
||||
thinking: Flags.string({
|
||||
description: `Set thinking level: ${getAvailableThinkingLevels().join(", ")}`,
|
||||
options: getAvailableThinkingLevels(),
|
||||
description: `Set thinking level: ${THINKING_EFFORTS.join(", ")}`,
|
||||
options: [...THINKING_EFFORTS],
|
||||
}),
|
||||
hook: Flags.string({
|
||||
description: "Load a hook/extension file (can be used multiple times)",
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
type Context,
|
||||
createModelManager,
|
||||
DEFAULT_LOCAL_TOKEN,
|
||||
enrichModelThinking,
|
||||
getBundledModels,
|
||||
getBundledProviders,
|
||||
googleAntigravityModelManagerOptions,
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
registerCustomApi,
|
||||
registerOAuthProvider,
|
||||
type SimpleStreamOptions,
|
||||
type ThinkingConfig,
|
||||
unregisterCustomApis,
|
||||
unregisterOAuthProviders,
|
||||
} from "@oh-my-pi/pi-ai";
|
||||
@@ -72,6 +74,28 @@ const OpenAICompatSchema = Type.Object({
|
||||
vercelGatewayRouting: Type.Optional(VercelGatewayRoutingSchema),
|
||||
});
|
||||
|
||||
const EffortSchema = Type.Union([
|
||||
Type.Literal("minimal"),
|
||||
Type.Literal("low"),
|
||||
Type.Literal("medium"),
|
||||
Type.Literal("high"),
|
||||
Type.Literal("xhigh"),
|
||||
]);
|
||||
|
||||
const ThinkingControlModeSchema = Type.Union([
|
||||
Type.Literal("effort"),
|
||||
Type.Literal("budget"),
|
||||
Type.Literal("google-level"),
|
||||
Type.Literal("anthropic-adaptive"),
|
||||
Type.Literal("anthropic-budget-effort"),
|
||||
]);
|
||||
|
||||
const ModelThinkingSchema = Type.Object({
|
||||
minLevel: EffortSchema,
|
||||
maxLevel: EffortSchema,
|
||||
mode: ThinkingControlModeSchema,
|
||||
});
|
||||
|
||||
// Schema for custom model definition
|
||||
// Most fields are optional with sensible defaults for local models (Ollama, LM Studio, etc.)
|
||||
const ModelDefinitionSchema = Type.Object({
|
||||
@@ -90,6 +114,7 @@ const ModelDefinitionSchema = Type.Object({
|
||||
),
|
||||
baseUrl: Type.Optional(Type.String({ minLength: 1 })),
|
||||
reasoning: Type.Optional(Type.Boolean()),
|
||||
thinking: Type.Optional(ModelThinkingSchema),
|
||||
input: Type.Optional(Type.Array(Type.Union([Type.Literal("text"), Type.Literal("image")]))),
|
||||
cost: Type.Optional(
|
||||
Type.Object({
|
||||
@@ -111,6 +136,7 @@ const ModelDefinitionSchema = Type.Object({
|
||||
const ModelOverrideSchema = Type.Object({
|
||||
name: Type.Optional(Type.String({ minLength: 1 })),
|
||||
reasoning: Type.Optional(Type.Boolean()),
|
||||
thinking: Type.Optional(ModelThinkingSchema),
|
||||
input: Type.Optional(Type.Array(Type.Union([Type.Literal("text"), Type.Literal("image")]))),
|
||||
cost: Type.Optional(
|
||||
Type.Object({
|
||||
@@ -376,6 +402,7 @@ function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<A
|
||||
const result = { ...model };
|
||||
if (override.name !== undefined) result.name = override.name;
|
||||
if (override.reasoning !== undefined) result.reasoning = override.reasoning;
|
||||
if (override.thinking !== undefined) result.thinking = override.thinking as ThinkingConfig;
|
||||
if (override.input !== undefined) result.input = override.input as ("text" | "image")[];
|
||||
if (override.contextWindow !== undefined) result.contextWindow = override.contextWindow;
|
||||
if (override.maxTokens !== undefined) result.maxTokens = override.maxTokens;
|
||||
@@ -393,7 +420,7 @@ function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<A
|
||||
result.headers = { ...model.headers, ...override.headers };
|
||||
}
|
||||
result.compat = mergeCompat(model.compat, override.compat);
|
||||
return result;
|
||||
return enrichModelThinking(result);
|
||||
}
|
||||
|
||||
interface CustomModelDefinitionLike {
|
||||
@@ -402,6 +429,7 @@ interface CustomModelDefinitionLike {
|
||||
api?: Api;
|
||||
baseUrl?: string;
|
||||
reasoning?: boolean;
|
||||
thinking?: ThinkingConfig;
|
||||
input?: ("text" | "image")[];
|
||||
cost?: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
||||
contextWindow?: number;
|
||||
@@ -447,13 +475,14 @@ function buildCustomModel(
|
||||
const withDefaults = options.useDefaults;
|
||||
const cost = modelDef.cost ?? (withDefaults ? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } : undefined);
|
||||
const input = modelDef.input ?? (withDefaults ? ["text"] : undefined);
|
||||
return {
|
||||
return enrichModelThinking({
|
||||
id: modelDef.id,
|
||||
name: modelDef.name ?? (withDefaults ? modelDef.id : undefined),
|
||||
api,
|
||||
provider: providerName,
|
||||
baseUrl: modelDef.baseUrl ?? providerBaseUrl,
|
||||
reasoning: modelDef.reasoning ?? (withDefaults ? false : undefined),
|
||||
thinking: modelDef.thinking as ThinkingConfig | undefined,
|
||||
input: input as ("text" | "image")[],
|
||||
cost,
|
||||
contextWindow: modelDef.contextWindow ?? (withDefaults ? 128000 : undefined),
|
||||
@@ -462,7 +491,7 @@ function buildCustomModel(
|
||||
compat: modelDef.compat,
|
||||
contextPromotionTarget: modelDef.contextPromotionTarget,
|
||||
premiumMultiplier: modelDef.premiumMultiplier,
|
||||
} as Model<Api>;
|
||||
} as Model<Api>);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -855,19 +884,21 @@ export class ModelRegistry {
|
||||
for (const item of models) {
|
||||
const id = item.model || item.name;
|
||||
if (!id) continue;
|
||||
discovered.push({
|
||||
id,
|
||||
name: item.name || id,
|
||||
api: providerConfig.api,
|
||||
provider: providerConfig.provider,
|
||||
baseUrl: `${endpoint}/v1`,
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 128000,
|
||||
maxTokens: 8192,
|
||||
headers: providerConfig.headers,
|
||||
});
|
||||
discovered.push(
|
||||
enrichModelThinking({
|
||||
id,
|
||||
name: item.name || id,
|
||||
api: providerConfig.api,
|
||||
provider: providerConfig.provider,
|
||||
baseUrl: `${endpoint}/v1`,
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 128000,
|
||||
maxTokens: 8192,
|
||||
headers: providerConfig.headers,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
|
||||
} catch (error) {
|
||||
@@ -909,24 +940,26 @@ export class ModelRegistry {
|
||||
for (const item of models) {
|
||||
const id = item.id;
|
||||
if (!id) continue;
|
||||
discovered.push({
|
||||
id,
|
||||
name: id,
|
||||
api: providerConfig.api,
|
||||
provider: providerConfig.provider,
|
||||
baseUrl,
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 128000,
|
||||
maxTokens: 8192,
|
||||
headers,
|
||||
compat: {
|
||||
supportsStore: false,
|
||||
supportsDeveloperRole: false,
|
||||
supportsReasoningEffort: false,
|
||||
},
|
||||
});
|
||||
discovered.push(
|
||||
enrichModelThinking({
|
||||
id,
|
||||
name: id,
|
||||
api: providerConfig.api,
|
||||
provider: providerConfig.provider,
|
||||
baseUrl,
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 128000,
|
||||
maxTokens: 8192,
|
||||
headers,
|
||||
compat: {
|
||||
supportsStore: false,
|
||||
supportsDeveloperRole: false,
|
||||
supportsReasoningEffort: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
|
||||
} catch (error) {
|
||||
@@ -1008,7 +1041,7 @@ export class ModelRegistry {
|
||||
providerConfig.headers,
|
||||
providerConfig.apiKey,
|
||||
providerConfig.authHeader,
|
||||
modelDef,
|
||||
modelDef as CustomModelDefinitionLike,
|
||||
{ useDefaults: true },
|
||||
);
|
||||
if (!model) continue;
|
||||
@@ -1161,7 +1194,7 @@ export class ModelRegistry {
|
||||
config.headers,
|
||||
config.apiKey,
|
||||
config.authHeader,
|
||||
modelDef,
|
||||
modelDef as CustomModelDefinitionLike,
|
||||
{ useDefaults: false },
|
||||
);
|
||||
if (!model) {
|
||||
@@ -1218,6 +1251,7 @@ export interface ProviderConfigInput {
|
||||
api?: Api;
|
||||
baseUrl?: string;
|
||||
reasoning: boolean;
|
||||
thinking?: ThinkingConfig;
|
||||
input: ("text" | "image")[];
|
||||
cost: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
||||
contextWindow: number;
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
/**
|
||||
* Model resolution, scoping, and initial selection
|
||||
*/
|
||||
|
||||
import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import {
|
||||
type Api,
|
||||
clampThinkingLevelForModel,
|
||||
DEFAULT_MODEL_PER_PROVIDER,
|
||||
type Effort,
|
||||
type KnownProvider,
|
||||
type Model,
|
||||
modelsAreEqual,
|
||||
parseThinkingLevel,
|
||||
type ThinkingLevel,
|
||||
} from "@oh-my-pi/pi-ai";
|
||||
import chalk from "chalk";
|
||||
import MODEL_PRIO from "../priority.json" with { type: "json" };
|
||||
import { parseThinkingLevel, resolveThinkingLevelForModel } from "../thinking";
|
||||
import { fuzzyMatch } from "../utils/fuzzy";
|
||||
import { MODEL_ROLE_IDS, type ModelRegistry, type ModelRole } from "./model-registry";
|
||||
import type { Settings } from "./settings";
|
||||
@@ -377,7 +380,14 @@ export function resolveModelRoleValue(
|
||||
options?.matchPreferences,
|
||||
);
|
||||
|
||||
return { model, thinkingLevel, explicitThinkingLevel, warning };
|
||||
return {
|
||||
model,
|
||||
thinkingLevel: explicitThinkingLevel
|
||||
? (resolveThinkingLevelForModel(model, thinkingLevel) ?? thinkingLevel)
|
||||
: thinkingLevel,
|
||||
explicitThinkingLevel,
|
||||
warning,
|
||||
};
|
||||
}
|
||||
|
||||
export function extractExplicitThinkingSelector(
|
||||
@@ -393,10 +403,10 @@ export function extractExplicitThinkingSelector(
|
||||
while (!visited.has(current)) {
|
||||
visited.add(current);
|
||||
const lastColonIndex = current.lastIndexOf(":");
|
||||
const hasThinkingSuffix =
|
||||
lastColonIndex > PREFIX_MODEL_ROLE.length && parseThinkingLevel(current.slice(lastColonIndex + 1));
|
||||
if (hasThinkingSuffix) {
|
||||
return current.slice(lastColonIndex + 1) as ThinkingLevel;
|
||||
const thinkingSelector =
|
||||
lastColonIndex > PREFIX_MODEL_ROLE.length ? parseThinkingLevel(current.slice(lastColonIndex + 1)) : undefined;
|
||||
if (thinkingSelector) {
|
||||
return thinkingSelector;
|
||||
}
|
||||
const expanded = expandRoleAlias(current, settings).trim();
|
||||
if (!expanded || expanded === current) break;
|
||||
@@ -520,7 +530,13 @@ export async function resolveModelScope(
|
||||
|
||||
for (const model of matchingModels) {
|
||||
if (!scopedModels.find(sm => modelsAreEqual(sm.model, model))) {
|
||||
scopedModels.push({ model, thinkingLevel, explicitThinkingLevel });
|
||||
scopedModels.push({
|
||||
model,
|
||||
thinkingLevel: explicitThinkingLevel
|
||||
? (resolveThinkingLevelForModel(model, thinkingLevel) ?? thinkingLevel)
|
||||
: thinkingLevel,
|
||||
explicitThinkingLevel,
|
||||
});
|
||||
}
|
||||
}
|
||||
continue;
|
||||
@@ -543,7 +559,13 @@ export async function resolveModelScope(
|
||||
|
||||
// Avoid duplicates
|
||||
if (!scopedModels.find(sm => modelsAreEqual(sm.model, model))) {
|
||||
scopedModels.push({ model, thinkingLevel, explicitThinkingLevel });
|
||||
scopedModels.push({
|
||||
model,
|
||||
thinkingLevel: explicitThinkingLevel
|
||||
? (resolveThinkingLevelForModel(model, thinkingLevel) ?? thinkingLevel)
|
||||
: thinkingLevel,
|
||||
explicitThinkingLevel,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -644,7 +666,7 @@ export function resolveCliModel(options: {
|
||||
|
||||
export interface InitialModelResult {
|
||||
model: Model<Api> | undefined;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
thinkingLevel?: ThinkingLevel;
|
||||
fallbackMessage: string | undefined;
|
||||
}
|
||||
|
||||
@@ -663,7 +685,7 @@ export async function findInitialModel(options: {
|
||||
isContinuing: boolean;
|
||||
defaultProvider?: string;
|
||||
defaultModelId?: string;
|
||||
defaultThinkingSelector?: ThinkingLevel;
|
||||
defaultThinkingSelector?: Effort;
|
||||
modelRegistry: ModelRegistry;
|
||||
}): Promise<InitialModelResult> {
|
||||
const {
|
||||
@@ -678,7 +700,7 @@ export async function findInitialModel(options: {
|
||||
} = options;
|
||||
|
||||
let model: Model<Api> | undefined;
|
||||
let thinkingLevel: ThinkingLevel = "off";
|
||||
let thinkingLevel: Effort | undefined;
|
||||
|
||||
// 1. CLI args take priority
|
||||
if (cliProvider && cliModel) {
|
||||
@@ -687,16 +709,22 @@ export async function findInitialModel(options: {
|
||||
console.error(chalk.red(`Model ${cliProvider}/${cliModel} not found`));
|
||||
process.exit(1);
|
||||
}
|
||||
return { model: found, thinkingLevel: "off", fallbackMessage: undefined };
|
||||
return { model: found, thinkingLevel: undefined, fallbackMessage: undefined };
|
||||
}
|
||||
|
||||
// 2. Use first model from scoped models (skip if continuing/resuming)
|
||||
if (scopedModels.length > 0 && !isContinuing) {
|
||||
const scoped = scopedModels[0];
|
||||
const scopedThinkingSelector = scoped.thinkingLevel ?? defaultThinkingSelector ?? "off";
|
||||
const scopedThinkingSelector =
|
||||
scoped.thinkingLevel === ThinkingLevel.Inherit
|
||||
? defaultThinkingSelector
|
||||
: (scoped.thinkingLevel ?? defaultThinkingSelector);
|
||||
return {
|
||||
model: scoped.model,
|
||||
thinkingLevel: scopedThinkingSelector,
|
||||
thinkingLevel:
|
||||
scopedThinkingSelector === ThinkingLevel.Off
|
||||
? ThinkingLevel.Off
|
||||
: clampThinkingLevelForModel(scoped.model, scopedThinkingSelector),
|
||||
fallbackMessage: undefined,
|
||||
};
|
||||
}
|
||||
@@ -706,9 +734,7 @@ export async function findInitialModel(options: {
|
||||
const found = modelRegistry.find(defaultProvider, defaultModelId);
|
||||
if (found) {
|
||||
model = found;
|
||||
if (defaultThinkingSelector) {
|
||||
thinkingLevel = defaultThinkingSelector;
|
||||
}
|
||||
thinkingLevel = clampThinkingLevelForModel(found, defaultThinkingSelector);
|
||||
return { model, thinkingLevel, fallbackMessage: undefined };
|
||||
}
|
||||
}
|
||||
@@ -722,16 +748,16 @@ export async function findInitialModel(options: {
|
||||
const defaultId = defaultModelPerProvider[provider];
|
||||
const match = availableModels.find(m => m.provider === provider && m.id === defaultId);
|
||||
if (match) {
|
||||
return { model: match, thinkingLevel: "off", fallbackMessage: undefined };
|
||||
return { model: match, thinkingLevel: undefined, fallbackMessage: undefined };
|
||||
}
|
||||
}
|
||||
|
||||
// If no default found, use first available
|
||||
return { model: availableModels[0], thinkingLevel: "off", fallbackMessage: undefined };
|
||||
return { model: availableModels[0], thinkingLevel: undefined, fallbackMessage: undefined };
|
||||
}
|
||||
|
||||
// 5. No model found
|
||||
return { model: undefined, thinkingLevel: "off", fallbackMessage: undefined };
|
||||
return { model: undefined, thinkingLevel: undefined, fallbackMessage: undefined };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getAvailableThinkingLevels } from "@oh-my-pi/pi-ai";
|
||||
import { THINKING_EFFORTS } from "@oh-my-pi/pi-ai";
|
||||
|
||||
/** Unified settings schema - single source of truth for all settings.
|
||||
* Unified settings schema - single source of truth for all settings.
|
||||
@@ -192,7 +192,7 @@ export const SETTINGS_SCHEMA = {
|
||||
},
|
||||
defaultThinkingLevel: {
|
||||
type: "enum",
|
||||
values: getAvailableThinkingLevels(),
|
||||
values: THINKING_EFFORTS,
|
||||
default: "high",
|
||||
ui: {
|
||||
tab: "agent",
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import type { ThinkingLevel } from "@oh-my-pi/pi-ai";
|
||||
import { parseThinkingLevel } from "@oh-my-pi/pi-ai";
|
||||
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import { FileType, glob } from "@oh-my-pi/pi-natives";
|
||||
import { CONFIG_DIR_NAME, tryParseJson } from "@oh-my-pi/pi-utils";
|
||||
import { readFile } from "../capability/fs";
|
||||
import { parseRuleConditionAndScope, type Rule, type RuleFrontmatter } from "../capability/rule";
|
||||
import type { Skill, SkillFrontmatter } from "../capability/skill";
|
||||
import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
|
||||
import { parseThinkingLevel } from "../thinking";
|
||||
import { parseFrontmatter } from "../utils/frontmatter";
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
import type * as fs1 from "node:fs";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
import type { ImageContent, Model, TextContent, ThinkingLevel } from "@oh-my-pi/pi-ai";
|
||||
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import type { ImageContent, Model, TextContent } from "@oh-my-pi/pi-ai";
|
||||
import * as piCodingAgent from "@oh-my-pi/pi-coding-agent";
|
||||
import type { KeyId } from "@oh-my-pi/pi-tui";
|
||||
import { hasFsCode, isEacces, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
||||
@@ -214,7 +215,7 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
|
||||
return this.runtime.setModel(model);
|
||||
}
|
||||
|
||||
getThinkingLevel(): ThinkingLevel {
|
||||
getThinkingLevel(): ThinkingLevel | undefined {
|
||||
return this.runtime.getThinkingLevel();
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* - Register commands, keyboard shortcuts, and CLI flags
|
||||
* - Interact with the user via UI primitives
|
||||
*/
|
||||
import type { AgentMessage, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
||||
import type { AgentMessage, AgentToolResult, AgentToolUpdateCallback, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import type {
|
||||
Api,
|
||||
AssistantMessageEvent,
|
||||
@@ -19,7 +19,6 @@ import type {
|
||||
OAuthLoginCallbacks,
|
||||
SimpleStreamOptions,
|
||||
TextContent,
|
||||
ThinkingLevel,
|
||||
ToolResultMessage,
|
||||
} from "@oh-my-pi/pi-ai";
|
||||
import type * as piCodingAgent from "@oh-my-pi/pi-coding-agent";
|
||||
@@ -1058,9 +1057,9 @@ export interface ExtensionAPI {
|
||||
setModel(model: Model): Promise<boolean>;
|
||||
|
||||
/** Get current thinking level. */
|
||||
getThinkingLevel(): ThinkingLevel;
|
||||
getThinkingLevel(): ThinkingLevel | undefined;
|
||||
|
||||
/** Set thinking level (clamped to model capabilities). */
|
||||
/** Set thinking level for the current session. */
|
||||
setThinkingLevel(level: ThinkingLevel): void;
|
||||
|
||||
// =========================================================================
|
||||
@@ -1086,11 +1085,11 @@ export interface ExtensionAPI {
|
||||
* id: "claude-sonnet-4@20250514",
|
||||
* name: "Claude Sonnet 4 (Vertex)",
|
||||
* reasoning: true,
|
||||
* thinking: { mode: "anthropic-adaptive", minLevel: "minimal", maxLevel: "high" },
|
||||
* input: ["text", "image"],
|
||||
* cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
||||
* contextWindow: 200000,
|
||||
* maxTokens: 64000,
|
||||
* }
|
||||
* ]
|
||||
* });
|
||||
*
|
||||
@@ -1149,8 +1148,10 @@ export interface ProviderModelConfig {
|
||||
name: string;
|
||||
/** API type override for this model. */
|
||||
api?: Api;
|
||||
/** Whether the model supports extended thinking. */
|
||||
/** Whether the model supports extended thinking at all. */
|
||||
reasoning: boolean;
|
||||
/** Optional canonical thinking capability metadata for per-model effort support. */
|
||||
thinking?: Model["thinking"];
|
||||
/** Supported input types. */
|
||||
input: ("text" | "image")[];
|
||||
/** Cost per million tokens. */
|
||||
@@ -1218,7 +1219,7 @@ export type SetActiveToolsHandler = (toolNames: string[]) => Promise<void>;
|
||||
|
||||
export type SetModelHandler = (model: Model) => Promise<boolean>;
|
||||
|
||||
export type GetThinkingLevelHandler = () => ThinkingLevel;
|
||||
export type GetThinkingLevelHandler = () => ThinkingLevel | undefined;
|
||||
|
||||
export type SetThinkingLevelHandler = (level: ThinkingLevel, persist?: boolean) => void;
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { createInterface } from "node:readline/promises";
|
||||
import { type ImageContent, supportsXhigh } from "@oh-my-pi/pi-ai";
|
||||
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
||||
import { $env, getProjectDir, logger, postmortem, setProjectDir, VERSION } from "@oh-my-pi/pi-utils";
|
||||
import chalk from "chalk";
|
||||
import type { Args } from "./cli/args";
|
||||
@@ -334,11 +334,10 @@ async function buildSessionOptions(
|
||||
scopedModels: ScopedModel[],
|
||||
sessionManager: SessionManager | undefined,
|
||||
modelRegistry: ModelRegistry,
|
||||
): Promise<{ options: CreateAgentSessionOptions; cliThinkingFromModel: boolean }> {
|
||||
): Promise<{ options: CreateAgentSessionOptions }> {
|
||||
const options: CreateAgentSessionOptions = {
|
||||
cwd: parsed.cwd ?? getProjectDir(),
|
||||
};
|
||||
let cliThinkingFromModel = false;
|
||||
|
||||
// Auto-discover SYSTEM.md if no CLI system prompt provided
|
||||
const systemPromptSource = parsed.systemPrompt ?? discoverSystemPromptFile();
|
||||
@@ -380,7 +379,6 @@ async function buildSessionOptions(
|
||||
settings.overrideModelRoles({ default: `${resolved.model.provider}/${resolved.model.id}` });
|
||||
if (!parsed.thinking && resolved.thinkingLevel) {
|
||||
options.thinkingLevel = resolved.thinkingLevel;
|
||||
cliThinkingFromModel = true;
|
||||
}
|
||||
}
|
||||
} else if (scopedModels.length > 0 && !parsed.continue && !parsed.resume) {
|
||||
@@ -483,7 +481,7 @@ async function buildSessionOptions(
|
||||
options.additionalExtensionPaths = [];
|
||||
}
|
||||
|
||||
return { options, cliThinkingFromModel };
|
||||
return { options };
|
||||
}
|
||||
|
||||
export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<void> {
|
||||
@@ -618,7 +616,7 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
||||
sessionManager = await SessionManager.open(selectedPath);
|
||||
}
|
||||
|
||||
const { options: sessionOptions, cliThinkingFromModel } = await logger.timeAsync("buildSessionOptions", () =>
|
||||
const { options: sessionOptions } = await logger.timeAsync("buildSessionOptions", () =>
|
||||
buildSessionOptions(parsedArgs, scopedModels, sessionManager, modelRegistry),
|
||||
);
|
||||
sessionOptions.authStorage = authStorage;
|
||||
@@ -692,21 +690,6 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Clamp thinking level to model capabilities for CLI-provided thinking levels.
|
||||
// This covers both --thinking <level> and --model <pattern>:<thinking>.
|
||||
const cliThinkingOverride = parsedArgs.thinking !== undefined || cliThinkingFromModel;
|
||||
if (session.model && cliThinkingOverride) {
|
||||
let effectiveThinking = session.thinkingLevel;
|
||||
if (!session.model.reasoning) {
|
||||
effectiveThinking = "off";
|
||||
} else if (effectiveThinking === "xhigh" && !supportsXhigh(session.model)) {
|
||||
effectiveThinking = "high";
|
||||
}
|
||||
if (effectiveThinking !== session.thinkingLevel) {
|
||||
session.setThinkingLevel(effectiveThinking);
|
||||
}
|
||||
}
|
||||
|
||||
if (mode === "rpc") {
|
||||
await runRpcMode(session);
|
||||
} else if (isInteractive) {
|
||||
@@ -717,7 +700,7 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
||||
if (scopedModelsForDisplay.length > 0) {
|
||||
const modelList = scopedModelsForDisplay
|
||||
.map(scopedModel => {
|
||||
const thinkingStr = scopedModel.thinkingLevel !== "off" ? `:${scopedModel.thinkingLevel}` : "";
|
||||
const thinkingStr = !scopedModel.thinkingLevel ? `:${scopedModel.thinkingLevel}` : "";
|
||||
return `${scopedModel.model.id}${thinkingStr}`;
|
||||
})
|
||||
.join(", ");
|
||||
|
||||
@@ -3,7 +3,7 @@ import type * as fsNode from "node:fs";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
||||
import { completeSimple, type Model } from "@oh-my-pi/pi-ai";
|
||||
import { completeSimple, Effort, type Model } from "@oh-my-pi/pi-ai";
|
||||
import { getAgentDbPath, logger, parseJsonlLenient } from "@oh-my-pi/pi-utils";
|
||||
import type { ModelRegistry } from "../config/model-registry";
|
||||
import { parseModelString } from "../config/model-resolver";
|
||||
@@ -583,7 +583,11 @@ async function runStage1Job(options: {
|
||||
systemPrompt: stageOneSystemTemplate,
|
||||
messages: [{ role: "user", content: [{ type: "text", text: inputPrompt }], timestamp: Date.now() }],
|
||||
},
|
||||
{ apiKey, maxTokens: Math.max(1024, Math.min(4096, Math.floor(modelMaxTokens * 0.2))), reasoning: "low" },
|
||||
{
|
||||
apiKey,
|
||||
maxTokens: Math.max(1024, Math.min(4096, Math.floor(modelMaxTokens * 0.2))),
|
||||
reasoning: Effort.Low,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.stopReason === "error") {
|
||||
@@ -709,7 +713,7 @@ async function runConsolidationModel(options: { memoryRoot: string; model: Model
|
||||
{
|
||||
messages: [{ role: "user", content: [{ type: "text", text: input }], timestamp: Date.now() }],
|
||||
},
|
||||
{ apiKey, maxTokens: 8192, reasoning: "medium" },
|
||||
{ apiKey, maxTokens: 8192, reasoning: Effort.Medium },
|
||||
);
|
||||
if (response.stopReason === "error") {
|
||||
throw new Error(response.errorMessage || "phase2 model error");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as fs from "node:fs";
|
||||
import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import { type Component, padding, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
|
||||
import { formatNumber, getProjectDir } from "@oh-my-pi/pi-utils";
|
||||
import { theme } from "../../modes/theme/theme";
|
||||
@@ -212,11 +213,11 @@ export class FooterComponent implements Component {
|
||||
// Add model name on the right side, plus thinking level if model supports it
|
||||
const modelName = state.model?.id || "no-model";
|
||||
|
||||
// Add thinking level hint if model supports reasoning and thinking is enabled
|
||||
// Add thinking level hint when the current model advertises supported efforts
|
||||
let rightSide = modelName;
|
||||
if (state.model?.reasoning) {
|
||||
const thinkingLevel = state.thinkingLevel || "off";
|
||||
if (thinkingLevel !== "off") {
|
||||
if (state.model?.thinking) {
|
||||
const thinkingLevel = state.thinkingLevel ?? ThinkingLevel.Off;
|
||||
if (thinkingLevel !== ThinkingLevel.Off) {
|
||||
rightSide = `${modelName} • ${thinkingLevel}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
import {
|
||||
getAvailableThinkingLevels,
|
||||
getThinkingMetadata,
|
||||
type Model,
|
||||
modelsAreEqual,
|
||||
supportsXhigh,
|
||||
type ThinkingMode,
|
||||
} from "@oh-my-pi/pi-ai";
|
||||
import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import { getSupportedEfforts, type Model, modelsAreEqual } from "@oh-my-pi/pi-ai";
|
||||
import { Container, Input, matchesKey, Spacer, type Tab, TabBar, Text, type TUI, visibleWidth } from "@oh-my-pi/pi-tui";
|
||||
import { MODEL_ROLE_IDS, MODEL_ROLES, type ModelRegistry, type ModelRole } from "../../config/model-registry";
|
||||
import { resolveModelRoleValue } from "../../config/model-resolver";
|
||||
import type { Settings } from "../../config/settings";
|
||||
import { type ThemeColor, theme } from "../../modes/theme/theme";
|
||||
import { getThinkingLevelMetadata } from "../../thinking";
|
||||
import { fuzzyFilter } from "../../utils/fuzzy";
|
||||
import { getTabBarTheme } from "../shared";
|
||||
import { DynamicBorder } from "./dynamic-border";
|
||||
@@ -29,15 +24,15 @@ interface ModelItem {
|
||||
|
||||
interface ScopedModelItem {
|
||||
model: Model;
|
||||
thinkingLevel: string;
|
||||
thinkingLevel?: string;
|
||||
}
|
||||
|
||||
interface RoleAssignment {
|
||||
model: Model;
|
||||
thinkingMode: ThinkingMode;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
}
|
||||
|
||||
type RoleSelectCallback = (model: Model, role: ModelRole | null, thinkingMode?: ThinkingMode) => void;
|
||||
type RoleSelectCallback = (model: Model, role: ModelRole | null, thinkingLevel?: ThinkingLevel) => void;
|
||||
type CancelCallback = () => void;
|
||||
interface MenuRoleAction {
|
||||
label: string;
|
||||
@@ -97,7 +92,7 @@ export class ModelSelectorComponent extends Container {
|
||||
settings: Settings,
|
||||
modelRegistry: ModelRegistry,
|
||||
scopedModels: ReadonlyArray<ScopedModelItem>,
|
||||
onSelect: (model: Model, role: ModelRole | null, thinkingMode?: ThinkingMode) => void,
|
||||
onSelect: (model: Model, role: ModelRole | null, thinkingLevel?: ThinkingLevel) => void,
|
||||
onCancel: () => void,
|
||||
options?: { temporaryOnly?: boolean; initialSearchInput?: string },
|
||||
) {
|
||||
@@ -192,7 +187,8 @@ export class ModelSelectorComponent extends Container {
|
||||
if (model) {
|
||||
this.#roles[role] = {
|
||||
model,
|
||||
thinkingMode: explicitThinkingLevel && thinkingLevel !== undefined ? thinkingLevel : "inherit",
|
||||
thinkingLevel:
|
||||
explicitThinkingLevel && thinkingLevel !== undefined ? thinkingLevel : ThinkingLevel.Inherit,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -409,7 +405,7 @@ export class ModelSelectorComponent extends Container {
|
||||
if (!tag || !assigned || !modelsAreEqual(assigned.model, item.model)) continue;
|
||||
|
||||
const badge = makeInvertedBadge(tag, color ?? "success");
|
||||
const thinkingLabel = getThinkingMetadata(assigned.thinkingMode).label;
|
||||
const thinkingLabel = getThinkingLevelMetadata(assigned.thinkingLevel).label;
|
||||
roleBadgeTokens.push(`${badge} ${theme.fg("dim", `(${thinkingLabel})`)}`);
|
||||
}
|
||||
const badgeText = roleBadgeTokens.length > 0 ? ` ${roleBadgeTokens.join(" ")}` : "";
|
||||
@@ -456,19 +452,18 @@ export class ModelSelectorComponent extends Container {
|
||||
this.#listContainer.addChild(new Text(theme.fg("muted", ` Model Name: ${selected.model.name}`), 0, 0));
|
||||
}
|
||||
}
|
||||
#getThinkingModesForModel(model: Model): ReadonlyArray<ThinkingMode> {
|
||||
return ["inherit", ...getAvailableThinkingLevels(supportsXhigh(model))];
|
||||
#getThinkingLevelsForModel(model: Model): ReadonlyArray<ThinkingLevel> {
|
||||
return [ThinkingLevel.Inherit, ThinkingLevel.Off, ...getSupportedEfforts(model)];
|
||||
}
|
||||
|
||||
#getCurrentRoleThinkingMode(role: ModelRole): ThinkingMode {
|
||||
return this.#roles[role]?.thinkingMode ?? "inherit";
|
||||
#getCurrentRoleThinkingLevel(role: ModelRole): ThinkingLevel {
|
||||
return this.#roles[role]?.thinkingLevel ?? ThinkingLevel.Inherit;
|
||||
}
|
||||
|
||||
#getThinkingPreselectIndex(role: ModelRole, model: Model): number {
|
||||
const options = this.#getThinkingModesForModel(model);
|
||||
const currentMode = this.#getCurrentRoleThinkingMode(role);
|
||||
const preferredMode = currentMode === "xhigh" && !options.includes("xhigh") ? "high" : currentMode;
|
||||
const foundIndex = options.indexOf(preferredMode);
|
||||
const options = this.#getThinkingLevelsForModel(model);
|
||||
const currentLevel = this.#getCurrentRoleThinkingLevel(role);
|
||||
const foundIndex = options.indexOf(currentLevel);
|
||||
return foundIndex >= 0 ? foundIndex : 0;
|
||||
}
|
||||
|
||||
@@ -496,11 +491,11 @@ export class ModelSelectorComponent extends Container {
|
||||
if (!selectedModel) return;
|
||||
|
||||
const showingThinking = this.#menuStep === "thinking" && this.#menuSelectedRole !== null;
|
||||
const thinkingOptions = showingThinking ? this.#getThinkingModesForModel(selectedModel.model) : [];
|
||||
const thinkingOptions = showingThinking ? this.#getThinkingLevelsForModel(selectedModel.model) : [];
|
||||
const optionLines = showingThinking
|
||||
? thinkingOptions.map((thinkingMode, index) => {
|
||||
? thinkingOptions.map((thinkingLevel, index) => {
|
||||
const prefix = index === this.#menuSelectedIndex ? ` ${theme.nav.cursor} ` : " ";
|
||||
const label = getThinkingMetadata(thinkingMode).label;
|
||||
const label = getThinkingLevelMetadata(thinkingLevel).label;
|
||||
return `${prefix}${label}`;
|
||||
})
|
||||
: MENU_ROLE_ACTIONS.map((action, index) => {
|
||||
@@ -607,7 +602,7 @@ export class ModelSelectorComponent extends Container {
|
||||
|
||||
const optionCount =
|
||||
this.#menuStep === "thinking" && this.#menuSelectedRole !== null
|
||||
? this.#getThinkingModesForModel(selectedModel.model).length
|
||||
? this.#getThinkingLevelsForModel(selectedModel.model).length
|
||||
: MENU_ROLE_ACTIONS.length;
|
||||
if (optionCount === 0) return;
|
||||
|
||||
@@ -635,10 +630,10 @@ export class ModelSelectorComponent extends Container {
|
||||
}
|
||||
|
||||
if (!this.#menuSelectedRole) return;
|
||||
const thinkingOptions = this.#getThinkingModesForModel(selectedModel.model);
|
||||
const thinkingMode = thinkingOptions[this.#menuSelectedIndex];
|
||||
if (!thinkingMode) return;
|
||||
this.#handleSelect(selectedModel.model, this.#menuSelectedRole, thinkingMode);
|
||||
const thinkingOptions = this.#getThinkingLevelsForModel(selectedModel.model);
|
||||
const thinkingLevel = thinkingOptions[this.#menuSelectedIndex];
|
||||
if (!thinkingLevel) return;
|
||||
this.#handleSelect(selectedModel.model, this.#menuSelectedRole, thinkingLevel);
|
||||
this.#closeMenu();
|
||||
return;
|
||||
}
|
||||
@@ -657,28 +652,28 @@ export class ModelSelectorComponent extends Container {
|
||||
}
|
||||
}
|
||||
|
||||
#formatRoleModelValue(model: Model, thinkingMode: ThinkingMode): string {
|
||||
#formatRoleModelValue(model: Model, thinkingLevel: ThinkingLevel): string {
|
||||
const modelKey = `${model.provider}/${model.id}`;
|
||||
if (thinkingMode === "inherit") return modelKey;
|
||||
return `${modelKey}:${thinkingMode}`;
|
||||
if (thinkingLevel === ThinkingLevel.Inherit) return modelKey;
|
||||
return `${modelKey}:${thinkingLevel}`;
|
||||
}
|
||||
#handleSelect(model: Model, role: ModelRole | null, thinkingMode?: ThinkingMode): void {
|
||||
#handleSelect(model: Model, role: ModelRole | null, thinkingLevel?: ThinkingLevel): void {
|
||||
// For temporary role, don't save to settings - just notify caller
|
||||
if (role === null) {
|
||||
this.#onSelectCallback(model, null);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedThinkingMode = thinkingMode ?? this.#getCurrentRoleThinkingMode(role);
|
||||
const selectedThinkingLevel = thinkingLevel ?? this.#getCurrentRoleThinkingLevel(role);
|
||||
|
||||
// Save to settings
|
||||
this.#settings.setModelRole(role, this.#formatRoleModelValue(model, selectedThinkingMode));
|
||||
this.#settings.setModelRole(role, this.#formatRoleModelValue(model, selectedThinkingLevel));
|
||||
|
||||
// Update local state for UI
|
||||
this.#roles[role] = { model, thinkingMode: selectedThinkingMode };
|
||||
this.#roles[role] = { model, thinkingLevel: selectedThinkingLevel };
|
||||
|
||||
// Notify caller (for updating agent state if needed)
|
||||
this.#onSelectCallback(model, role, selectedThinkingMode);
|
||||
this.#onSelectCallback(model, role, selectedThinkingLevel);
|
||||
|
||||
// Update list to show new badges
|
||||
this.#updateList();
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* 2. That's it - it appears in the UI automatically
|
||||
*/
|
||||
|
||||
import { getAvailableThinkingLevels, getThinkingMetadata } from "@oh-my-pi/pi-ai";
|
||||
import { THINKING_EFFORTS } from "@oh-my-pi/pi-ai";
|
||||
import { TERMINAL } from "@oh-my-pi/pi-tui";
|
||||
import {
|
||||
getDefault,
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
type SettingPath,
|
||||
type SettingTab,
|
||||
} from "../../config/settings-schema";
|
||||
import { getThinkingLevelMetadata } from "../../thinking";
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// UI Definition Types
|
||||
@@ -251,7 +252,7 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
|
||||
{ value: "on", label: "On", description: "Force websockets for OpenAI Codex models" },
|
||||
],
|
||||
// Default thinking level
|
||||
defaultThinkingLevel: [...getAvailableThinkingLevels().map(getThinkingMetadata)],
|
||||
defaultThinkingLevel: [...THINKING_EFFORTS.map(getThinkingLevelMetadata)],
|
||||
// Temperature
|
||||
temperature: [
|
||||
{ value: "-1", label: "Default", description: "Use provider default" },
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ThinkingLevel } from "@oh-my-pi/pi-ai";
|
||||
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import type { Effort } from "@oh-my-pi/pi-ai";
|
||||
import {
|
||||
Container,
|
||||
matchesKey,
|
||||
@@ -134,9 +135,9 @@ function getSettingsTabs(): Tab[] {
|
||||
*/
|
||||
export interface SettingsRuntimeContext {
|
||||
/** Available thinking levels (from session) */
|
||||
availableThinkingLevels: ThinkingLevel[];
|
||||
availableThinkingLevels: Effort[];
|
||||
/** Current thinking level (from session) */
|
||||
thinkingLevel: ThinkingLevel;
|
||||
thinkingLevel: ThinkingLevel | undefined;
|
||||
/** Available themes */
|
||||
availableThemes: string[];
|
||||
/** Working directory for plugins tab */
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as os from "node:os";
|
||||
import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import { TERMINAL } from "@oh-my-pi/pi-tui";
|
||||
import { formatDuration, formatNumber, getProjectDir } from "@oh-my-pi/pi-utils";
|
||||
import { theme } from "../../../modes/theme/theme";
|
||||
@@ -50,9 +51,9 @@ const modelSegment: StatusLineSegment = {
|
||||
}
|
||||
|
||||
// Add thinking level with dot separator
|
||||
if (opts.showThinkingLevel !== false && state.model?.reasoning) {
|
||||
const level = state.thinkingLevel || "off";
|
||||
if (level !== "off") {
|
||||
if (opts.showThinkingLevel !== false && state.model?.thinking) {
|
||||
const level = state.thinkingLevel ?? ThinkingLevel.Off;
|
||||
if (level !== ThinkingLevel.Off) {
|
||||
const thinkingText = theme.thinking[level as keyof typeof theme.thinking];
|
||||
if (thinkingText) {
|
||||
content += `${theme.sep.dot}${thinkingText}`;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getThinkingMetadata, type ThinkingLevel } from "@oh-my-pi/pi-ai";
|
||||
|
||||
import type { Effort } from "@oh-my-pi/pi-ai";
|
||||
import { Container, type SelectItem, SelectList } from "@oh-my-pi/pi-tui";
|
||||
import { getSelectListTheme } from "../../modes/theme/theme";
|
||||
import { getThinkingLevelMetadata } from "../../thinking";
|
||||
import { DynamicBorder } from "./dynamic-border";
|
||||
|
||||
/**
|
||||
@@ -11,14 +11,14 @@ export class ThinkingSelectorComponent extends Container {
|
||||
#selectList: SelectList;
|
||||
|
||||
constructor(
|
||||
currentLevel: ThinkingLevel,
|
||||
availableLevels: ThinkingLevel[],
|
||||
onSelect: (level: ThinkingLevel) => void,
|
||||
currentLevel: Effort,
|
||||
availableLevels: Effort[],
|
||||
onSelect: (level: Effort) => void,
|
||||
onCancel: () => void,
|
||||
) {
|
||||
super();
|
||||
|
||||
const thinkingLevels: SelectItem[] = availableLevels.map(getThinkingMetadata);
|
||||
const thinkingLevels: SelectItem[] = availableLevels.map(getThinkingLevelMetadata);
|
||||
|
||||
// Add top border
|
||||
this.addChild(new DynamicBorder());
|
||||
@@ -33,7 +33,7 @@ export class ThinkingSelectorComponent extends Container {
|
||||
}
|
||||
|
||||
this.#selectList.onSelect = item => {
|
||||
onSelect(item.value as ThinkingLevel);
|
||||
onSelect(item.value as Effort);
|
||||
};
|
||||
|
||||
this.#selectList.onCancel = () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import {
|
||||
type Component,
|
||||
Container,
|
||||
@@ -382,7 +383,7 @@ class TreeList implements Component {
|
||||
parts.push("model", entry.model);
|
||||
break;
|
||||
case "thinking_level_change":
|
||||
parts.push("thinking", entry.thinkingLevel);
|
||||
parts.push("thinking", entry.thinkingLevel ?? ThinkingLevel.Off);
|
||||
break;
|
||||
case "custom":
|
||||
parts.push("custom", entry.customType);
|
||||
@@ -585,7 +586,7 @@ class TreeList implements Component {
|
||||
result = theme.fg("dim", `[model: ${entry.model}]`);
|
||||
break;
|
||||
case "thinking_level_change":
|
||||
result = theme.fg("dim", `[thinking: ${entry.thinkingLevel}]`);
|
||||
result = theme.fg("dim", `[thinking: ${entry.thinkingLevel ?? ThinkingLevel.Off}]`);
|
||||
break;
|
||||
case "custom":
|
||||
result = theme.fg("dim", `[custom: ${entry.customType}]`);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
||||
import { type AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import { copyToClipboard, readImageFromClipboard, sanitizeText } from "@oh-my-pi/pi-natives";
|
||||
import { $env } from "@oh-my-pi/pi-utils";
|
||||
import { settings } from "../../config/settings";
|
||||
@@ -544,7 +544,9 @@ export class InputController {
|
||||
const roleLabel = result.role === "default" ? "default" : result.role;
|
||||
const roleLabelStyled = theme.bold(theme.fg("accent", roleLabel));
|
||||
const thinkingStr =
|
||||
result.model.reasoning && result.thinkingLevel !== "off" ? ` (thinking: ${result.thinkingLevel})` : "";
|
||||
result.model.thinking && result.thinkingLevel !== ThinkingLevel.Off
|
||||
? ` (thinking: ${result.thinkingLevel})`
|
||||
: "";
|
||||
const tempLabel = options?.temporary ? " (temporary)" : "";
|
||||
const cycleSeparator = theme.fg("dim", " > ");
|
||||
const cycleLabel = roleOrder
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getOAuthProviders, type OAuthProvider, type ThinkingLevel } from "@oh-my-pi/pi-ai";
|
||||
import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import { getOAuthProviders, type OAuthProvider } from "@oh-my-pi/pi-ai";
|
||||
import type { Component } from "@oh-my-pi/pi-tui";
|
||||
import { Input, Loader, Spacer, Text } from "@oh-my-pi/pi-tui";
|
||||
import { getAgentDbPath, getProjectDir } from "@oh-my-pi/pi-utils";
|
||||
@@ -380,7 +381,7 @@ export class SelectorController {
|
||||
this.ctx.settings,
|
||||
this.ctx.session.modelRegistry,
|
||||
this.ctx.session.scopedModels,
|
||||
async (model, role, thinkingMode) => {
|
||||
async (model, role, thinkingLevel) => {
|
||||
try {
|
||||
if (role === null) {
|
||||
// Temporary: update agent state but don't persist to settings
|
||||
@@ -393,8 +394,8 @@ export class SelectorController {
|
||||
} else if (role === "default") {
|
||||
// Default: update agent state and persist
|
||||
await this.ctx.session.setModel(model, role);
|
||||
if (thinkingMode && thinkingMode !== "inherit") {
|
||||
this.ctx.session.setThinkingLevel(thinkingMode as ThinkingLevel);
|
||||
if (thinkingLevel && thinkingLevel !== ThinkingLevel.Inherit) {
|
||||
this.ctx.session.setThinkingLevel(thinkingLevel);
|
||||
}
|
||||
this.ctx.statusLine.invalidate();
|
||||
this.ctx.updateEditorBorderColor();
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Handles TUI rendering and user interaction, delegating business logic to AgentSession.
|
||||
*/
|
||||
import * as path from "node:path";
|
||||
import type { Agent, AgentMessage } from "@oh-my-pi/pi-agent-core";
|
||||
import { type Agent, type AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import type { AssistantMessage, ImageContent, Message, Model, UsageReport } from "@oh-my-pi/pi-ai";
|
||||
import type { Component, Loader, SlashCommand } from "@oh-my-pi/pi-tui";
|
||||
import {
|
||||
@@ -423,7 +423,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
} else if (this.isPythonMode) {
|
||||
this.editor.borderColor = theme.getPythonModeBorderColor();
|
||||
} else {
|
||||
const level = this.session.thinkingLevel || "off";
|
||||
const level = this.session.thinkingLevel ?? ThinkingLevel.Off;
|
||||
this.editor.borderColor = theme.getThinkingBorderColor(level);
|
||||
}
|
||||
this.updateEditorTopBorder();
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
*
|
||||
* Spawns the agent in RPC mode and provides a typed API for all operations.
|
||||
*/
|
||||
import type { AgentEvent, AgentMessage } from "@oh-my-pi/pi-agent-core";
|
||||
import type { ImageContent, ThinkingLevel } from "@oh-my-pi/pi-ai";
|
||||
import type { AgentEvent, AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import type { Effort, ImageContent, Model } from "@oh-my-pi/pi-ai";
|
||||
import { isRecord, ptree, readJsonl } from "@oh-my-pi/pi-utils";
|
||||
import type { BashResult } from "../../exec/bash-executor";
|
||||
import type { SessionStats } from "../../session/agent-session";
|
||||
@@ -34,12 +34,7 @@ export interface RpcClientOptions {
|
||||
args?: string[];
|
||||
}
|
||||
|
||||
export interface ModelInfo {
|
||||
provider: string;
|
||||
id: string;
|
||||
contextWindow: number;
|
||||
reasoning: boolean;
|
||||
}
|
||||
export type ModelInfo = Pick<Model, "provider" | "id" | "contextWindow" | "reasoning" | "thinking">;
|
||||
|
||||
export type RpcEventListener = (event: AgentEvent) => void;
|
||||
|
||||
@@ -284,7 +279,7 @@ export class RpcClient {
|
||||
*/
|
||||
async cycleModel(): Promise<{
|
||||
model: { provider: string; id: string };
|
||||
thinkingLevel: ThinkingLevel;
|
||||
thinkingLevel: ThinkingLevel | undefined;
|
||||
isScoped: boolean;
|
||||
} | null> {
|
||||
const response = await this.#send({ type: "cycle_model" });
|
||||
@@ -309,7 +304,7 @@ export class RpcClient {
|
||||
/**
|
||||
* Cycle thinking level.
|
||||
*/
|
||||
async cycleThinkingLevel(): Promise<{ level: ThinkingLevel } | null> {
|
||||
async cycleThinkingLevel(): Promise<{ level: Effort } | null> {
|
||||
const response = await this.#send({ type: "cycle_thinking_level" });
|
||||
return this.#getData(response);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
* Commands are sent as JSON lines on stdin.
|
||||
* Responses and events are emitted as JSON lines on stdout.
|
||||
*/
|
||||
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
||||
import type { ImageContent, Model, ThinkingLevel } from "@oh-my-pi/pi-ai";
|
||||
import type { AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import type { Effort, ImageContent, Model } from "@oh-my-pi/pi-ai";
|
||||
import type { BashResult } from "../../exec/bash-executor";
|
||||
import type { SessionStats } from "../../session/agent-session";
|
||||
import type { CompactionResult } from "../../session/compaction";
|
||||
@@ -70,7 +70,7 @@ export type RpcCommand =
|
||||
|
||||
export interface RpcSessionState {
|
||||
model?: Model;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
thinkingLevel: ThinkingLevel | undefined;
|
||||
isStreaming: boolean;
|
||||
isCompacting: boolean;
|
||||
steeringMode: "all" | "one-at-a-time";
|
||||
@@ -114,7 +114,7 @@ export type RpcResponse =
|
||||
type: "response";
|
||||
command: "cycle_model";
|
||||
success: true;
|
||||
data: { model: Model; thinkingLevel: ThinkingLevel; isScoped: boolean } | null;
|
||||
data: { model: Model; thinkingLevel: ThinkingLevel | undefined; isScoped: boolean } | null;
|
||||
}
|
||||
| {
|
||||
id?: string;
|
||||
@@ -131,7 +131,7 @@ export type RpcResponse =
|
||||
type: "response";
|
||||
command: "cycle_thinking_level";
|
||||
success: true;
|
||||
data: { level: ThinkingLevel } | null;
|
||||
data: { level: Effort } | null;
|
||||
}
|
||||
|
||||
// Queue modes
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import type { ThinkingLevel } from "@oh-my-pi/pi-ai";
|
||||
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import type { Effort } from "@oh-my-pi/pi-ai";
|
||||
import {
|
||||
detectMacOSAppearance,
|
||||
type HighlightColors as NativeHighlightColors,
|
||||
@@ -1223,7 +1224,7 @@ export class Theme {
|
||||
return this.mode;
|
||||
}
|
||||
|
||||
getThinkingBorderColor(level: ThinkingLevel): (str: string) => string {
|
||||
getThinkingBorderColor(level: ThinkingLevel | Effort): (str: string) => string {
|
||||
// Map thinking levels to dedicated theme colors
|
||||
switch (level) {
|
||||
case "off":
|
||||
|
||||
@@ -1 +1 @@
|
||||
Threshold-triggered maintenance: preserve critical implementation state and immediate next actions.
|
||||
Threshold-triggered maintenance: preserve critical implementation state and immediate next actions.
|
||||
@@ -1,5 +1,12 @@
|
||||
import { Agent, type AgentEvent, type AgentMessage, type AgentTool, INTENT_FIELD } from "@oh-my-pi/pi-agent-core";
|
||||
import { type Message, type Model, supportsXhigh, type ThinkingLevel } from "@oh-my-pi/pi-ai";
|
||||
import {
|
||||
Agent,
|
||||
type AgentEvent,
|
||||
type AgentMessage,
|
||||
type AgentTool,
|
||||
INTENT_FIELD,
|
||||
type ThinkingLevel,
|
||||
} from "@oh-my-pi/pi-agent-core";
|
||||
import type { Message, Model } from "@oh-my-pi/pi-ai";
|
||||
|
||||
import { prewarmOpenAICodexResponses } from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
|
||||
import type { Component } from "@oh-my-pi/pi-tui";
|
||||
@@ -72,6 +79,7 @@ import {
|
||||
loadProjectContextFiles as loadContextFilesInternal,
|
||||
} from "./system-prompt";
|
||||
import { AgentOutputManager } from "./task/output-manager";
|
||||
import { resolveThinkingLevelForModel, toReasoningEffort } from "./thinking";
|
||||
import {
|
||||
BashTool,
|
||||
BUILTIN_TOOLS,
|
||||
@@ -117,10 +125,10 @@ export interface CreateAgentSessionOptions {
|
||||
/** Raw model pattern string (e.g. from --model CLI flag) to resolve after extensions load.
|
||||
* Used when model lookup is deferred because extension-provided models aren't registered yet. */
|
||||
modelPattern?: string;
|
||||
/** Thinking level. Default: from settings, else 'off' (clamped to model capabilities) */
|
||||
/** Thinking selector. Default: from settings, else unset */
|
||||
thinkingLevel?: ThinkingLevel;
|
||||
/** Models available for cycling (Ctrl+P in interactive mode) */
|
||||
scopedModels?: Array<{ model: Model; thinkingLevel: ThinkingLevel }>;
|
||||
scopedModels?: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
|
||||
|
||||
/** System prompt. String replaces default, function receives default and returns final. */
|
||||
systemPrompt?: string | ((defaultPrompt: string) => string);
|
||||
@@ -697,7 +705,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||
|
||||
// If session has data and includes a thinking entry, restore it
|
||||
if (thinkingLevel === undefined && hasExistingSession && hasThinkingEntry) {
|
||||
thinkingLevel = existingSession.thinkingLevel as ThinkingLevel;
|
||||
thinkingLevel = existingSession.thinkingLevel as ThinkingLevel | undefined;
|
||||
}
|
||||
|
||||
if (thinkingLevel === undefined && !hasExplicitModel && !hasThinkingEntry && defaultRoleSpec.explicitThinkingLevel) {
|
||||
@@ -706,14 +714,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||
|
||||
// Fall back to settings default
|
||||
if (thinkingLevel === undefined) {
|
||||
thinkingLevel = settings.get("defaultThinkingLevel") ?? "off";
|
||||
thinkingLevel = settings.get("defaultThinkingLevel");
|
||||
}
|
||||
|
||||
// Clamp to model capabilities
|
||||
if (!model || !model.reasoning) {
|
||||
thinkingLevel = "off";
|
||||
} else if (thinkingLevel === "xhigh" && !supportsXhigh(model)) {
|
||||
thinkingLevel = "high";
|
||||
if (model) {
|
||||
thinkingLevel = resolveThinkingLevelForModel(model, thinkingLevel);
|
||||
}
|
||||
|
||||
let skills: Skill[];
|
||||
@@ -1350,7 +1354,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||
initialState: {
|
||||
systemPrompt,
|
||||
model,
|
||||
thinkingLevel,
|
||||
thinkingLevel: toReasoningEffort(thinkingLevel),
|
||||
tools: initialTools,
|
||||
},
|
||||
convertToLlm: convertToLlmFinal,
|
||||
@@ -1421,6 +1425,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||
|
||||
session = new AgentSession({
|
||||
agent,
|
||||
thinkingLevel,
|
||||
sessionManager,
|
||||
settings,
|
||||
scopedModels: options.scopedModels,
|
||||
|
||||
@@ -24,16 +24,17 @@ import {
|
||||
type AgentState,
|
||||
type AgentTool,
|
||||
INTENT_FIELD,
|
||||
ThinkingLevel,
|
||||
} from "@oh-my-pi/pi-agent-core";
|
||||
import type {
|
||||
AssistantMessage,
|
||||
Effort,
|
||||
ImageContent,
|
||||
Message,
|
||||
Model,
|
||||
ProviderSessionState,
|
||||
ServiceTier,
|
||||
TextContent,
|
||||
ThinkingLevel,
|
||||
ToolCall,
|
||||
ToolChoice,
|
||||
Usage,
|
||||
@@ -41,11 +42,10 @@ import type {
|
||||
} from "@oh-my-pi/pi-ai";
|
||||
import {
|
||||
calculateRateLimitBackoffMs,
|
||||
getAvailableThinkingLevels,
|
||||
getSupportedEfforts,
|
||||
isContextOverflow,
|
||||
modelsAreEqual,
|
||||
parseRateLimitReason,
|
||||
supportsXhigh,
|
||||
} from "@oh-my-pi/pi-ai";
|
||||
import { abortableSleep, getAgentDbPath, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
||||
import type { AsyncJob, AsyncJobManager } from "../async";
|
||||
@@ -96,6 +96,7 @@ import planModeToolDecisionReminderPrompt from "../prompts/system/plan-mode-tool
|
||||
};
|
||||
import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { type: "text" };
|
||||
import type { SecretObfuscator } from "../secrets/obfuscator";
|
||||
import { resolveThinkingLevelForModel, toReasoningEffort } from "../thinking";
|
||||
import type { CheckpointState } from "../tools/checkpoint";
|
||||
import { outputMeta } from "../tools/output-meta";
|
||||
import { resolveToCwd } from "../tools/path-utils";
|
||||
@@ -166,7 +167,9 @@ export interface AgentSessionConfig {
|
||||
/** Async background jobs launched by tools */
|
||||
asyncJobManager?: AsyncJobManager;
|
||||
/** Models to cycle through with Ctrl+P (from --models flag) */
|
||||
scopedModels?: Array<{ model: Model; thinkingLevel: ThinkingLevel }>;
|
||||
scopedModels?: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
|
||||
/** Initial session thinking selector. */
|
||||
thinkingLevel?: ThinkingLevel;
|
||||
/** Prompt templates for expansion */
|
||||
promptTemplates?: PromptTemplate[];
|
||||
/** File-based slash commands for expansion */
|
||||
@@ -215,7 +218,7 @@ export interface PromptOptions {
|
||||
/** Result from cycleModel() */
|
||||
export interface ModelCycleResult {
|
||||
model: Model;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
thinkingLevel: ThinkingLevel | undefined;
|
||||
/** Whether cycling through scoped models (--models flag) or all available */
|
||||
isScoped: boolean;
|
||||
}
|
||||
@@ -223,7 +226,7 @@ export interface ModelCycleResult {
|
||||
/** Result from cycleRoleModels() */
|
||||
export interface RoleModelCycleResult {
|
||||
model: Model;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
thinkingLevel: ThinkingLevel | undefined;
|
||||
role: ModelRole;
|
||||
}
|
||||
|
||||
@@ -305,7 +308,8 @@ export class AgentSession {
|
||||
readonly settings: Settings;
|
||||
|
||||
#asyncJobManager: AsyncJobManager | undefined = undefined;
|
||||
#scopedModels: Array<{ model: Model; thinkingLevel: ThinkingLevel }>;
|
||||
#scopedModels: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
|
||||
#thinkingLevel: ThinkingLevel | undefined;
|
||||
#promptTemplates: PromptTemplate[];
|
||||
#slashCommands: FileSlashCommand[];
|
||||
|
||||
@@ -406,6 +410,7 @@ export class AgentSession {
|
||||
this.settings = config.settings;
|
||||
this.#asyncJobManager = config.asyncJobManager;
|
||||
this.#scopedModels = config.scopedModels ?? [];
|
||||
this.#thinkingLevel = config.thinkingLevel;
|
||||
this.#promptTemplates = config.promptTemplates ?? [];
|
||||
this.#slashCommands = config.slashCommands ?? [];
|
||||
this.#extensionRunner = config.extensionRunner;
|
||||
@@ -1544,8 +1549,8 @@ export class AgentSession {
|
||||
}
|
||||
|
||||
/** Current thinking level */
|
||||
get thinkingLevel(): ThinkingLevel {
|
||||
return this.agent.state.thinkingLevel;
|
||||
get thinkingLevel(): ThinkingLevel | undefined {
|
||||
return this.#thinkingLevel;
|
||||
}
|
||||
|
||||
get serviceTier(): ServiceTier | undefined {
|
||||
@@ -1724,7 +1729,7 @@ export class AgentSession {
|
||||
}
|
||||
|
||||
/** Scoped models for cycling (from --models flag) */
|
||||
get scopedModels(): ReadonlyArray<{ model: Model; thinkingLevel: ThinkingLevel }> {
|
||||
get scopedModels(): ReadonlyArray<{ model: Model; thinkingLevel?: ThinkingLevel }> {
|
||||
return this.#scopedModels;
|
||||
}
|
||||
|
||||
@@ -2654,7 +2659,7 @@ export class AgentSession {
|
||||
this.settings.setModelRole(role, this.#formatRoleModelValue(role, model));
|
||||
this.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
|
||||
|
||||
// Re-clamp thinking level for new model's capabilities without persisting settings
|
||||
// Re-apply the current thinking level for the newly selected model
|
||||
this.setThinkingLevel(this.thinkingLevel);
|
||||
}
|
||||
|
||||
@@ -2673,7 +2678,7 @@ export class AgentSession {
|
||||
this.sessionManager.appendModelChange(`${model.provider}/${model.id}`, "temporary");
|
||||
this.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
|
||||
|
||||
// Re-clamp thinking level for new model's capabilities without persisting settings
|
||||
// Re-apply the current thinking level for the newly selected model
|
||||
this.setThinkingLevel(this.thinkingLevel);
|
||||
}
|
||||
|
||||
@@ -2758,9 +2763,9 @@ export class AgentSession {
|
||||
return { model: next.model, thinkingLevel: this.thinkingLevel, role: next.role };
|
||||
}
|
||||
|
||||
async #getScopedModelsWithApiKey(): Promise<Array<{ model: Model; thinkingLevel: ThinkingLevel }>> {
|
||||
async #getScopedModelsWithApiKey(): Promise<Array<{ model: Model; thinkingLevel?: ThinkingLevel }>> {
|
||||
const apiKeysByProvider = new Map<string, string | undefined>();
|
||||
const result: Array<{ model: Model; thinkingLevel: ThinkingLevel }> = [];
|
||||
const result: Array<{ model: Model; thinkingLevel?: ThinkingLevel }> = [];
|
||||
|
||||
for (const scoped of this.#scopedModels) {
|
||||
const provider = scoped.model.provider;
|
||||
@@ -2798,7 +2803,7 @@ export class AgentSession {
|
||||
this.settings.setModelRole("default", this.#formatRoleModelValue("default", next.model));
|
||||
this.settings.getStorage()?.recordModelUsage(`${next.model.provider}/${next.model.id}`);
|
||||
|
||||
// Apply thinking level (setThinkingLevel clamps to model capabilities)
|
||||
// Apply the scoped model's configured thinking level
|
||||
this.setThinkingLevel(next.thinkingLevel);
|
||||
|
||||
return { model: next.model, thinkingLevel: this.thinkingLevel, isScoped: true };
|
||||
@@ -2826,7 +2831,7 @@ export class AgentSession {
|
||||
this.settings.setModelRole("default", this.#formatRoleModelValue("default", nextModel));
|
||||
this.settings.getStorage()?.recordModelUsage(`${nextModel.provider}/${nextModel.id}`);
|
||||
|
||||
// Re-clamp thinking level for new model's capabilities without persisting settings
|
||||
// Re-apply the current thinking level for the newly selected model
|
||||
this.setThinkingLevel(this.thinkingLevel);
|
||||
|
||||
return { model: nextModel, thinkingLevel: this.thinkingLevel, isScoped: false };
|
||||
@@ -2845,21 +2850,18 @@ export class AgentSession {
|
||||
|
||||
/**
|
||||
* Set thinking level.
|
||||
* Clamps to model capabilities based on available thinking levels.
|
||||
* Saves to session and settings only if the level actually changes.
|
||||
* Saves the effective metadata-clamped level to session and settings only if it changes.
|
||||
*/
|
||||
setThinkingLevel(level: ThinkingLevel, persist: boolean = false): void {
|
||||
const availableLevels = this.getAvailableThinkingLevels();
|
||||
const effectiveLevel = availableLevels.includes(level) ? level : this.#clampThinkingLevel(level, availableLevels);
|
||||
setThinkingLevel(level: ThinkingLevel | undefined, persist: boolean = false): void {
|
||||
const effectiveLevel = resolveThinkingLevelForModel(this.model, level);
|
||||
const isChanging = effectiveLevel !== this.#thinkingLevel;
|
||||
|
||||
// Only persist if actually changing
|
||||
const isChanging = effectiveLevel !== this.agent.state.thinkingLevel;
|
||||
|
||||
this.agent.setThinkingLevel(effectiveLevel);
|
||||
this.#thinkingLevel = effectiveLevel;
|
||||
this.agent.setThinkingLevel(toReasoningEffort(effectiveLevel));
|
||||
|
||||
if (isChanging) {
|
||||
this.sessionManager.appendThinkingLevelChange(effectiveLevel);
|
||||
if (persist) {
|
||||
if (persist && effectiveLevel !== undefined && effectiveLevel !== ThinkingLevel.Off) {
|
||||
this.settings.set("defaultThinkingLevel", effectiveLevel);
|
||||
}
|
||||
}
|
||||
@@ -2869,13 +2871,17 @@ export class AgentSession {
|
||||
* Cycle to next thinking level.
|
||||
* @returns New level, or undefined if model doesn't support thinking
|
||||
*/
|
||||
cycleThinkingLevel(): ThinkingLevel | undefined {
|
||||
if (!this.supportsThinking()) return undefined;
|
||||
cycleThinkingLevel(): Effort | undefined {
|
||||
if (!this.model?.reasoning) return undefined;
|
||||
|
||||
const levels = this.getAvailableThinkingLevels();
|
||||
const currentIndex = levels.indexOf(this.thinkingLevel);
|
||||
const currentIndex =
|
||||
this.thinkingLevel && this.thinkingLevel !== ThinkingLevel.Off && this.thinkingLevel !== ThinkingLevel.Inherit
|
||||
? levels.indexOf(this.thinkingLevel)
|
||||
: -1;
|
||||
const nextIndex = (currentIndex + 1) % levels.length;
|
||||
const nextLevel = levels[nextIndex];
|
||||
if (!nextLevel) return undefined;
|
||||
|
||||
this.setThinkingLevel(nextLevel);
|
||||
return nextLevel;
|
||||
@@ -2903,43 +2909,10 @@ export class AgentSession {
|
||||
|
||||
/**
|
||||
* Get available thinking levels for current model.
|
||||
* The provider will clamp to what the specific model supports internally.
|
||||
*/
|
||||
getAvailableThinkingLevels(): ReadonlyArray<ThinkingLevel> {
|
||||
if (!this.supportsThinking()) return ["off"];
|
||||
return getAvailableThinkingLevels(this.supportsXhighThinking());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current model supports xhigh thinking level.
|
||||
*/
|
||||
supportsXhighThinking(): boolean {
|
||||
return this.model ? supportsXhigh(this.model) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current model supports thinking/reasoning.
|
||||
*/
|
||||
supportsThinking(): boolean {
|
||||
return !!this.model?.reasoning;
|
||||
}
|
||||
|
||||
#clampThinkingLevel(level: ThinkingLevel, availableLevels: ReadonlyArray<ThinkingLevel>): ThinkingLevel {
|
||||
const ordered = getAvailableThinkingLevels(true);
|
||||
const available = new Set(availableLevels);
|
||||
const requestedIndex = ordered.indexOf(level);
|
||||
if (requestedIndex === -1) {
|
||||
return availableLevels[0] ?? "off";
|
||||
}
|
||||
for (let i = requestedIndex; i < ordered.length; i++) {
|
||||
const candidate = ordered[i];
|
||||
if (available.has(candidate)) return candidate;
|
||||
}
|
||||
for (let i = requestedIndex - 1; i >= 0; i--) {
|
||||
const candidate = ordered[i];
|
||||
if (available.has(candidate)) return candidate;
|
||||
}
|
||||
return availableLevels[0] ?? "off";
|
||||
getAvailableThinkingLevels(): ReadonlyArray<Effort> {
|
||||
if (!this.model) return [];
|
||||
return getSupportedEfforts(this.model);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@@ -4647,18 +4620,15 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
||||
|
||||
const hasThinkingEntry = this.sessionManager.getBranch().some(entry => entry.type === "thinking_level_change");
|
||||
const hasServiceTierEntry = this.sessionManager.getBranch().some(entry => entry.type === "service_tier_change");
|
||||
const defaultThinkingLevel = (this.settings.get("defaultThinkingLevel") ?? "off") as ThinkingLevel;
|
||||
const defaultThinkingLevel = this.settings.get("defaultThinkingLevel");
|
||||
|
||||
if (hasThinkingEntry) {
|
||||
// Restore thinking level if saved (setThinkingLevel clamps to model capabilities)
|
||||
this.setThinkingLevel(sessionContext.thinkingLevel as ThinkingLevel);
|
||||
this.setThinkingLevel(sessionContext.thinkingLevel as ThinkingLevel | undefined);
|
||||
} else {
|
||||
const availableLevels = this.getAvailableThinkingLevels();
|
||||
const effectiveLevel = availableLevels.includes(defaultThinkingLevel)
|
||||
? defaultThinkingLevel
|
||||
: this.#clampThinkingLevel(defaultThinkingLevel, availableLevels);
|
||||
this.agent.setThinkingLevel(effectiveLevel);
|
||||
this.sessionManager.appendThinkingLevelChange(effectiveLevel);
|
||||
const effectiveDefaultThinkingLevel = resolveThinkingLevelForModel(this.model, defaultThinkingLevel);
|
||||
this.#thinkingLevel = effectiveDefaultThinkingLevel;
|
||||
this.agent.setThinkingLevel(toReasoningEffort(effectiveDefaultThinkingLevel));
|
||||
this.sessionManager.appendThinkingLevelChange(effectiveDefaultThinkingLevel);
|
||||
}
|
||||
|
||||
if (hasServiceTierEntry) {
|
||||
@@ -5181,7 +5151,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
||||
|
||||
// Include model and thinking level
|
||||
const model = this.agent.state.model;
|
||||
const thinkingLevel = this.agent.state.thinkingLevel;
|
||||
const thinkingLevel = this.#thinkingLevel;
|
||||
lines.push("## Configuration\n");
|
||||
lines.push(`Model: ${model.provider}/${model.id}`);
|
||||
lines.push(`Thinking Level: ${thinkingLevel}`);
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
* and after compaction the session is reloaded.
|
||||
*/
|
||||
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
||||
import type { AssistantMessage, Model, Usage } from "@oh-my-pi/pi-ai";
|
||||
import { completeSimple } from "@oh-my-pi/pi-ai";
|
||||
import { type AssistantMessage, completeSimple, Effort, type Model, type Usage } from "@oh-my-pi/pi-ai";
|
||||
import {
|
||||
CODEX_BASE_URL,
|
||||
getCodexAccountId,
|
||||
@@ -985,7 +984,7 @@ export async function generateSummary(
|
||||
const response = await completeSimple(
|
||||
model,
|
||||
{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },
|
||||
{ maxTokens, signal, apiKey, reasoning: "high" },
|
||||
{ maxTokens, signal, apiKey, reasoning: Effort.High },
|
||||
);
|
||||
|
||||
if (response.stopReason === "error") {
|
||||
@@ -1034,7 +1033,7 @@ async function generateShortSummary(
|
||||
systemPrompt: SUMMARIZATION_SYSTEM_PROMPT,
|
||||
messages: [{ role: "user", content: [{ type: "text", text: promptText }], timestamp: Date.now() }],
|
||||
},
|
||||
{ maxTokens, signal, apiKey, reasoning: "high" },
|
||||
{ maxTokens, signal, apiKey, reasoning: Effort.High },
|
||||
);
|
||||
|
||||
if (response.stopReason === "error") {
|
||||
@@ -1335,7 +1334,7 @@ async function generateTurnPrefixSummary(
|
||||
const response = await completeSimple(
|
||||
model,
|
||||
{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },
|
||||
{ maxTokens, signal, apiKey, reasoning: "high" },
|
||||
{ maxTokens, signal, apiKey, reasoning: Effort.High },
|
||||
);
|
||||
|
||||
if (response.stopReason === "error") {
|
||||
|
||||
@@ -67,7 +67,7 @@ export interface SessionMessageEntry extends SessionEntryBase {
|
||||
|
||||
export interface ThinkingLevelChangeEntry extends SessionEntryBase {
|
||||
type: "thinking_level_change";
|
||||
thinkingLevel: string;
|
||||
thinkingLevel?: string | null;
|
||||
}
|
||||
|
||||
export interface ModelChangeEntry extends SessionEntryBase {
|
||||
@@ -209,7 +209,7 @@ export interface SessionTreeNode {
|
||||
|
||||
export interface SessionContext {
|
||||
messages: AgentMessage[];
|
||||
thinkingLevel: string;
|
||||
thinkingLevel?: string;
|
||||
serviceTier?: ServiceTier;
|
||||
/** Model roles: { default: "provider/modelId", small: "provider/modelId", ... } */
|
||||
models: Record<string, string>;
|
||||
@@ -433,7 +433,7 @@ export function buildSessionContext(
|
||||
// Explicitly null - return no messages (navigated to before first entry)
|
||||
return {
|
||||
messages: [],
|
||||
thinkingLevel: "off",
|
||||
thinkingLevel: undefined,
|
||||
serviceTier: undefined,
|
||||
models: {},
|
||||
injectedTtsrRules: [],
|
||||
@@ -451,7 +451,7 @@ export function buildSessionContext(
|
||||
if (!leaf) {
|
||||
return {
|
||||
messages: [],
|
||||
thinkingLevel: "off",
|
||||
thinkingLevel: undefined,
|
||||
serviceTier: undefined,
|
||||
models: {},
|
||||
injectedTtsrRules: [],
|
||||
@@ -468,7 +468,7 @@ export function buildSessionContext(
|
||||
}
|
||||
|
||||
// Extract settings and find compaction
|
||||
let thinkingLevel = "off";
|
||||
let thinkingLevel: string | undefined;
|
||||
let serviceTier: ServiceTier | undefined;
|
||||
const models: Record<string, string> = {};
|
||||
let compaction: CompactionEntry | null = null;
|
||||
@@ -478,7 +478,7 @@ export function buildSessionContext(
|
||||
|
||||
for (const entry of path) {
|
||||
if (entry.type === "thinking_level_change") {
|
||||
thinkingLevel = entry.thinkingLevel;
|
||||
thinkingLevel = entry.thinkingLevel ?? undefined;
|
||||
} else if (entry.type === "model_change") {
|
||||
// New format: { model: "provider/id", role?: string }
|
||||
if (entry.model) {
|
||||
@@ -1829,13 +1829,13 @@ export class SessionManager {
|
||||
}
|
||||
|
||||
/** Append a thinking level change as child of current leaf, then advance leaf. Returns entry id. */
|
||||
appendThinkingLevelChange(thinkingLevel: string): string {
|
||||
appendThinkingLevelChange(thinkingLevel?: string): string {
|
||||
const entry: ThinkingLevelChangeEntry = {
|
||||
type: "thinking_level_change",
|
||||
id: generateId(this.#byId),
|
||||
parentId: this.#leafId,
|
||||
timestamp: new Date().toISOString(),
|
||||
thinkingLevel,
|
||||
thinkingLevel: thinkingLevel ?? null,
|
||||
};
|
||||
this.#appendEntry(entry);
|
||||
return entry.id;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*
|
||||
* Agents are embedded at build time via Bun's import with { type: "text" }.
|
||||
*/
|
||||
import { Effort } from "@oh-my-pi/pi-ai";
|
||||
import { renderPromptTemplate } from "../config/prompt-templates";
|
||||
import { parseAgentFields } from "../discovery/helpers";
|
||||
import designerMd from "../prompts/agents/designer.md" with { type: "text" };
|
||||
@@ -53,7 +54,7 @@ const EMBEDDED_AGENT_DEFS: EmbeddedAgentDef[] = [
|
||||
description: "General-purpose subagent with full capabilities for delegated multi-step tasks",
|
||||
spawns: "*",
|
||||
model: "default",
|
||||
thinkingLevel: "medium",
|
||||
thinkingLevel: Effort.Medium,
|
||||
},
|
||||
template: taskMd,
|
||||
},
|
||||
@@ -63,7 +64,7 @@ const EMBEDDED_AGENT_DEFS: EmbeddedAgentDef[] = [
|
||||
name: "quick_task",
|
||||
description: "Low-reasoning agent for strictly mechanical updates or data collection only",
|
||||
model: "pi/smol",
|
||||
thinkingLevel: "minimal",
|
||||
thinkingLevel: Effort.Minimal,
|
||||
},
|
||||
template: taskMd,
|
||||
},
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
* Runs each subagent on the main thread and forwards AgentEvents for progress tracking.
|
||||
*/
|
||||
import path from "node:path";
|
||||
import type { AgentEvent } from "@oh-my-pi/pi-agent-core";
|
||||
import type { Api, Model, ThinkingLevel, ToolChoice } from "@oh-my-pi/pi-ai";
|
||||
import type { AgentEvent, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import type { Api, Model, ToolChoice } from "@oh-my-pi/pi-ai";
|
||||
import { logger, untilAborted } from "@oh-my-pi/pi-utils";
|
||||
import type { TSchema } from "@sinclair/typebox";
|
||||
import Ajv, { type ValidateFunction } from "ajv";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ThinkingLevel, Usage } from "@oh-my-pi/pi-ai";
|
||||
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import type { Usage } from "@oh-my-pi/pi-ai";
|
||||
import { $env } from "@oh-my-pi/pi-utils";
|
||||
import { type Static, Type } from "@sinclair/typebox";
|
||||
import type { NestedRepoPatch } from "./worktree";
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { type ResolvedThinkingLevel, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import { clampThinkingLevelForModel, type Effort, type Model, THINKING_EFFORTS } from "@oh-my-pi/pi-ai";
|
||||
|
||||
/**
|
||||
* Metadata used to render thinking selector values in the coding-agent UI.
|
||||
*/
|
||||
export interface ThinkingLevelMetadata {
|
||||
value: ThinkingLevel;
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const THINKING_LEVEL_METADATA: Record<ThinkingLevel, ThinkingLevelMetadata> = {
|
||||
[ThinkingLevel.Inherit]: {
|
||||
value: ThinkingLevel.Inherit,
|
||||
label: "inherit",
|
||||
description: "Inherit session default",
|
||||
},
|
||||
[ThinkingLevel.Off]: { value: ThinkingLevel.Off, label: "off", description: "No reasoning" },
|
||||
[ThinkingLevel.Minimal]: {
|
||||
value: ThinkingLevel.Minimal,
|
||||
label: "min",
|
||||
description: "Very brief reasoning (~1k tokens)",
|
||||
},
|
||||
[ThinkingLevel.Low]: { value: ThinkingLevel.Low, label: "low", description: "Light reasoning (~2k tokens)" },
|
||||
[ThinkingLevel.Medium]: {
|
||||
value: ThinkingLevel.Medium,
|
||||
label: "medium",
|
||||
description: "Moderate reasoning (~8k tokens)",
|
||||
},
|
||||
[ThinkingLevel.High]: { value: ThinkingLevel.High, label: "high", description: "Deep reasoning (~16k tokens)" },
|
||||
[ThinkingLevel.XHigh]: {
|
||||
value: ThinkingLevel.XHigh,
|
||||
label: "xhigh",
|
||||
description: "Maximum reasoning (~32k tokens)",
|
||||
},
|
||||
};
|
||||
|
||||
const THINKING_LEVELS = new Set<string>([ThinkingLevel.Inherit, ThinkingLevel.Off, ...THINKING_EFFORTS]);
|
||||
const EFFORT_LEVELS = new Set<string>(THINKING_EFFORTS);
|
||||
|
||||
/**
|
||||
* Parses a provider-facing effort value.
|
||||
*/
|
||||
export function parseEffort(value: string | null | undefined): Effort | undefined {
|
||||
return value !== undefined && value !== null && EFFORT_LEVELS.has(value) ? (value as Effort) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an agent-local thinking selector.
|
||||
*/
|
||||
export function parseThinkingLevel(value: string | null | undefined): ThinkingLevel | undefined {
|
||||
return value !== undefined && value !== null && THINKING_LEVELS.has(value) ? (value as ThinkingLevel) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns display metadata for a thinking selector.
|
||||
*/
|
||||
export function getThinkingLevelMetadata(level: ThinkingLevel): ThinkingLevelMetadata {
|
||||
return THINKING_LEVEL_METADATA[level];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an agent-local selector into the effort sent to providers.
|
||||
*/
|
||||
export function toReasoningEffort(level: ThinkingLevel | undefined): Effort | undefined {
|
||||
if (level === undefined || level === ThinkingLevel.Off || level === ThinkingLevel.Inherit) {
|
||||
return undefined;
|
||||
}
|
||||
return level;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a selector against the current model while preserving explicit "off".
|
||||
*/
|
||||
export function resolveThinkingLevelForModel(
|
||||
model: Model | undefined,
|
||||
level: ThinkingLevel | undefined,
|
||||
): ResolvedThinkingLevel | undefined {
|
||||
if (level === undefined || level === ThinkingLevel.Inherit) {
|
||||
return undefined;
|
||||
}
|
||||
if (level === ThinkingLevel.Off) {
|
||||
return ThinkingLevel.Off;
|
||||
}
|
||||
return clampThinkingLevelForModel(model, level);
|
||||
}
|
||||
@@ -1,13 +1,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
||||
import * as path from "node:path";
|
||||
import { Agent } from "@oh-my-pi/pi-agent-core";
|
||||
import {
|
||||
getBundledModel,
|
||||
getBundledModels,
|
||||
getBundledProviders,
|
||||
supportsXhigh,
|
||||
type ThinkingLevel,
|
||||
} from "@oh-my-pi/pi-ai";
|
||||
import { Effort, getBundledModel } from "@oh-my-pi/pi-ai";
|
||||
import { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry";
|
||||
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
||||
import { AgentSession } from "@oh-my-pi/pi-coding-agent/session/agent-session";
|
||||
@@ -37,18 +31,9 @@ describe("AgentSession role model thinking behavior", () => {
|
||||
return model;
|
||||
}
|
||||
|
||||
function getReasoningModelWithoutXhighOrThrow() {
|
||||
for (const provider of getBundledProviders()) {
|
||||
for (const model of getBundledModels(provider as Parameters<typeof getBundledModels>[0])) {
|
||||
if (model.reasoning && !supportsXhigh(model)) return model;
|
||||
}
|
||||
}
|
||||
throw new Error("Expected at least one bundled reasoning model without xhigh support");
|
||||
}
|
||||
|
||||
async function createSession(options: {
|
||||
initialModelId: string;
|
||||
initialThinkingLevel: ThinkingLevel;
|
||||
initialThinkingLevel: Effort;
|
||||
modelRoles: Record<string, string>;
|
||||
}) {
|
||||
const model = getAnthropicModelOrThrow(options.initialModelId);
|
||||
@@ -83,7 +68,7 @@ describe("AgentSession role model thinking behavior", () => {
|
||||
|
||||
await createSession({
|
||||
initialModelId: defaultModel.id,
|
||||
initialThinkingLevel: "high",
|
||||
initialThinkingLevel: Effort.High,
|
||||
modelRoles: {
|
||||
default: `${defaultModel.provider}/${defaultModel.id}`,
|
||||
slow: `${slowModel.provider}/${slowModel.id}:off`,
|
||||
@@ -96,13 +81,13 @@ describe("AgentSession role model thinking behavior", () => {
|
||||
expect(firstSwitch?.thinkingLevel).toBe("off");
|
||||
expect(session.thinkingLevel).toBe("off");
|
||||
|
||||
session.setThinkingLevel("high");
|
||||
expect(session.thinkingLevel).toBe("high");
|
||||
session.setThinkingLevel(Effort.High);
|
||||
expect(session.thinkingLevel).toBe(Effort.High);
|
||||
|
||||
const secondSwitch = await session.cycleRoleModels(["default", "slow"]);
|
||||
expect(secondSwitch?.role).toBe("default");
|
||||
expect(secondSwitch?.model.id).toBe(defaultModel.id);
|
||||
expect(session.thinkingLevel).toBe("high");
|
||||
expect(session.thinkingLevel).toBe(Effort.High);
|
||||
|
||||
const thirdSwitch = await session.cycleRoleModels(["default", "slow"]);
|
||||
expect(thirdSwitch?.role).toBe("slow");
|
||||
@@ -117,7 +102,7 @@ describe("AgentSession role model thinking behavior", () => {
|
||||
|
||||
await createSession({
|
||||
initialModelId: defaultModel.id,
|
||||
initialThinkingLevel: "low",
|
||||
initialThinkingLevel: Effort.Low,
|
||||
modelRoles: {
|
||||
default: `${defaultModel.provider}/${defaultModel.id}`,
|
||||
slow: `${slowModel.provider}/${slowModel.id}:high`,
|
||||
@@ -126,17 +111,17 @@ describe("AgentSession role model thinking behavior", () => {
|
||||
|
||||
const toSlow = await session.cycleRoleModels(["default", "slow"]);
|
||||
expect(toSlow?.role).toBe("slow");
|
||||
expect(toSlow?.thinkingLevel).toBe("high");
|
||||
expect(session.thinkingLevel).toBe("high");
|
||||
expect(toSlow?.thinkingLevel).toBe(Effort.High);
|
||||
expect(session.thinkingLevel).toBe(Effort.High);
|
||||
|
||||
session.setThinkingLevel("minimal");
|
||||
expect(session.thinkingLevel).toBe("minimal");
|
||||
session.setThinkingLevel(Effort.Minimal);
|
||||
expect(session.thinkingLevel).toBe(Effort.Minimal);
|
||||
|
||||
const toDefault = await session.cycleRoleModels(["default", "slow"]);
|
||||
expect(toDefault?.role).toBe("default");
|
||||
expect(toDefault?.model.id).toBe(defaultModel.id);
|
||||
expect(toDefault?.thinkingLevel).toBe("minimal");
|
||||
expect(session.thinkingLevel).toBe("minimal");
|
||||
expect(toDefault?.thinkingLevel).toBe(Effort.Minimal);
|
||||
expect(session.thinkingLevel).toBe(Effort.Minimal);
|
||||
});
|
||||
|
||||
it("applies slow role thinking even when plan shares the same model", async () => {
|
||||
@@ -146,7 +131,7 @@ describe("AgentSession role model thinking behavior", () => {
|
||||
|
||||
await createSession({
|
||||
initialModelId: defaultModel.id,
|
||||
initialThinkingLevel: "medium",
|
||||
initialThinkingLevel: Effort.Medium,
|
||||
modelRoles: {
|
||||
default: `${defaultModel.provider}/${defaultModel.id}`,
|
||||
smol: `${smolModel.provider}/${smolModel.id}:low`,
|
||||
@@ -157,14 +142,14 @@ describe("AgentSession role model thinking behavior", () => {
|
||||
|
||||
const toSmol = await session.cycleRoleModels(["slow", "default", "smol"]);
|
||||
expect(toSmol?.role).toBe("smol");
|
||||
expect(toSmol?.thinkingLevel).toBe("low");
|
||||
expect(session.thinkingLevel).toBe("low");
|
||||
expect(toSmol?.thinkingLevel).toBe(Effort.Low);
|
||||
expect(session.thinkingLevel).toBe(Effort.Low);
|
||||
|
||||
const toSlow = await session.cycleRoleModels(["slow", "default", "smol"]);
|
||||
expect(toSlow?.role).toBe("slow");
|
||||
expect(toSlow?.model.id).toBe(slowPlanModel.id);
|
||||
expect(toSlow?.thinkingLevel).toBe("high");
|
||||
expect(session.thinkingLevel).toBe("high");
|
||||
expect(toSlow?.thinkingLevel).toBe(Effort.High);
|
||||
expect(session.thinkingLevel).toBe(Effort.High);
|
||||
});
|
||||
|
||||
it("preserves explicit role thinking when updating default model despite unresolved previous model", async () => {
|
||||
@@ -173,7 +158,7 @@ describe("AgentSession role model thinking behavior", () => {
|
||||
|
||||
await createSession({
|
||||
initialModelId: defaultModel.id,
|
||||
initialThinkingLevel: "high",
|
||||
initialThinkingLevel: Effort.High,
|
||||
modelRoles: {
|
||||
default: "anthropic/nonexistent-model:off",
|
||||
},
|
||||
@@ -184,15 +169,15 @@ describe("AgentSession role model thinking behavior", () => {
|
||||
expect(sessionSettings.getModelRole("default")).toBe(`${slowModel.provider}/${slowModel.id}:off`);
|
||||
});
|
||||
|
||||
it("clamps unsupported xhigh to highest supported level instead of off", async () => {
|
||||
const model = getReasoningModelWithoutXhighOrThrow();
|
||||
it("clamps unsupported selections from model metadata", async () => {
|
||||
const model = getAnthropicModelOrThrow("claude-sonnet-4-6");
|
||||
const agent = new Agent({
|
||||
initialState: {
|
||||
model,
|
||||
systemPrompt: "Test",
|
||||
tools: [],
|
||||
messages: [],
|
||||
thinkingLevel: "off",
|
||||
thinkingLevel: undefined,
|
||||
},
|
||||
});
|
||||
const authStorage = await AuthStorage.create(path.join(tempDir.path(), "testauth-non-xhigh.db"));
|
||||
@@ -207,7 +192,8 @@ describe("AgentSession role model thinking behavior", () => {
|
||||
modelRegistry,
|
||||
});
|
||||
|
||||
session.setThinkingLevel("xhigh");
|
||||
expect(session.thinkingLevel).toBe("high");
|
||||
session.setThinkingLevel(Effort.XHigh);
|
||||
expect(session.thinkingLevel).toBe(Effort.High);
|
||||
expect(session.getAvailableThinkingLevels()).not.toContain("xhigh");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Effort } from "@oh-my-pi/pi-ai";
|
||||
import { parseArgs } from "@oh-my-pi/pi-coding-agent/cli/args";
|
||||
|
||||
describe("parseArgs", () => {
|
||||
@@ -133,7 +134,7 @@ describe("parseArgs", () => {
|
||||
|
||||
test("parses --thinking", () => {
|
||||
const result = parseArgs(["--thinking", "high"]);
|
||||
expect(result.thinking).toBe("high");
|
||||
expect(result.thinking).toBe(Effort.High);
|
||||
});
|
||||
|
||||
test("parses --models as comma-separated list", () => {
|
||||
@@ -247,7 +248,7 @@ describe("parseArgs", () => {
|
||||
expect(result.provider).toBe("anthropic");
|
||||
expect(result.model).toBe("claude-sonnet");
|
||||
expect(result.print).toBe(true);
|
||||
expect(result.thinking).toBe("high");
|
||||
expect(result.thinking).toBe(Effort.High);
|
||||
expect(result.fileArgs).toEqual(["prompt.md"]);
|
||||
expect(result.messages).toEqual(["Do the task"]);
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { Agent } from "@oh-my-pi/pi-agent-core";
|
||||
import { getBundledModel, type Model, type ThinkingLevel } from "@oh-my-pi/pi-ai";
|
||||
import { Effort, getBundledModel, type Model, type Effort as ThinkingLevelType } from "@oh-my-pi/pi-ai";
|
||||
import { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry";
|
||||
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
||||
import { AgentSession } from "@oh-my-pi/pi-coding-agent/session/agent-session";
|
||||
@@ -47,7 +47,7 @@ describe.skipIf(!HAS_ANTIGRAVITY_AUTH)("Compaction with thinking models (Antigra
|
||||
|
||||
async function createSession(
|
||||
modelId: "claude-opus-4-5-thinking" | "claude-sonnet-4-5",
|
||||
thinkingLevel: ThinkingLevel = "high",
|
||||
thinkingLevel: ThinkingLevelType = Effort.High,
|
||||
) {
|
||||
const toolSession: ToolSession = {
|
||||
cwd: tempDir,
|
||||
@@ -92,7 +92,7 @@ describe.skipIf(!HAS_ANTIGRAVITY_AUTH)("Compaction with thinking models (Antigra
|
||||
}
|
||||
|
||||
it("should compact successfully with claude-opus-4-5-thinking and thinking level high", async () => {
|
||||
createSession("claude-opus-4-5-thinking", "high");
|
||||
createSession("claude-opus-4-5-thinking", Effort.High);
|
||||
|
||||
// Send a simple prompt
|
||||
await session.prompt("Write down the first 10 prime numbers.");
|
||||
@@ -119,7 +119,7 @@ describe.skipIf(!HAS_ANTIGRAVITY_AUTH)("Compaction with thinking models (Antigra
|
||||
}, 180000);
|
||||
|
||||
it("should compact successfully with claude-sonnet-4-5 (non-thinking) for comparison", async () => {
|
||||
createSession("claude-sonnet-4-5", "off");
|
||||
createSession("claude-sonnet-4-5");
|
||||
|
||||
await session.prompt("Write down the first 10 prime numbers.");
|
||||
await session.agent.waitForIdle();
|
||||
@@ -156,7 +156,7 @@ describe.skipIf(!HAS_ANTHROPIC_AUTH)("Compaction with thinking models (Anthropic
|
||||
}
|
||||
});
|
||||
|
||||
async function createSession(model: Model, thinkingLevel: ThinkingLevel = "high") {
|
||||
async function createSession(model: Model, thinkingLevel: ThinkingLevelType = Effort.High) {
|
||||
const toolSession: ToolSession = {
|
||||
cwd: tempDir,
|
||||
hasUI: false,
|
||||
@@ -196,7 +196,7 @@ describe.skipIf(!HAS_ANTHROPIC_AUTH)("Compaction with thinking models (Anthropic
|
||||
|
||||
it("should compact successfully with claude-3-7-sonnet and thinking level high", async () => {
|
||||
const model = getBundledModel("anthropic", "claude-3-7-sonnet-latest")!;
|
||||
createSession(model, "high");
|
||||
createSession(model, Effort.High);
|
||||
|
||||
// Send a simple prompt
|
||||
await session.prompt("Write down the first 10 prime numbers.");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Effort } from "@oh-my-pi/pi-ai";
|
||||
import { parseAgentFields } from "../../src/discovery/helpers";
|
||||
|
||||
describe("parseAgentFields", () => {
|
||||
@@ -42,7 +43,7 @@ describe("parseAgentFields", () => {
|
||||
});
|
||||
|
||||
expect(fields).toBeDefined();
|
||||
expect(fields?.thinkingLevel).toBe("medium");
|
||||
expect(fields?.thinkingLevel).toBe(Effort.Medium);
|
||||
});
|
||||
|
||||
test("prefers thinking-level over legacy thinking", () => {
|
||||
@@ -50,9 +51,9 @@ describe("parseAgentFields", () => {
|
||||
name: "reviewer",
|
||||
description: "desc",
|
||||
thinking: "minimal",
|
||||
thinkingLevel: "high",
|
||||
thinkingLevel: Effort.High,
|
||||
});
|
||||
|
||||
expect(fields?.thinkingLevel).toBe("high");
|
||||
expect(fields?.thinkingLevel).toBe(Effort.High);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import * as path from "node:path";
|
||||
import {
|
||||
type AssistantMessageEventStream,
|
||||
clearCustomApis,
|
||||
Effort,
|
||||
getCustomApi,
|
||||
getOAuthProviders,
|
||||
type OAuthCredentials,
|
||||
@@ -102,6 +103,36 @@ describe("ModelRegistry runtime provider registration", () => {
|
||||
expect(model?.headers?.["X-Model"]).toBe("model-header");
|
||||
});
|
||||
|
||||
test("registerProvider preserves explicit thinking on runtime models", () => {
|
||||
const registry = new ModelRegistry(authStorage, modelsJsonPath);
|
||||
const config: ProviderConfigInput = {
|
||||
baseUrl: "https://runtime.example.com/v1",
|
||||
apiKey: "RUNTIME_KEY",
|
||||
api: "anthropic-messages",
|
||||
models: [
|
||||
{
|
||||
...baseModel,
|
||||
id: "runtime-thinking-model",
|
||||
reasoning: true,
|
||||
thinking: {
|
||||
mode: "anthropic-adaptive",
|
||||
minLevel: Effort.Minimal,
|
||||
maxLevel: Effort.High,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
registry.registerProvider("runtime-provider", config, "ext://runtime");
|
||||
const model = registry.find("runtime-provider", "runtime-thinking-model");
|
||||
|
||||
expect(model?.thinking).toEqual({
|
||||
mode: "anthropic-adaptive",
|
||||
minLevel: Effort.Minimal,
|
||||
maxLevel: Effort.High,
|
||||
});
|
||||
});
|
||||
|
||||
test("clearSourceRegistrations and syncExtensionSources remove source-scoped API and OAuth providers", () => {
|
||||
const registry = new ModelRegistry(authStorage, modelsJsonPath);
|
||||
const oauthCredentials: OAuthCredentials = {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import type { OpenAICompat } from "@oh-my-pi/pi-ai";
|
||||
import { Effort, type OpenAICompat, type ThinkingConfig } from "@oh-my-pi/pi-ai";
|
||||
import { kNoAuth, MODEL_ROLES, ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry";
|
||||
import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage";
|
||||
import { Snowflake } from "@oh-my-pi/pi-utils";
|
||||
@@ -38,6 +38,7 @@ describe("ModelRegistry", () => {
|
||||
id: string;
|
||||
name: string;
|
||||
reasoning: boolean;
|
||||
thinking?: ThinkingConfig;
|
||||
input: string[];
|
||||
cost: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
||||
contextWindow: number;
|
||||
@@ -48,7 +49,7 @@ describe("ModelRegistry", () => {
|
||||
/** Create minimal provider config */
|
||||
function providerConfig(
|
||||
baseUrl: string,
|
||||
models: Array<{ id: string; name?: string }>,
|
||||
models: Array<{ id: string; name?: string; reasoning?: boolean; thinking?: ThinkingConfig }>,
|
||||
api: string = "anthropic-messages",
|
||||
) {
|
||||
return {
|
||||
@@ -58,7 +59,8 @@ describe("ModelRegistry", () => {
|
||||
models: models.map(m => ({
|
||||
id: m.id,
|
||||
name: m.name ?? m.id,
|
||||
reasoning: false,
|
||||
reasoning: m.reasoning ?? false,
|
||||
thinking: m.thinking,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 100000,
|
||||
@@ -350,6 +352,48 @@ describe("ModelRegistry", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("thinking metadata normalization", () => {
|
||||
test("custom models preserve explicit thinking", () => {
|
||||
const thinking: ThinkingConfig = {
|
||||
mode: "anthropic-adaptive",
|
||||
minLevel: Effort.Minimal,
|
||||
maxLevel: Effort.High,
|
||||
};
|
||||
|
||||
writeModelsJson({
|
||||
anthropic: providerConfig("https://my-proxy.example.com/v1", [
|
||||
{ id: "claude-custom", reasoning: true, thinking },
|
||||
]),
|
||||
});
|
||||
|
||||
const registry = new ModelRegistry(authStorage, modelsJsonPath);
|
||||
const model = getModelsForProvider(registry, "anthropic").find(m => m.id === "claude-custom");
|
||||
|
||||
expect(model?.thinking).toEqual(thinking);
|
||||
});
|
||||
|
||||
test("model overrides can replace canonical thinking metadata", () => {
|
||||
writeRawModelsJson({
|
||||
openrouter: {
|
||||
modelOverrides: {
|
||||
"anthropic/claude-sonnet-4": {
|
||||
thinking: { mode: "budget", minLevel: Effort.Low, maxLevel: Effort.Medium },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const registry = new ModelRegistry(authStorage, modelsJsonPath);
|
||||
const model = getModelsForProvider(registry, "openrouter").find(m => m.id === "anthropic/claude-sonnet-4");
|
||||
|
||||
expect(model?.thinking).toEqual({
|
||||
mode: "budget",
|
||||
minLevel: Effort.Low,
|
||||
maxLevel: Effort.Medium,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("modelOverrides (per-model customization)", () => {
|
||||
test("model override applies to a single built-in model", () => {
|
||||
writeRawModelsJson({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import type { Model } from "@oh-my-pi/pi-ai";
|
||||
import { Effort, type Model } from "@oh-my-pi/pi-ai";
|
||||
import {
|
||||
parseModelPattern,
|
||||
parseModelString,
|
||||
@@ -18,6 +18,11 @@ const mockModels: Model<"anthropic-messages">[] = [
|
||||
provider: "anthropic",
|
||||
baseUrl: "https://api.anthropic.com",
|
||||
reasoning: true,
|
||||
thinking: {
|
||||
mode: "budget",
|
||||
minLevel: Effort.Minimal,
|
||||
maxLevel: Effort.High,
|
||||
},
|
||||
input: ["text", "image"],
|
||||
cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
||||
contextWindow: 200000,
|
||||
@@ -46,6 +51,11 @@ const mockOpenRouterModels: Model<"anthropic-messages">[] = [
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
reasoning: true,
|
||||
thinking: {
|
||||
mode: "budget",
|
||||
minLevel: Effort.Minimal,
|
||||
maxLevel: Effort.High,
|
||||
},
|
||||
input: ["text"],
|
||||
cost: { input: 1, output: 2, cacheRead: 0.1, cacheWrite: 1 },
|
||||
contextWindow: 128000,
|
||||
@@ -100,6 +110,11 @@ const mockCodexOverlapModels: Model<"anthropic-messages">[] = [
|
||||
provider: "openai-codex",
|
||||
baseUrl: "https://api.openai.com",
|
||||
reasoning: true,
|
||||
thinking: {
|
||||
mode: "effort",
|
||||
minLevel: Effort.Low,
|
||||
maxLevel: Effort.XHigh,
|
||||
},
|
||||
input: ["text"],
|
||||
cost: { input: 1.5, output: 6, cacheRead: 0.15, cacheWrite: 1.5 },
|
||||
contextWindow: 200000,
|
||||
@@ -112,6 +127,11 @@ const mockCodexOverlapModels: Model<"anthropic-messages">[] = [
|
||||
provider: "openai-codex",
|
||||
baseUrl: "https://api.openai.com",
|
||||
reasoning: true,
|
||||
thinking: {
|
||||
mode: "effort",
|
||||
minLevel: Effort.Low,
|
||||
maxLevel: Effort.XHigh,
|
||||
},
|
||||
input: ["text"],
|
||||
cost: { input: 1, output: 4, cacheRead: 0.1, cacheWrite: 1 },
|
||||
contextWindow: 200000,
|
||||
@@ -152,19 +172,19 @@ describe("parseModelPattern", () => {
|
||||
test("sonnet:high returns sonnet with high thinking level", () => {
|
||||
const result = parseModelPattern("sonnet:high", allModels);
|
||||
expect(result.model?.id).toBe("claude-sonnet-4-5");
|
||||
expect(result.thinkingLevel).toBe("high");
|
||||
expect(result.thinkingLevel).toBe(Effort.High);
|
||||
expect(result.warning).toBeUndefined();
|
||||
});
|
||||
|
||||
test("gpt-4o:medium returns gpt-4o with medium thinking level", () => {
|
||||
const result = parseModelPattern("gpt-4o:medium", allModels);
|
||||
expect(result.model?.id).toBe("gpt-4o");
|
||||
expect(result.thinkingLevel).toBe("medium");
|
||||
expect(result.thinkingLevel).toBe(Effort.Medium);
|
||||
expect(result.warning).toBeUndefined();
|
||||
});
|
||||
|
||||
test("all valid thinking levels work", () => {
|
||||
const levels = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
||||
const levels = ["off", Effort.Minimal, Effort.Low, Effort.Medium, Effort.High, Effort.XHigh] as const;
|
||||
for (const level of levels) {
|
||||
const result = parseModelPattern(`sonnet:${level}`, allModels);
|
||||
expect(result.model?.id).toBe("claude-sonnet-4-5");
|
||||
@@ -214,7 +234,7 @@ describe("parseModelPattern", () => {
|
||||
test("qwen3-coder:exacto:high matches model with high thinking level", () => {
|
||||
const result = parseModelPattern("qwen/qwen3-coder:exacto:high", allModels);
|
||||
expect(result.model?.id).toBe("qwen/qwen3-coder:exacto");
|
||||
expect(result.thinkingLevel).toBe("high");
|
||||
expect(result.thinkingLevel).toBe(Effort.High);
|
||||
expect(result.explicitThinkingLevel).toBe(true);
|
||||
expect(result.warning).toBeUndefined();
|
||||
});
|
||||
@@ -223,7 +243,7 @@ describe("parseModelPattern", () => {
|
||||
const result = parseModelPattern("openrouter/qwen/qwen3-coder:exacto:high", allModels);
|
||||
expect(result.model?.id).toBe("qwen/qwen3-coder:exacto");
|
||||
expect(result.model?.provider).toBe("openrouter");
|
||||
expect(result.thinkingLevel).toBe("high");
|
||||
expect(result.thinkingLevel).toBe(Effort.High);
|
||||
expect(result.explicitThinkingLevel).toBe(true);
|
||||
expect(result.warning).toBeUndefined();
|
||||
});
|
||||
@@ -308,7 +328,7 @@ describe("resolveModelRoleValue", () => {
|
||||
|
||||
expect(result.model?.provider).toBe("openrouter");
|
||||
expect(result.model?.id).toBe("qwen/qwen3-coder:exacto");
|
||||
expect(result.thinkingLevel).toBe("high");
|
||||
expect(result.thinkingLevel).toBe(Effort.High);
|
||||
expect(result.explicitThinkingLevel).toBe(true);
|
||||
});
|
||||
|
||||
@@ -330,15 +350,24 @@ describe("resolveModelRoleValue", () => {
|
||||
const providerQualified = resolveModelRoleValue("openai-codex/gpt-5.3-codex:xhigh", allModels);
|
||||
expect(providerQualified.model?.provider).toBe("openai-codex");
|
||||
expect(providerQualified.model?.id).toBe("gpt-5.3-codex");
|
||||
expect(providerQualified.thinkingLevel).toBe("xhigh");
|
||||
expect(providerQualified.thinkingLevel).toBe(Effort.XHigh);
|
||||
expect(providerQualified.explicitThinkingLevel).toBe(true);
|
||||
|
||||
const idOnly = resolveModelRoleValue("gpt-5.3-codex:xhigh", allModels);
|
||||
expect(idOnly.model?.provider).toBe("openai-codex");
|
||||
expect(idOnly.model?.id).toBe("gpt-5.3-codex");
|
||||
expect(idOnly.thinkingLevel).toBe("xhigh");
|
||||
expect(idOnly.thinkingLevel).toBe(Effort.XHigh);
|
||||
expect(idOnly.explicitThinkingLevel).toBe(true);
|
||||
});
|
||||
|
||||
test("clamps explicit thinking selectors from model metadata", () => {
|
||||
const result = resolveModelRoleValue("anthropic/claude-sonnet-4-5:xhigh", allModels);
|
||||
|
||||
expect(result.model?.provider).toBe("anthropic");
|
||||
expect(result.model?.id).toBe("claude-sonnet-4-5");
|
||||
expect(result.thinkingLevel).toBe(Effort.High);
|
||||
expect(result.explicitThinkingLevel).toBe(true);
|
||||
});
|
||||
});
|
||||
describe("resolveModelFromString", () => {
|
||||
test("falls back to pattern parsing for provider/model:thinking when strict provider+id miss", () => {
|
||||
@@ -376,7 +405,7 @@ describe("resolveModelOverride", () => {
|
||||
|
||||
expect(result.model?.provider).toBe("openrouter");
|
||||
expect(result.model?.id).toBe("qwen/qwen3-coder:exacto");
|
||||
expect(result.thinkingLevel).toBe("high");
|
||||
expect(result.thinkingLevel).toBe(Effort.High);
|
||||
expect(result.explicitThinkingLevel).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -424,7 +453,7 @@ describe("resolveCliModel", () => {
|
||||
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.model?.id).toBe("claude-sonnet-4-5");
|
||||
expect(result.thinkingLevel).toBe("high");
|
||||
expect(result.thinkingLevel).toBe(Effort.High);
|
||||
});
|
||||
|
||||
test("prefers exact model id match over provider inference (OpenRouter-style ids)", () => {
|
||||
@@ -507,11 +536,11 @@ describe("parseModelString", () => {
|
||||
describe("thinking level suffix extraction", () => {
|
||||
test("extracts valid thinking level from provider/id:level", () => {
|
||||
const result = parseModelString("anthropic/claude-sonnet-4-5:high");
|
||||
expect(result).toEqual({ provider: "anthropic", id: "claude-sonnet-4-5", thinkingLevel: "high" });
|
||||
expect(result).toEqual({ provider: "anthropic", id: "claude-sonnet-4-5", thinkingLevel: Effort.High });
|
||||
});
|
||||
|
||||
test("extracts all valid thinking levels", () => {
|
||||
const levels = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
||||
const levels = ["off", Effort.Minimal, Effort.Low, Effort.Medium, Effort.High, Effort.XHigh] as const;
|
||||
for (const level of levels) {
|
||||
const result = parseModelString(`anthropic/claude-sonnet-4-5:${level}`);
|
||||
expect(result?.id).toBe("claude-sonnet-4-5");
|
||||
@@ -527,7 +556,11 @@ describe("parseModelString", () => {
|
||||
test("handles model ID with colon followed by valid thinking level", () => {
|
||||
// e.g. "openrouter/qwen/qwen3-coder:exacto:high" — last colon is thinking level
|
||||
const result = parseModelString("openrouter/qwen/qwen3-coder:exacto:high");
|
||||
expect(result).toEqual({ provider: "openrouter", id: "qwen/qwen3-coder:exacto", thinkingLevel: "high" });
|
||||
expect(result).toEqual({
|
||||
provider: "openrouter",
|
||||
id: "qwen/qwen3-coder:exacto",
|
||||
thinkingLevel: Effort.High,
|
||||
});
|
||||
});
|
||||
|
||||
test("does not extract thinking level from model ID with invalid suffix", () => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import type { AgentEvent, AgentMessage } from "@oh-my-pi/pi-agent-core";
|
||||
import type { AssistantMessage, TextContent } from "@oh-my-pi/pi-ai";
|
||||
import { type AssistantMessage, Effort, type TextContent } from "@oh-my-pi/pi-ai";
|
||||
import {
|
||||
type CompactionEntry,
|
||||
type FileEntry,
|
||||
@@ -198,11 +198,11 @@ describe.skipIf(!e2eApiKey("ANTHROPIC_API_KEY"))("RPC mode", () => {
|
||||
await client.start();
|
||||
|
||||
// Set thinking level
|
||||
await client.setThinkingLevel("high");
|
||||
await client.setThinkingLevel(Effort.High);
|
||||
|
||||
// Verify via state
|
||||
const state = await client.getState();
|
||||
expect(state.thinkingLevel).toBe("high");
|
||||
expect(state.thinkingLevel).toBe(Effort.High);
|
||||
}, 30000);
|
||||
|
||||
test("should cycle thinking level", async () => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { Effort } from "@oh-my-pi/pi-ai";
|
||||
import { _resetSettingsForTest, Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
||||
import { getPathsForTab, getUi } from "@oh-my-pi/pi-coding-agent/config/settings-schema";
|
||||
import { getProjectAgentDir, Snowflake } from "@oh-my-pi/pi-utils";
|
||||
@@ -70,12 +71,12 @@ describe("Settings", () => {
|
||||
});
|
||||
|
||||
// Settings saves a change - should merge, not overwrite
|
||||
settings.set("defaultThinkingLevel", "high");
|
||||
settings.set("defaultThinkingLevel", Effort.High);
|
||||
await settings.flush();
|
||||
|
||||
const savedSettings = await readSettings();
|
||||
expect(savedSettings.enabledModels).toEqual(["claude-opus-4-5", "gpt-5.2-codex"]);
|
||||
expect(savedSettings.defaultThinkingLevel).toBe("high");
|
||||
expect(savedSettings.defaultThinkingLevel).toBe(Effort.High);
|
||||
expect(savedSettings.theme).toEqual({ dark: "anthracite" });
|
||||
expect((savedSettings.modelRoles as { default?: string } | undefined)?.default).toBe("claude-sonnet");
|
||||
});
|
||||
@@ -111,14 +112,14 @@ describe("Settings", () => {
|
||||
|
||||
await writeSettings({
|
||||
theme: { dark: "anthracite" },
|
||||
defaultThinkingLevel: "low",
|
||||
defaultThinkingLevel: Effort.Low,
|
||||
});
|
||||
|
||||
settings.set("defaultThinkingLevel", "high");
|
||||
settings.set("defaultThinkingLevel", Effort.High);
|
||||
await settings.flush();
|
||||
|
||||
const savedSettings = await readSettings();
|
||||
expect(savedSettings.defaultThinkingLevel).toBe("high");
|
||||
expect(savedSettings.defaultThinkingLevel).toBe(Effort.High);
|
||||
});
|
||||
});
|
||||
describe("compaction remote setting", () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { afterEach, describe, expect, it, vi } from "bun:test";
|
||||
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
||||
import { type AssistantMessage, Effort } from "@oh-my-pi/pi-ai";
|
||||
import { Settings } from "../../src/config/settings";
|
||||
import type { LoadExtensionsResult } from "../../src/extensibility/extensions/types";
|
||||
import * as sdkModule from "../../src/sdk";
|
||||
@@ -241,7 +241,7 @@ describe("runSubprocess submit_result reminders", () => {
|
||||
...baseOptions,
|
||||
id: "subagent-thinking-fallback",
|
||||
modelOverride: "openai/gpt-4o",
|
||||
thinkingLevel: "high",
|
||||
thinkingLevel: Effort.High,
|
||||
modelRegistry,
|
||||
});
|
||||
|
||||
@@ -260,7 +260,7 @@ describe("runSubprocess submit_result reminders", () => {
|
||||
} as unknown as import("../../src/config/model-registry").ModelRegistry;
|
||||
|
||||
const cases = [
|
||||
{ modelOverride: "openai/gpt-4o:low", expectedThinkingLevel: "low" },
|
||||
{ modelOverride: "openai/gpt-4o:low", expectedThinkingLevel: Effort.Low },
|
||||
{ modelOverride: "openai/gpt-4o:off", expectedThinkingLevel: "off" },
|
||||
] as const;
|
||||
|
||||
@@ -290,7 +290,7 @@ describe("runSubprocess submit_result reminders", () => {
|
||||
...baseOptions,
|
||||
id: `subagent-thinking-override-${index}`,
|
||||
modelOverride: testCase.modelOverride,
|
||||
thinkingLevel: "high",
|
||||
thinkingLevel: Effort.High,
|
||||
modelRegistry,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,13 +11,22 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { parseArgs } from "node:util";
|
||||
import { getAvailableThinkingLevels, parseThinkingLevel, type ThinkingLevel } from "@oh-my-pi/pi-ai";
|
||||
import { type ResolvedThinkingLevel, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import { Effort, THINKING_EFFORTS } from "@oh-my-pi/pi-ai";
|
||||
import { padding } from "@oh-my-pi/pi-tui";
|
||||
import { TempDir } from "@oh-my-pi/pi-utils";
|
||||
import { generateJsonReport, generateReport } from "./report";
|
||||
import { type BenchmarkConfig, type ProgressEvent, runBenchmark } from "./runner";
|
||||
import { type EditTask, loadTasksFromDir, validateFixturesFromDir } from "./tasks";
|
||||
|
||||
function parseThinkingLevel(value: string | null | undefined): ResolvedThinkingLevel | undefined {
|
||||
return value !== undefined &&
|
||||
value !== null &&
|
||||
[ThinkingLevel.Off, ...THINKING_EFFORTS].includes(value as ResolvedThinkingLevel)
|
||||
? (value as ResolvedThinkingLevel)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function generateReportFilename(config: BenchmarkConfig, format: "markdown" | "json"): string {
|
||||
const modelName = config.model
|
||||
.split("/")
|
||||
@@ -207,12 +216,12 @@ async function main(): Promise<void> {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
let thinkingLevel: ThinkingLevel = "low";
|
||||
let thinkingLevel: ResolvedThinkingLevel = Effort.Low;
|
||||
if (values.thinking) {
|
||||
const level = parseThinkingLevel(values.thinking);
|
||||
if (!level) {
|
||||
console.error(`Invalid thinking level: ${values.thinking}`);
|
||||
console.error(`Valid levels: ${getAvailableThinkingLevels().join(", ")}`);
|
||||
console.error(`Valid levels: ${[ThinkingLevel.Off, ...THINKING_EFFORTS].join(", ")}`);
|
||||
process.exit(1);
|
||||
}
|
||||
thinkingLevel = level;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
/// <reference types="./bun-imports.d.ts" />
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import type { ThinkingLevel } from "@oh-my-pi/pi-ai";
|
||||
import type { ResolvedThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
|
||||
import { computeLineHash, RpcClient, renderPromptTemplate } from "@oh-my-pi/pi-coding-agent";
|
||||
import { Snowflake } from "@oh-my-pi/pi-utils";
|
||||
@@ -31,7 +31,7 @@ function makeTempDir(pre?: string): string {
|
||||
export interface BenchmarkConfig {
|
||||
provider: string;
|
||||
model: string;
|
||||
thinkingLevel?: ThinkingLevel;
|
||||
thinkingLevel?: ResolvedThinkingLevel;
|
||||
runsPerTask: number;
|
||||
timeout: number;
|
||||
maxTurns?: number;
|
||||
|
||||
@@ -0,0 +1,428 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import asdict, dataclass
|
||||
from pathlib import Path
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
|
||||
if __package__ in (None, ""):
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from tool_io import ReservoirSample, ToolIOConfig, ToolInvocation, iter_tool_invocations, list_recent_session_files
|
||||
else:
|
||||
from scripts.tool_io import ReservoirSample, ToolIOConfig, ToolInvocation, iter_tool_invocations, list_recent_session_files
|
||||
|
||||
TOOL_NAMES = ("edit", "ast_edit")
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class DiffSummary:
|
||||
small: bool
|
||||
added_lines: int
|
||||
removed_lines: int
|
||||
changed_lines: int
|
||||
changed_preview: list[str]
|
||||
category: str | None = None
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class CompletedEdit:
|
||||
session_file: str
|
||||
tool_call_id: str
|
||||
tool_name: str
|
||||
path: str
|
||||
args: dict[str, object]
|
||||
result_text: str
|
||||
diff: str | None
|
||||
is_error: bool
|
||||
assistant_thinking: str | None
|
||||
assistant_timestamp: str | None
|
||||
tool_timestamp: str | None
|
||||
issue: str
|
||||
small: bool
|
||||
small_category: str | None
|
||||
added_lines: int
|
||||
removed_lines: int
|
||||
changed_lines: int
|
||||
changed_preview: list[str]
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class PreviousEditSummary:
|
||||
tool_name: str
|
||||
path: str
|
||||
issue: str
|
||||
is_error: bool
|
||||
small: bool
|
||||
same_path: bool
|
||||
changed_preview: list[str]
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Candidate:
|
||||
kind: str
|
||||
edit: CompletedEdit
|
||||
previous_edit: PreviousEditSummary | None = None
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class RunStats:
|
||||
files_scanned: int = 0
|
||||
total_edit_attempts: int = 0
|
||||
failed_edits: int = 0
|
||||
small_edits: int = 0
|
||||
small_edits_with_previous_edit: int = 0
|
||||
small_edits_with_previous_same_path: int = 0
|
||||
small_edits_after_failed_edit: int = 0
|
||||
small_edits_after_same_path_failed_edit: int = 0
|
||||
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Analyze small edit/ast_edit tool usage in session logs.")
|
||||
parser.add_argument("--sessions-dir", type=Path, default=Path.home() / ".omp" / "agent" / "sessions")
|
||||
parser.add_argument("--sample-size", type=positive_int, default=30)
|
||||
parser.add_argument("--max-files", type=positive_int, default=500)
|
||||
parser.add_argument("--since-days", type=positive_int, default=30)
|
||||
parser.add_argument("--max-items", type=positive_int, default=50_000)
|
||||
parser.add_argument("--limit-mode", choices=("calls", "events"), default="calls")
|
||||
parser.add_argument("--json", action="store_true")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
|
||||
def positive_int(value: str) -> int:
|
||||
parsed = int(value)
|
||||
if parsed <= 0:
|
||||
raise argparse.ArgumentTypeError("value must be a positive integer")
|
||||
return parsed
|
||||
|
||||
|
||||
|
||||
def strip_decorations(line: str) -> str:
|
||||
return re.sub(r"^\s*\d+\s+", "", line).strip()
|
||||
|
||||
|
||||
def is_delimiter_line(line: str) -> bool:
|
||||
return bool(re.match(r"^[\]}),;]+$", line))
|
||||
|
||||
|
||||
|
||||
def is_tiny_structural_line(line: str) -> bool:
|
||||
if len(line) == 0:
|
||||
return True
|
||||
if is_delimiter_line(line):
|
||||
return True
|
||||
if re.match(r"^(pub\s+mod|pub\s+use|mod|use|import|export)\b", line):
|
||||
return True
|
||||
if re.match(r"^(return|break|continue);$", line):
|
||||
return True
|
||||
if re.match(r"^[A-Za-z0-9_.$]+\([^)]*\);$", line) and len(line) <= 60:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
|
||||
def classify_success_issue(summary: DiffSummary) -> str:
|
||||
previews = summary.changed_preview
|
||||
if previews and all(len(line) == 0 for line in previews):
|
||||
return "blank-line-adjustment"
|
||||
if previews and all(is_delimiter_line(line) for line in previews):
|
||||
return "delimiter-adjustment"
|
||||
if previews and all(re.match(r"^(pub\s+mod|pub\s+use|mod|use|import|export)\b", line) for line in previews):
|
||||
return "import-or-module-tweak"
|
||||
if summary.removed_lines == 1 and summary.added_lines == 0:
|
||||
return "single-line-delete"
|
||||
if summary.added_lines == 1 and summary.removed_lines == 0:
|
||||
return "single-line-add"
|
||||
if summary.added_lines == 1 and summary.removed_lines == 1:
|
||||
return "single-line-replace"
|
||||
return "small-structural-fix"
|
||||
|
||||
|
||||
|
||||
def classify_failure_issue(result_text: str) -> str:
|
||||
if re.search(r"identical content|No changes made", result_text, re.IGNORECASE):
|
||||
return "no-op-identical"
|
||||
if re.search(r"Failed to find context|matches for context|expected lines|tag mismatch|>>>", result_text, re.IGNORECASE):
|
||||
return "context-mismatch"
|
||||
if re.search(r"Unexpected line in hunk|parse error|SyntaxError", result_text, re.IGNORECASE):
|
||||
return "invalid-patch-shape"
|
||||
if re.search(r"File not found", result_text, re.IGNORECASE):
|
||||
return "missing-file"
|
||||
if re.search(r"occurrence|ambiguous", result_text, re.IGNORECASE):
|
||||
return "ambiguous-target"
|
||||
if re.search(r"Validation failed|required property|must have required property", result_text, re.IGNORECASE):
|
||||
return "invalid-arguments"
|
||||
return "other-failure"
|
||||
|
||||
|
||||
|
||||
def summarize_diff(diff: str | None) -> DiffSummary:
|
||||
if not diff:
|
||||
return DiffSummary(
|
||||
small=False,
|
||||
added_lines=0,
|
||||
removed_lines=0,
|
||||
changed_lines=0,
|
||||
changed_preview=[],
|
||||
)
|
||||
|
||||
added: list[str] = []
|
||||
removed: list[str] = []
|
||||
for raw_line in diff.splitlines():
|
||||
if raw_line.startswith(("+++", "---", "@@")):
|
||||
continue
|
||||
if raw_line.startswith("+"):
|
||||
added.append(strip_decorations(raw_line[1:]))
|
||||
continue
|
||||
if raw_line.startswith("-"):
|
||||
removed.append(strip_decorations(raw_line[1:]))
|
||||
|
||||
all_changes = [*removed, *added]
|
||||
include_blank = previews_need_blank_marker(all_changes)
|
||||
previews = [line for line in all_changes if line or include_blank]
|
||||
changed_lines = len(added) + len(removed)
|
||||
tiny_only = all(is_tiny_structural_line(line) for line in all_changes)
|
||||
small = changed_lines > 0 and (changed_lines <= 2 or (changed_lines <= 4 and tiny_only))
|
||||
preview_slice = previews[:4]
|
||||
category = None
|
||||
if small:
|
||||
category = classify_success_issue(
|
||||
DiffSummary(
|
||||
small=small,
|
||||
added_lines=len(added),
|
||||
removed_lines=len(removed),
|
||||
changed_lines=changed_lines,
|
||||
changed_preview=preview_slice,
|
||||
)
|
||||
)
|
||||
return DiffSummary(
|
||||
small=small,
|
||||
added_lines=len(added),
|
||||
removed_lines=len(removed),
|
||||
changed_lines=changed_lines,
|
||||
changed_preview=preview_slice,
|
||||
category=category,
|
||||
)
|
||||
|
||||
|
||||
|
||||
def previews_need_blank_marker(lines: list[str]) -> bool:
|
||||
return any(len(line) == 0 for line in lines)
|
||||
|
||||
|
||||
|
||||
def build_completed_edit(invocation: ToolInvocation) -> CompletedEdit | None:
|
||||
if not invocation.has_result:
|
||||
return None
|
||||
diff_summary = summarize_diff(invocation.diff)
|
||||
is_error = invocation.is_error
|
||||
issue = classify_failure_issue(invocation.result_text) if is_error else (diff_summary.category or "other-success")
|
||||
return CompletedEdit(
|
||||
session_file=str(invocation.session_file),
|
||||
tool_call_id=invocation.tool_call_id,
|
||||
tool_name=invocation.tool_name,
|
||||
path=invocation.path_hint,
|
||||
args=invocation.arguments,
|
||||
result_text=invocation.result_text,
|
||||
diff=invocation.diff,
|
||||
is_error=is_error,
|
||||
assistant_thinking=invocation.assistant_thinking,
|
||||
assistant_timestamp=invocation.assistant_timestamp,
|
||||
tool_timestamp=invocation.tool_timestamp,
|
||||
issue=issue,
|
||||
small=(not is_error and diff_summary.small),
|
||||
small_category=diff_summary.category,
|
||||
added_lines=diff_summary.added_lines,
|
||||
removed_lines=diff_summary.removed_lines,
|
||||
changed_lines=diff_summary.changed_lines,
|
||||
changed_preview=diff_summary.changed_preview,
|
||||
)
|
||||
|
||||
|
||||
|
||||
def analyze_small_edits(stream: Iterable[ToolInvocation], *, sample_size: int, files_scanned: int) -> dict[str, object]:
|
||||
sample: ReservoirSample[Candidate] = ReservoirSample(size=sample_size)
|
||||
issue_counts: dict[str, int] = {}
|
||||
stats = RunStats(files_scanned=files_scanned)
|
||||
last_edit: CompletedEdit | None = None
|
||||
|
||||
for invocation in stream:
|
||||
completed = build_completed_edit(invocation)
|
||||
if completed is None:
|
||||
continue
|
||||
stats.total_edit_attempts += 1
|
||||
|
||||
if completed.is_error:
|
||||
stats.failed_edits += 1
|
||||
issue_counts[completed.issue] = issue_counts.get(completed.issue, 0) + 1
|
||||
sample.add(Candidate(kind="failed", edit=completed))
|
||||
|
||||
if completed.small:
|
||||
stats.small_edits += 1
|
||||
issue_counts[completed.issue] = issue_counts.get(completed.issue, 0) + 1
|
||||
previous = None
|
||||
if last_edit is not None:
|
||||
stats.small_edits_with_previous_edit += 1
|
||||
if last_edit.is_error:
|
||||
stats.small_edits_after_failed_edit += 1
|
||||
if last_edit.path and last_edit.path == completed.path:
|
||||
stats.small_edits_with_previous_same_path += 1
|
||||
if last_edit.is_error:
|
||||
stats.small_edits_after_same_path_failed_edit += 1
|
||||
previous = PreviousEditSummary(
|
||||
tool_name=last_edit.tool_name,
|
||||
path=last_edit.path,
|
||||
issue=last_edit.issue,
|
||||
is_error=last_edit.is_error,
|
||||
small=last_edit.small,
|
||||
same_path=last_edit.path == completed.path,
|
||||
changed_preview=last_edit.changed_preview,
|
||||
)
|
||||
sample.add(Candidate(kind="small", edit=completed, previous_edit=previous))
|
||||
|
||||
last_edit = completed
|
||||
|
||||
return {
|
||||
"stats": asdict(stats),
|
||||
"top_issues": top_entries(issue_counts, 20),
|
||||
"sample": [candidate_to_dict(candidate) for candidate in sample.items],
|
||||
}
|
||||
|
||||
|
||||
|
||||
def candidate_to_dict(candidate: Candidate) -> dict[str, object]:
|
||||
payload = {"kind": candidate.kind, "edit": asdict(candidate.edit)}
|
||||
if candidate.previous_edit is not None:
|
||||
payload["previous_edit"] = asdict(candidate.previous_edit)
|
||||
return payload
|
||||
|
||||
|
||||
|
||||
def top_entries(counts: dict[str, int], limit: int) -> list[dict[str, object]]:
|
||||
return [
|
||||
{"name": name, "count": count}
|
||||
for name, count in sorted(counts.items(), key=lambda entry: (-entry[1], entry[0]))[:limit]
|
||||
]
|
||||
|
||||
|
||||
|
||||
def short_path(target_path: str) -> str:
|
||||
home = str(Path.home())
|
||||
return f"~{target_path[len(home):]}" if target_path.startswith(home) else target_path
|
||||
|
||||
|
||||
|
||||
def truncate(text: str, limit: int) -> str:
|
||||
if len(text) <= limit:
|
||||
return text
|
||||
return f"{text[: limit - 1]}…"
|
||||
|
||||
|
||||
|
||||
def format_sample_entry(candidate: dict[str, object], index: int) -> str:
|
||||
edit = candidate["edit"]
|
||||
assert isinstance(edit, dict)
|
||||
lines = [
|
||||
f"{index + 1}. [{candidate['kind']}] {edit['issue']}",
|
||||
f" file: {Path(str(edit['session_file'])).name}",
|
||||
f" target: {short_path(str(edit['path'])) if edit['path'] else '(unknown path)'}",
|
||||
f" tool: {edit['tool_name']}",
|
||||
]
|
||||
if candidate["kind"] == "small":
|
||||
lines.append(
|
||||
f" change: +{edit['added_lines']} / -{edit['removed_lines']} ({edit['changed_lines']} changed line(s))"
|
||||
)
|
||||
changed_preview = edit.get("changed_preview")
|
||||
if isinstance(changed_preview, list) and changed_preview:
|
||||
lines.append(f" preview: {' | '.join(str(item) for item in changed_preview)}")
|
||||
previous = candidate.get("previous_edit")
|
||||
if isinstance(previous, dict):
|
||||
path_part = f" ({short_path(str(previous['path']))})" if previous.get("path") else ""
|
||||
lines.append(
|
||||
" previous edit: "
|
||||
f"{'same-path' if previous.get('same_path') else 'other-path'} "
|
||||
f"{'failed' if previous.get('is_error') else previous.get('issue')}{path_part}"
|
||||
)
|
||||
previous_preview = previous.get("changed_preview")
|
||||
if isinstance(previous_preview, list) and previous_preview:
|
||||
lines.append(f" previous preview: {' | '.join(str(item) for item in previous_preview)}")
|
||||
else:
|
||||
lines.append(" previous edit: none")
|
||||
else:
|
||||
lines.append(f" result: {truncate(' '.join(str(edit['result_text']).split()), 220)}")
|
||||
changed_preview = edit.get("changed_preview")
|
||||
if isinstance(changed_preview, list) and changed_preview:
|
||||
lines.append(f" diff preview: {' | '.join(str(item) for item in changed_preview)}")
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
|
||||
def main() -> None:
|
||||
options = parse_args()
|
||||
config = ToolIOConfig(
|
||||
sessions_dir=options.sessions_dir,
|
||||
since_days=options.since_days,
|
||||
max_files=options.max_files,
|
||||
max_items=options.max_items,
|
||||
limit_mode=options.limit_mode,
|
||||
include_unresolved=False,
|
||||
)
|
||||
files = list_recent_session_files(config)
|
||||
stream = iter_tool_invocations(TOOL_NAMES, config)
|
||||
analysis = analyze_small_edits(stream, sample_size=options.sample_size, files_scanned=len(files))
|
||||
|
||||
if options.json:
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"options": {
|
||||
"sessions_dir": str(options.sessions_dir),
|
||||
"sample_size": options.sample_size,
|
||||
"max_files": options.max_files,
|
||||
"since_days": options.since_days,
|
||||
"max_items": options.max_items,
|
||||
"limit_mode": options.limit_mode,
|
||||
"json": options.json,
|
||||
},
|
||||
**analysis,
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
stats = analysis["stats"]
|
||||
top_issues = analysis["top_issues"]
|
||||
sample = analysis["sample"]
|
||||
assert isinstance(stats, dict)
|
||||
assert isinstance(top_issues, list)
|
||||
assert isinstance(sample, list)
|
||||
print(f"Scanned {stats['files_scanned']} session file(s) from {short_path(str(options.sessions_dir))}")
|
||||
print(f"Edit attempts: {stats['total_edit_attempts']}")
|
||||
print(f"Failed edits: {stats['failed_edits']}")
|
||||
print(f"Small edits: {stats['small_edits']}")
|
||||
print(f"Small edits with previous edit: {stats['small_edits_with_previous_edit']}")
|
||||
print(f"Small edits with previous same-path edit: {stats['small_edits_with_previous_same_path']}")
|
||||
print(f"Small edits after failed edit: {stats['small_edits_after_failed_edit']}")
|
||||
print(f"Small edits after same-path failed edit: {stats['small_edits_after_same_path_failed_edit']}")
|
||||
print()
|
||||
print("Top issues:")
|
||||
for entry in top_issues[:12]:
|
||||
assert isinstance(entry, dict)
|
||||
print(f" - {entry['name']}: {entry['count']}")
|
||||
print()
|
||||
print(f"Random sample ({len(sample)}):")
|
||||
for index, candidate in enumerate(sample):
|
||||
assert isinstance(candidate, dict)
|
||||
print(format_sample_entry(candidate, index))
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,386 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable, Iterator
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
import json
|
||||
import random
|
||||
import time
|
||||
from typing import Any, Literal
|
||||
|
||||
LimitMode = Literal["calls", "events"]
|
||||
|
||||
DEFAULT_MAX_ITEMS = 50_000
|
||||
DEFAULT_SINCE_DAYS = 30
|
||||
DEFAULT_MAX_FILES = 500
|
||||
DEFAULT_SESSIONS_DIR = Path.home() / ".omp" / "agent" / "sessions"
|
||||
|
||||
|
||||
TOOL_GROUPS: dict[str, tuple[str, ...]] = {
|
||||
"edits": ("edit", "ast_edit"),
|
||||
"reads": ("read", "grep", "find", "ast_grep", "lsp"),
|
||||
"writes": ("edit", "ast_edit", "write"),
|
||||
}
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ToolIOConfig:
|
||||
sessions_dir: Path = DEFAULT_SESSIONS_DIR
|
||||
since_days: int = DEFAULT_SINCE_DAYS
|
||||
max_files: int = DEFAULT_MAX_FILES
|
||||
max_items: int = DEFAULT_MAX_ITEMS
|
||||
limit_mode: LimitMode = "calls"
|
||||
include_unresolved: bool = True
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ToolCall:
|
||||
session_file: Path
|
||||
tool_call_id: str
|
||||
tool_name: str
|
||||
arguments: dict[str, Any]
|
||||
assistant_thinking: str | None = None
|
||||
assistant_timestamp: str | None = None
|
||||
path_hint: str = ""
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ToolResult:
|
||||
tool_call_id: str
|
||||
tool_name: str
|
||||
is_error: bool
|
||||
result_text: str
|
||||
details: dict[str, Any] = field(default_factory=dict)
|
||||
tool_timestamp: str | None = None
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ToolInvocation:
|
||||
call: ToolCall
|
||||
result: ToolResult | None = None
|
||||
|
||||
@property
|
||||
def session_file(self) -> Path:
|
||||
return self.call.session_file
|
||||
|
||||
@property
|
||||
def tool_call_id(self) -> str:
|
||||
return self.call.tool_call_id
|
||||
|
||||
@property
|
||||
def tool_name(self) -> str:
|
||||
return self.call.tool_name
|
||||
|
||||
@property
|
||||
def arguments(self) -> dict[str, Any]:
|
||||
return self.call.arguments
|
||||
|
||||
@property
|
||||
def assistant_thinking(self) -> str | None:
|
||||
return self.call.assistant_thinking
|
||||
|
||||
@property
|
||||
def assistant_timestamp(self) -> str | None:
|
||||
return self.call.assistant_timestamp
|
||||
|
||||
@property
|
||||
def tool_timestamp(self) -> str | None:
|
||||
return self.result.tool_timestamp if self.result else None
|
||||
|
||||
@property
|
||||
def path_hint(self) -> str:
|
||||
return self.call.path_hint
|
||||
|
||||
@property
|
||||
def has_result(self) -> bool:
|
||||
return self.result is not None
|
||||
|
||||
@property
|
||||
def is_error(self) -> bool:
|
||||
return bool(self.result and self.result.is_error)
|
||||
|
||||
@property
|
||||
def result_text(self) -> str:
|
||||
return self.result.result_text if self.result else ""
|
||||
|
||||
@property
|
||||
def details(self) -> dict[str, Any]:
|
||||
return self.result.details if self.result else {}
|
||||
|
||||
@property
|
||||
def diff(self) -> str | None:
|
||||
diff = self.details.get("diff")
|
||||
return diff if isinstance(diff, str) else None
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ReservoirSample[T]:
|
||||
size: int
|
||||
items: list[T] = field(default_factory=list)
|
||||
seen: int = 0
|
||||
rng: random.Random = field(default_factory=random.Random)
|
||||
|
||||
def add(self, item: T) -> None:
|
||||
if self.size <= 0:
|
||||
return
|
||||
self.seen += 1
|
||||
if len(self.items) < self.size:
|
||||
self.items.append(item)
|
||||
return
|
||||
index = self.rng.randrange(self.seen)
|
||||
if index < self.size:
|
||||
self.items[index] = item
|
||||
|
||||
|
||||
|
||||
def list_recent_session_files(config: ToolIOConfig) -> list[Path]:
|
||||
min_mtime = time.time() - config.since_days * 24 * 60 * 60
|
||||
candidates: list[tuple[float, Path]] = []
|
||||
for session_file in config.sessions_dir.rglob("*.jsonl"):
|
||||
try:
|
||||
stat = session_file.stat()
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
if stat.st_mtime < min_mtime:
|
||||
continue
|
||||
candidates.append((stat.st_mtime, session_file))
|
||||
candidates.sort(key=lambda entry: entry[0], reverse=True)
|
||||
return [entry[1] for entry in candidates[: config.max_files]]
|
||||
|
||||
|
||||
|
||||
def iter_tool_invocations(
|
||||
tool_names: str | Iterable[str],
|
||||
config: ToolIOConfig | None = None,
|
||||
) -> Iterator[ToolInvocation]:
|
||||
resolved = config or ToolIOConfig()
|
||||
wanted = _normalize_tool_names(tool_names)
|
||||
seen_items = 0
|
||||
|
||||
for session_file in list_recent_session_files(resolved):
|
||||
pending: dict[str, ToolCall] = {}
|
||||
for entry in _iter_session_entries(session_file):
|
||||
if entry.get("type") != "message":
|
||||
continue
|
||||
message = _as_record(entry.get("message"))
|
||||
if message is None:
|
||||
continue
|
||||
|
||||
role = message.get("role")
|
||||
if role == "assistant":
|
||||
content = message.get("content")
|
||||
if not isinstance(content, list):
|
||||
continue
|
||||
thinking = _extract_thinking(content)
|
||||
assistant_timestamp = _as_string(entry.get("timestamp"))
|
||||
for item in content:
|
||||
payload = _as_record(item)
|
||||
if payload is None:
|
||||
continue
|
||||
if payload.get("type") != "toolCall":
|
||||
continue
|
||||
tool_name = _as_string(payload.get("name"))
|
||||
tool_call_id = _as_string(payload.get("id"))
|
||||
if tool_name is None or tool_call_id is None or tool_name not in wanted:
|
||||
continue
|
||||
arguments = _as_record(payload.get("arguments")) or {}
|
||||
pending[tool_call_id] = ToolCall(
|
||||
session_file=session_file,
|
||||
tool_call_id=tool_call_id,
|
||||
tool_name=tool_name,
|
||||
arguments=arguments,
|
||||
assistant_thinking=thinking,
|
||||
assistant_timestamp=assistant_timestamp,
|
||||
path_hint=extract_path(arguments),
|
||||
)
|
||||
continue
|
||||
|
||||
if role != "toolResult":
|
||||
continue
|
||||
tool_name = _as_string(message.get("toolName"))
|
||||
tool_call_id = _as_string(message.get("toolCallId"))
|
||||
if tool_name is None or tool_call_id is None or tool_name not in wanted:
|
||||
continue
|
||||
pending_call = pending.pop(tool_call_id, None)
|
||||
if pending_call is None:
|
||||
continue
|
||||
result = ToolResult(
|
||||
tool_call_id=tool_call_id,
|
||||
tool_name=tool_name,
|
||||
is_error=message.get("isError") is True,
|
||||
result_text=extract_result_text(message),
|
||||
details=_as_record(message.get("details")) or {},
|
||||
tool_timestamp=_as_string(entry.get("timestamp")),
|
||||
)
|
||||
invocation = ToolInvocation(call=pending_call, result=result)
|
||||
seen_items += _event_weight(invocation, resolved.limit_mode)
|
||||
yield invocation
|
||||
if seen_items >= resolved.max_items:
|
||||
return
|
||||
|
||||
if not resolved.include_unresolved:
|
||||
continue
|
||||
for pending_call in pending.values():
|
||||
invocation = ToolInvocation(call=pending_call)
|
||||
seen_items += _event_weight(invocation, resolved.limit_mode)
|
||||
yield invocation
|
||||
if seen_items >= resolved.max_items:
|
||||
return
|
||||
|
||||
|
||||
|
||||
def iter_results(stream: Iterable[ToolInvocation]) -> Iterator[ToolInvocation]:
|
||||
for invocation in stream:
|
||||
if invocation.has_result:
|
||||
yield invocation
|
||||
|
||||
|
||||
|
||||
def iter_failed(stream: Iterable[ToolInvocation]) -> Iterator[ToolInvocation]:
|
||||
for invocation in stream:
|
||||
if invocation.is_error:
|
||||
yield invocation
|
||||
|
||||
|
||||
|
||||
def iter_successful(stream: Iterable[ToolInvocation]) -> Iterator[ToolInvocation]:
|
||||
for invocation in stream:
|
||||
if invocation.has_result and not invocation.is_error:
|
||||
yield invocation
|
||||
|
||||
|
||||
|
||||
def iter_with_diff(stream: Iterable[ToolInvocation]) -> Iterator[ToolInvocation]:
|
||||
for invocation in stream:
|
||||
if invocation.diff:
|
||||
yield invocation
|
||||
|
||||
|
||||
|
||||
def iter_paths(stream: Iterable[ToolInvocation], *paths: str) -> Iterator[ToolInvocation]:
|
||||
wanted = set(paths)
|
||||
for invocation in stream:
|
||||
if invocation.path_hint in wanted:
|
||||
yield invocation
|
||||
|
||||
|
||||
|
||||
def take(stream: Iterable[ToolInvocation], limit: int) -> Iterator[ToolInvocation]:
|
||||
if limit <= 0:
|
||||
return
|
||||
remaining = limit
|
||||
for invocation in stream:
|
||||
if remaining <= 0:
|
||||
return
|
||||
yield invocation
|
||||
remaining -= 1
|
||||
|
||||
|
||||
|
||||
def sample_reservoir[T](stream: Iterable[T], size: int, seed: int | None = None) -> list[T]:
|
||||
sample: ReservoirSample[T] = ReservoirSample(size=size, rng=random.Random(seed))
|
||||
for item in stream:
|
||||
sample.add(item)
|
||||
return sample.items
|
||||
|
||||
|
||||
|
||||
def extract_result_text(message: dict[str, Any] | None) -> str:
|
||||
if message is None:
|
||||
return ""
|
||||
content = message.get("content")
|
||||
if not isinstance(content, list):
|
||||
return ""
|
||||
for item in content:
|
||||
payload = _as_record(item)
|
||||
if payload is None:
|
||||
continue
|
||||
if payload.get("type") != "text":
|
||||
continue
|
||||
text = _as_string(payload.get("text"))
|
||||
if text is not None:
|
||||
return text
|
||||
return ""
|
||||
|
||||
|
||||
|
||||
def extract_path(arguments: dict[str, Any]) -> str:
|
||||
for key in ("path", "file", "move"):
|
||||
value = arguments.get(key)
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
return ""
|
||||
|
||||
|
||||
|
||||
def _iter_session_entries(session_file: Path) -> Iterator[dict[str, Any]]:
|
||||
with session_file.open("r", encoding="utf-8") as handle:
|
||||
for line in handle:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
entry = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
payload = _as_record(entry)
|
||||
if payload is not None:
|
||||
yield payload
|
||||
|
||||
|
||||
|
||||
def _extract_thinking(content: list[Any]) -> str | None:
|
||||
for item in content:
|
||||
payload = _as_record(item)
|
||||
if payload is None:
|
||||
continue
|
||||
if payload.get("type") != "thinking":
|
||||
continue
|
||||
thinking = _as_string(payload.get("thinking"))
|
||||
if thinking:
|
||||
return thinking
|
||||
return None
|
||||
|
||||
|
||||
|
||||
def resolve_tool_names(*names_or_groups: str) -> tuple[str, ...]:
|
||||
ordered: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for name in names_or_groups:
|
||||
expanded = TOOL_GROUPS.get(name, (name,))
|
||||
for tool_name in expanded:
|
||||
if tool_name in seen:
|
||||
continue
|
||||
seen.add(tool_name)
|
||||
ordered.append(tool_name)
|
||||
return tuple(ordered)
|
||||
|
||||
|
||||
def _normalize_tool_names(tool_names: str | Iterable[str]) -> set[str]:
|
||||
if isinstance(tool_names, str):
|
||||
return set(resolve_tool_names(tool_names))
|
||||
ordered: list[str] = []
|
||||
for name in tool_names:
|
||||
ordered.extend(resolve_tool_names(name))
|
||||
return set(ordered)
|
||||
|
||||
|
||||
|
||||
def _event_weight(invocation: ToolInvocation, limit_mode: LimitMode) -> int:
|
||||
if limit_mode == "calls":
|
||||
return 1
|
||||
return 2 if invocation.has_result else 1
|
||||
|
||||
|
||||
|
||||
def _as_record(value: Any) -> dict[str, Any] | None:
|
||||
if not isinstance(value, dict):
|
||||
return None
|
||||
return value
|
||||
|
||||
|
||||
|
||||
def _as_string(value: Any) -> str | None:
|
||||
return value if isinstance(value, str) else None
|
||||
Reference in New Issue
Block a user