Compare commits
49 Commits
25d6370b20
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
6c17462040
|
|||
|
1536cf384c
|
|||
|
d6842d7e29
|
|||
|
fbc0acda2a
|
|||
|
0327d041b6
|
|||
|
6a01fd4fbd
|
|||
| d822180205 | |||
|
89d0fdce26
|
|||
|
b3ecdce979
|
|||
|
3873821a31
|
|||
|
9c2801b643
|
|||
|
d78820dcd4
|
|||
|
d43c4232a2
|
|||
|
f41c85b703
|
|||
|
9e056bdcf0
|
|||
|
d6022b9f98
|
|||
|
6fc1abf94a
|
|||
|
92ea0f624e
|
|||
|
c3fd8fbc1c
|
|||
|
7fd3f7761c
|
|||
|
05e19098b2
|
|||
|
60067ae757
|
|||
|
c72003b0b6
|
|||
|
7c9d500116
|
|||
|
6b2c87b562
|
|||
|
b2dbdfb4b1
|
|||
|
063e198f96
|
|||
|
73cbe16ec1
|
|||
|
bdea854a9f
|
|||
|
9b4c800597
|
|||
|
eb4d1c02f4
|
|||
|
c428990900
|
|||
|
03b9cc70b9
|
|||
|
3fa0eb832c
|
|||
|
83f66e1061
|
|||
|
741b9c364c
|
|||
|
b6f6f456db
|
|||
|
00a6cf74d7
|
|||
|
d35ca352ca
|
|||
| 57dc1cb252 | |||
|
101a9cdd6e
|
|||
|
c5f52e1efb
|
|||
|
470149b606
|
|||
|
02062c5a50
|
|||
|
e6e99b6926
|
|||
|
15a293204f
|
|||
|
ecf3780aed
|
|||
|
e798747135
|
|||
|
60493728a0
|
@@ -0,0 +1,11 @@
|
|||||||
|
### AI assistance (if any):
|
||||||
|
- List tools here and files touched by them
|
||||||
|
|
||||||
|
### Authorship & Understanding
|
||||||
|
|
||||||
|
- [ ] I wrote or heavily modified this code myself
|
||||||
|
- [ ] I understand how it works end-to-end
|
||||||
|
- [ ] I can maintain this code in the future
|
||||||
|
- [ ] No undisclosed AI-generated code was used
|
||||||
|
- [ ] If AI assistance was used, it is documented below
|
||||||
|
|
||||||
@@ -76,6 +76,13 @@ Then, you can run workflows locally without having to commit and see if the GitH
|
|||||||
act -W .github/workflows/release.yml --input_type bump=minor
|
act -W .github/workflows/release.yml --input_type bump=minor
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Authorship Policy
|
||||||
|
|
||||||
|
All code in this repository is written and reviewed by humans. AI-generated code (e.g., Copilot, ChatGPT,
|
||||||
|
Claude, etc.) is not permitted unless explicitly disclosed and approved.
|
||||||
|
|
||||||
|
Submissions must certify that the contributor understands and can maintain the code they submit.
|
||||||
|
|
||||||
## Questions? Reach out to me!
|
## Questions? Reach out to me!
|
||||||
If you encounter any questions while developing Loki, please don't hesitate to reach out to me at
|
If you encounter any questions while developing Loki, please don't hesitate to reach out to me at
|
||||||
alex.j.tusa@gmail.com. I'm happy to help contributors in any way I can, regardless of if they're new or experienced!
|
alex.j.tusa@gmail.com. I'm happy to help contributors in any way I can, regardless of if they're new or experienced!
|
||||||
|
|||||||
Generated
+52
-70
@@ -1238,7 +1238,7 @@ dependencies = [
|
|||||||
"encode_unicode",
|
"encode_unicode",
|
||||||
"libc",
|
"libc",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"unicode-width 0.2.2",
|
"unicode-width",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1338,22 +1338,6 @@ version = "0.8.21"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossterm"
|
|
||||||
version = "0.25.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 1.3.2",
|
|
||||||
"crossterm_winapi",
|
|
||||||
"libc",
|
|
||||||
"mio 0.8.11",
|
|
||||||
"parking_lot",
|
|
||||||
"signal-hook",
|
|
||||||
"signal-hook-mio",
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossterm"
|
name = "crossterm"
|
||||||
version = "0.28.1"
|
version = "0.28.1"
|
||||||
@@ -1363,7 +1347,7 @@ dependencies = [
|
|||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"crossterm_winapi",
|
"crossterm_winapi",
|
||||||
"filedescriptor",
|
"filedescriptor",
|
||||||
"mio 1.1.1",
|
"mio",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"rustix 0.38.44",
|
"rustix 0.38.44",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -1382,7 +1366,7 @@ dependencies = [
|
|||||||
"crossterm_winapi",
|
"crossterm_winapi",
|
||||||
"derive_more 2.1.1",
|
"derive_more 2.1.1",
|
||||||
"document-features",
|
"document-features",
|
||||||
"mio 1.1.1",
|
"mio",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"rustix 1.1.3",
|
"rustix 1.1.3",
|
||||||
"signal-hook",
|
"signal-hook",
|
||||||
@@ -2164,7 +2148,7 @@ version = "0.2.24"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
|
checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-width 0.2.2",
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2838,19 +2822,16 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "inquire"
|
name = "inquire"
|
||||||
version = "0.7.5"
|
version = "0.9.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a"
|
checksum = "6654738b8024300cf062d04a1c13c10c8e2cea598ec1c47dc9b6641159429756"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"crossterm 0.25.0",
|
"crossterm 0.29.0",
|
||||||
"dyn-clone",
|
"dyn-clone",
|
||||||
"fuzzy-matcher",
|
"fuzzy-matcher",
|
||||||
"fxhash",
|
|
||||||
"newline-converter",
|
|
||||||
"once_cell",
|
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"unicode-width 0.1.14",
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2869,6 +2850,15 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is-docker"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "is-macro"
|
name = "is-macro"
|
||||||
version = "0.3.7"
|
version = "0.3.7"
|
||||||
@@ -2892,6 +2882,16 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is-wsl"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
|
||||||
|
dependencies = [
|
||||||
|
"is-docker",
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "is_executable"
|
name = "is_executable"
|
||||||
version = "1.0.5"
|
version = "1.0.5"
|
||||||
@@ -3193,6 +3193,7 @@ dependencies = [
|
|||||||
"log4rs",
|
"log4rs",
|
||||||
"nu-ansi-term",
|
"nu-ansi-term",
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
|
"open",
|
||||||
"os_info",
|
"os_info",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"path-absolutize",
|
"path-absolutize",
|
||||||
@@ -3222,7 +3223,8 @@ dependencies = [
|
|||||||
"tokio-graceful",
|
"tokio-graceful",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"unicode-width 0.2.2",
|
"unicode-width",
|
||||||
|
"url",
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
"uuid",
|
"uuid",
|
||||||
"which",
|
"which",
|
||||||
@@ -3436,18 +3438,6 @@ dependencies = [
|
|||||||
"simd-adler32",
|
"simd-adler32",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mio"
|
|
||||||
version = "0.8.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"log",
|
|
||||||
"wasi",
|
|
||||||
"windows-sys 0.48.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@@ -3512,15 +3502,6 @@ version = "1.0.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "newline-converter"
|
|
||||||
version = "0.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f"
|
|
||||||
dependencies = [
|
|
||||||
"unicode-segmentation",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nix"
|
name = "nix"
|
||||||
version = "0.26.4"
|
version = "0.26.4"
|
||||||
@@ -3869,6 +3850,17 @@ version = "0.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "open"
|
||||||
|
version = "5.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc"
|
||||||
|
dependencies = [
|
||||||
|
"is-wsl",
|
||||||
|
"libc",
|
||||||
|
"pathdiff",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl"
|
name = "openssl"
|
||||||
version = "0.10.75"
|
version = "0.10.75"
|
||||||
@@ -4039,6 +4031,12 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pathdiff"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pem"
|
name = "pem"
|
||||||
version = "3.0.6"
|
version = "3.0.6"
|
||||||
@@ -4517,7 +4515,7 @@ dependencies = [
|
|||||||
"strum_macros 0.26.4",
|
"strum_macros 0.26.4",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"unicode-width 0.2.2",
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5339,8 +5337,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
|
checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"mio 0.8.11",
|
"mio",
|
||||||
"mio 1.1.1",
|
|
||||||
"signal-hook",
|
"signal-hook",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -5644,7 +5641,7 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"memchr",
|
"memchr",
|
||||||
"mio 1.1.1",
|
"mio",
|
||||||
"terminal-trx",
|
"terminal-trx",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
"xterm-color",
|
"xterm-color",
|
||||||
@@ -5679,7 +5676,7 @@ checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"smawk",
|
"smawk",
|
||||||
"unicode-linebreak",
|
"unicode-linebreak",
|
||||||
"unicode-width 0.2.2",
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5824,7 +5821,7 @@ checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
"mio 1.1.1",
|
"mio",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
@@ -6255,12 +6252,6 @@ version = "1.12.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-width"
|
|
||||||
version = "0.1.14"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-width"
|
name = "unicode-width"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -6928,15 +6919,6 @@ dependencies = [
|
|||||||
"windows-link 0.2.1",
|
"windows-link 0.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-sys"
|
|
||||||
version = "0.48.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
|
||||||
dependencies = [
|
|
||||||
"windows-targets 0.48.5",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.52.0"
|
version = "0.52.0"
|
||||||
|
|||||||
+4
-2
@@ -19,7 +19,7 @@ bytes = "1.4.0"
|
|||||||
clap = { version = "4.5.40", features = ["cargo", "derive", "wrap_help"] }
|
clap = { version = "4.5.40", features = ["cargo", "derive", "wrap_help"] }
|
||||||
dirs = "6.0.0"
|
dirs = "6.0.0"
|
||||||
futures-util = "0.3.29"
|
futures-util = "0.3.29"
|
||||||
inquire = "0.7.0"
|
inquire = "0.9.4"
|
||||||
is-terminal = "0.4.9"
|
is-terminal = "0.4.9"
|
||||||
reedline = "0.40.0"
|
reedline = "0.40.0"
|
||||||
serde = { version = "1.0.152", features = ["derive"] }
|
serde = { version = "1.0.152", features = ["derive"] }
|
||||||
@@ -96,6 +96,9 @@ colored = "3.0.0"
|
|||||||
clap_complete = { version = "4.5.58", features = ["unstable-dynamic"] }
|
clap_complete = { version = "4.5.58", features = ["unstable-dynamic"] }
|
||||||
gman = "0.3.0"
|
gman = "0.3.0"
|
||||||
clap_complete_nushell = "4.5.9"
|
clap_complete_nushell = "4.5.9"
|
||||||
|
open = "5"
|
||||||
|
rand = "0.9.0"
|
||||||
|
url = "2.5.8"
|
||||||
|
|
||||||
[dependencies.reqwest]
|
[dependencies.reqwest]
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
@@ -126,7 +129,6 @@ arboard = { version = "3.3.0", default-features = false }
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pretty_assertions = "1.4.0"
|
pretty_assertions = "1.4.0"
|
||||||
rand = "0.9.0"
|
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "loki"
|
name = "loki"
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ Coming from [AIChat](https://github.com/sigoden/aichat)? Follow the [migration g
|
|||||||
* [Todo System](./docs/TODO-SYSTEM.md): Built-in task tracking for improved agent reliability with smaller models.
|
* [Todo System](./docs/TODO-SYSTEM.md): Built-in task tracking for improved agent reliability with smaller models.
|
||||||
* [Environment Variables](./docs/ENVIRONMENT-VARIABLES.md): Override and customize your Loki configuration at runtime with environment variables.
|
* [Environment Variables](./docs/ENVIRONMENT-VARIABLES.md): Override and customize your Loki configuration at runtime with environment variables.
|
||||||
* [Client Configurations](./docs/clients/CLIENTS.md): Configuration instructions for various LLM providers.
|
* [Client Configurations](./docs/clients/CLIENTS.md): Configuration instructions for various LLM providers.
|
||||||
|
* [Authentication (API Key & OAuth)](./docs/clients/CLIENTS.md#authentication): Authenticate with API keys or OAuth for subscription-based access.
|
||||||
* [Patching API Requests](./docs/clients/PATCHES.md): Learn how to patch API requests for advanced customization.
|
* [Patching API Requests](./docs/clients/PATCHES.md): Learn how to patch API requests for advanced customization.
|
||||||
* [Custom Themes](./docs/THEMES.md): Change the look and feel of Loki to your preferences with custom themes.
|
* [Custom Themes](./docs/THEMES.md): Change the look and feel of Loki to your preferences with custom themes.
|
||||||
* [History](#history): A history of how Loki came to be.
|
* [History](#history): A history of how Loki came to be.
|
||||||
@@ -150,6 +151,26 @@ guide you through the process when you first attempt to access the vault. So, to
|
|||||||
loki --list-secrets
|
loki --list-secrets
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
Each client in your configuration needs authentication (with a few exceptions; e.g. ollama). Most clients use an API key
|
||||||
|
(set via `api_key` in the config or through the [vault](./docs/VAULT.md)). For providers that support OAuth (e.g. Claude Pro/Max
|
||||||
|
subscribers, Google Gemini), you can authenticate with your existing subscription instead:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# In your config.yaml
|
||||||
|
clients:
|
||||||
|
- type: claude
|
||||||
|
name: my-claude-oauth
|
||||||
|
auth: oauth # Indicate you want to authenticate with OAuth instead of an API key
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
loki --authenticate my-claude-oauth
|
||||||
|
# Or via the REPL: .authenticate
|
||||||
|
```
|
||||||
|
|
||||||
|
For full details, see the [authentication documentation](./docs/clients/CLIENTS.md#authentication).
|
||||||
|
|
||||||
### Tab-Completions
|
### Tab-Completions
|
||||||
You can also enable tab completions to make using Loki easier. To do so, add the following to your shell profile:
|
You can also enable tab completions to make using Loki easier. To do so, add the following to your shell profile:
|
||||||
```shell
|
```shell
|
||||||
|
|||||||
@@ -2,68 +2,6 @@
|
|||||||
# Shared Agent Utilities - Minimal, focused helper functions
|
# Shared Agent Utilities - Minimal, focused helper functions
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
#############################
|
|
||||||
## CONTEXT FILE MANAGEMENT ##
|
|
||||||
#############################
|
|
||||||
|
|
||||||
get_context_file() {
|
|
||||||
local project_dir="${LLM_AGENT_VAR_PROJECT_DIR:-.}"
|
|
||||||
echo "${project_dir}/.loki-context"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Initialize context file for a new task
|
|
||||||
# Usage: init_context "Task description"
|
|
||||||
init_context() {
|
|
||||||
local task="$1"
|
|
||||||
local project_dir="${LLM_AGENT_VAR_PROJECT_DIR:-.}"
|
|
||||||
local context_file
|
|
||||||
context_file=$(get_context_file)
|
|
||||||
|
|
||||||
cat > "${context_file}" <<EOF
|
|
||||||
## Project: ${project_dir}
|
|
||||||
## Task: ${task}
|
|
||||||
## Started: $(date -Iseconds)
|
|
||||||
|
|
||||||
### Prior Findings
|
|
||||||
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
# Append findings to the context file
|
|
||||||
# Usage: append_context "agent_name" "finding summary
|
|
||||||
append_context() {
|
|
||||||
local agent="$1"
|
|
||||||
local finding="$2"
|
|
||||||
local context_file
|
|
||||||
context_file=$(get_context_file)
|
|
||||||
|
|
||||||
if [[ -f "${context_file}" ]]; then
|
|
||||||
{
|
|
||||||
echo ""
|
|
||||||
echo "[${agent}]:"
|
|
||||||
echo "${finding}"
|
|
||||||
} >> "${context_file}"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Read the current context (returns empty string if no context)
|
|
||||||
# Usage: context=$(read_context)
|
|
||||||
read_context() {
|
|
||||||
local context_file
|
|
||||||
context_file=$(get_context_file)
|
|
||||||
|
|
||||||
if [[ -f "${context_file}" ]]; then
|
|
||||||
cat "${context_file}"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Clear the context file
|
|
||||||
clear_context() {
|
|
||||||
local context_file
|
|
||||||
context_file=$(get_context_file)
|
|
||||||
rm -f "${context_file}"
|
|
||||||
}
|
|
||||||
|
|
||||||
#######################
|
#######################
|
||||||
## PROJECT DETECTION ##
|
## PROJECT DETECTION ##
|
||||||
#######################
|
#######################
|
||||||
@@ -348,77 +286,11 @@ detect_project() {
|
|||||||
echo '{"type":"unknown","build":"","test":"","check":""}'
|
echo '{"type":"unknown","build":"","test":"","check":""}'
|
||||||
}
|
}
|
||||||
|
|
||||||
######################
|
|
||||||
## AGENT INVOCATION ##
|
|
||||||
######################
|
|
||||||
|
|
||||||
# Invoke a subagent with optional context injection
|
|
||||||
# Usage: invoke_agent <agent_name> <prompt> [extra_args...]
|
|
||||||
invoke_agent() {
|
|
||||||
local agent="$1"
|
|
||||||
local prompt="$2"
|
|
||||||
shift 2
|
|
||||||
|
|
||||||
local context
|
|
||||||
context=$(read_context)
|
|
||||||
|
|
||||||
local full_prompt
|
|
||||||
if [[ -n "${context}" ]]; then
|
|
||||||
full_prompt="## Orchestrator Context
|
|
||||||
|
|
||||||
The orchestrator (sisyphus) has gathered this context from prior work:
|
|
||||||
|
|
||||||
<context>
|
|
||||||
${context}
|
|
||||||
</context>
|
|
||||||
|
|
||||||
## Your Task
|
|
||||||
|
|
||||||
${prompt}"
|
|
||||||
else
|
|
||||||
full_prompt="${prompt}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
env AUTO_CONFIRM=true loki --agent "${agent}" "$@" "${full_prompt}" 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Invoke a subagent and capture a summary of its findings
|
|
||||||
# Usage: result=$(invoke_agent_with_summary "explore" "find auth patterns")
|
|
||||||
invoke_agent_with_summary() {
|
|
||||||
local agent="$1"
|
|
||||||
local prompt="$2"
|
|
||||||
shift 2
|
|
||||||
|
|
||||||
local output
|
|
||||||
output=$(invoke_agent "${agent}" "${prompt}" "$@")
|
|
||||||
|
|
||||||
local summary=""
|
|
||||||
|
|
||||||
if echo "${output}" | grep -q "FINDINGS:"; then
|
|
||||||
summary=$(echo "${output}" | sed -n '/FINDINGS:/,/^[A-Z_]*COMPLETE/p' | grep "^- " | sed 's/^- / - /')
|
|
||||||
elif echo "${output}" | grep -q "CODER_COMPLETE:"; then
|
|
||||||
summary=$(echo "${output}" | grep "CODER_COMPLETE:" | sed 's/CODER_COMPLETE: *//')
|
|
||||||
elif echo "${output}" | grep -q "ORACLE_COMPLETE"; then
|
|
||||||
summary=$(echo "${output}" | sed -n '/^## Recommendation/,/^## /{/^## Recommendation/d;/^## /d;p}' | sed '/^$/d' | head -10)
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Failsafe: extract up to 5 meaningful lines if no markers found
|
|
||||||
if [[ -z "${summary}" ]]; then
|
|
||||||
summary=$(echo "${output}" | grep -v "^$" | grep -v "^#" | grep -v "^\-\-\-" | tail -10 | head -5)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "${summary}" ]]; then
|
|
||||||
append_context "${agent}" "${summary}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "${output}"
|
|
||||||
}
|
|
||||||
|
|
||||||
###########################
|
###########################
|
||||||
## FILE SEARCH UTILITIES ##
|
## FILE SEARCH UTILITIES ##
|
||||||
###########################
|
###########################
|
||||||
|
|
||||||
search_files() {
|
_search_files() {
|
||||||
local pattern="$1"
|
local pattern="$1"
|
||||||
local dir="${2:-.}"
|
local dir="${2:-.}"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Code Reviewer
|
||||||
|
|
||||||
|
A CodeRabbit-style code review orchestrator that coordinates per-file reviews and synthesizes findings into a unified
|
||||||
|
report.
|
||||||
|
|
||||||
|
This agent acts as the manager for the review process, delegating actual file analysis to **[File Reviewer](../file-reviewer/README.md)**
|
||||||
|
agents while handling coordination and final reporting.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🤖 **Orchestration**: Spawns parallel reviewers for each changed file.
|
||||||
|
- 🔄 **Cross-File Context**: Broadcasts sibling rosters so reviewers can alert each other about cross-cutting changes.
|
||||||
|
- 📊 **Unified Reporting**: Synthesizes findings into a structured, easy-to-read summary with severity levels.
|
||||||
|
- ⚡ **Parallel Execution**: Runs reviews concurrently for maximum speed.
|
||||||
|
|
||||||
|
## Pro-Tip: Use an IDE MCP Server for Improved Performance
|
||||||
|
Many modern IDEs now include MCP servers that let LLMs perform operations within the IDE itself and use IDE tools. Using
|
||||||
|
an IDE's MCP server dramatically improves the performance of coding agents. So if you have an IDE, try adding that MCP
|
||||||
|
server to your config (see the [MCP Server docs](../../../docs/function-calling/MCP-SERVERS.md) to see how to configure
|
||||||
|
them), and modify the agent definition to look like this:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ...
|
||||||
|
|
||||||
|
mcp_servers:
|
||||||
|
- jetbrains # The name of your configured IDE MCP server
|
||||||
|
|
||||||
|
global_tools:
|
||||||
|
- fs_read.sh
|
||||||
|
- fs_grep.sh
|
||||||
|
- fs_glob.sh
|
||||||
|
# - execute_command.sh
|
||||||
|
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
@@ -2,7 +2,6 @@ name: code-reviewer
|
|||||||
description: CodeRabbit-style code reviewer - spawns per-file reviewers, synthesizes findings
|
description: CodeRabbit-style code reviewer - spawns per-file reviewers, synthesizes findings
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
temperature: 0.1
|
temperature: 0.1
|
||||||
top_p: 0.95
|
|
||||||
|
|
||||||
auto_continue: true
|
auto_continue: true
|
||||||
max_auto_continues: 20
|
max_auto_continues: 20
|
||||||
@@ -123,3 +122,6 @@ instructions: |
|
|||||||
- Project: {{project_dir}}
|
- Project: {{project_dir}}
|
||||||
- CWD: {{__cwd__}}
|
- CWD: {{__cwd__}}
|
||||||
- Shell: {{__shell__}}
|
- Shell: {{__shell__}}
|
||||||
|
|
||||||
|
## Available Tools:
|
||||||
|
{{__tools__}}
|
||||||
|
|||||||
@@ -14,3 +14,27 @@ acts as the coordinator/architect, while Coder handles the implementation detail
|
|||||||
- 📊 Precise diff-based file editing for controlled code modifications
|
- 📊 Precise diff-based file editing for controlled code modifications
|
||||||
|
|
||||||
It can also be used as a standalone tool for direct coding assistance.
|
It can also be used as a standalone tool for direct coding assistance.
|
||||||
|
|
||||||
|
## Pro-Tip: Use an IDE MCP Server for Improved Performance
|
||||||
|
Many modern IDEs now include MCP servers that let LLMs perform operations within the IDE itself and use IDE tools. Using
|
||||||
|
an IDE's MCP server dramatically improves the performance of coding agents. So if you have an IDE, try adding that MCP
|
||||||
|
server to your config (see the [MCP Server docs](../../../docs/function-calling/MCP-SERVERS.md) to see how to configure
|
||||||
|
them), and modify the agent definition to look like this:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ...
|
||||||
|
|
||||||
|
mcp_servers:
|
||||||
|
- jetbrains # The name of your configured IDE MCP server
|
||||||
|
|
||||||
|
global_tools:
|
||||||
|
# Keep useful read-only tools for reading files in other non-project directories
|
||||||
|
- fs_read.sh
|
||||||
|
- fs_grep.sh
|
||||||
|
- fs_glob.sh
|
||||||
|
# - fs_write.sh
|
||||||
|
# - fs_patch.sh
|
||||||
|
- execute_command.sh
|
||||||
|
|
||||||
|
# ...
|
||||||
|
```
|
||||||
@@ -2,7 +2,6 @@ name: coder
|
|||||||
description: Implementation agent - writes code, follows patterns, verifies with builds
|
description: Implementation agent - writes code, follows patterns, verifies with builds
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
temperature: 0.1
|
temperature: 0.1
|
||||||
top_p: 0.95
|
|
||||||
|
|
||||||
auto_continue: true
|
auto_continue: true
|
||||||
max_auto_continues: 15
|
max_auto_continues: 15
|
||||||
@@ -30,11 +29,30 @@ instructions: |
|
|||||||
## Your Mission
|
## Your Mission
|
||||||
|
|
||||||
Given an implementation task:
|
Given an implementation task:
|
||||||
1. Understand what to build (from context provided)
|
1. Check for orchestrator context first (see below)
|
||||||
2. Study existing patterns (read 1-2 similar files)
|
2. Fill gaps only. Read files NOT already covered in context
|
||||||
3. Write the code (using tools, NOT chat output)
|
3. Write the code (using tools, NOT chat output)
|
||||||
4. Verify it compiles/builds
|
4. Verify it compiles/builds
|
||||||
5. Signal completion
|
5. Signal completion with a summary
|
||||||
|
|
||||||
|
## Using Orchestrator Context (IMPORTANT)
|
||||||
|
|
||||||
|
When spawned by sisyphus, your prompt will often contain a `<context>` block
|
||||||
|
with prior findings: file paths, code patterns, and conventions discovered by
|
||||||
|
explore agents.
|
||||||
|
|
||||||
|
**If context is provided:**
|
||||||
|
1. Use it as your primary reference. Don't re-read files already summarized
|
||||||
|
2. Follow the code patterns shown. Snippets in context ARE the style guide
|
||||||
|
3. Read the referenced files ONLY IF you need more detail (e.g. full function
|
||||||
|
signature, import list, or adjacent code not included in the snippet)
|
||||||
|
4. If context includes a "Conventions" section, follow it exactly
|
||||||
|
|
||||||
|
**If context is NOT provided or is too vague to act on:**
|
||||||
|
Fall back to self-exploration: grep for similar files, read 1-2 examples,
|
||||||
|
match their style.
|
||||||
|
|
||||||
|
**Never ignore provided context.** It represents work already done upstream.
|
||||||
|
|
||||||
## Todo System
|
## Todo System
|
||||||
|
|
||||||
@@ -83,12 +101,13 @@ instructions: |
|
|||||||
|
|
||||||
## Completion Signal
|
## Completion Signal
|
||||||
|
|
||||||
End with:
|
When done, end your response with a summary so the parent agent knows what happened:
|
||||||
|
|
||||||
```
|
```
|
||||||
CODER_COMPLETE: [summary of what was implemented]
|
CODER_COMPLETE: [summary of what was implemented, which files were created/modified, and build status]
|
||||||
```
|
```
|
||||||
|
|
||||||
Or if failed:
|
Or if something went wrong:
|
||||||
```
|
```
|
||||||
CODER_FAILED: [what went wrong]
|
CODER_FAILED: [what went wrong]
|
||||||
```
|
```
|
||||||
@@ -106,3 +125,5 @@ instructions: |
|
|||||||
- CWD: {{__cwd__}}
|
- CWD: {{__cwd__}}
|
||||||
- Shell: {{__shell__}}
|
- Shell: {{__shell__}}
|
||||||
|
|
||||||
|
## Available tools:
|
||||||
|
{{__tools__}}
|
||||||
@@ -14,11 +14,28 @@ _project_dir() {
|
|||||||
(cd "${dir}" 2>/dev/null && pwd) || echo "${dir}"
|
(cd "${dir}" 2>/dev/null && pwd) || echo "${dir}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Normalize a path to be relative to project root.
|
||||||
|
# Strips the project_dir prefix if the LLM passes an absolute path.
|
||||||
|
# Usage: local rel_path; rel_path=$(_normalize_path "/abs/or/rel/path")
|
||||||
|
_normalize_path() {
|
||||||
|
local input_path="$1"
|
||||||
|
local project_dir
|
||||||
|
project_dir=$(_project_dir)
|
||||||
|
|
||||||
|
if [[ "${input_path}" == /* ]]; then
|
||||||
|
input_path="${input_path#"${project_dir}"/}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
input_path="${input_path#./}"
|
||||||
|
echo "${input_path}"
|
||||||
|
}
|
||||||
|
|
||||||
# @cmd Read a file's contents before modifying
|
# @cmd Read a file's contents before modifying
|
||||||
# @option --path! Path to the file (relative to project root)
|
# @option --path! Path to the file (relative to project root)
|
||||||
read_file() {
|
read_file() {
|
||||||
|
local file_path
|
||||||
# shellcheck disable=SC2154
|
# shellcheck disable=SC2154
|
||||||
local file_path="${argc_path}"
|
file_path=$(_normalize_path "${argc_path}")
|
||||||
local project_dir
|
local project_dir
|
||||||
project_dir=$(_project_dir)
|
project_dir=$(_project_dir)
|
||||||
local full_path="${project_dir}/${file_path}"
|
local full_path="${project_dir}/${file_path}"
|
||||||
@@ -39,7 +56,8 @@ read_file() {
|
|||||||
# @option --path! Path for the file (relative to project root)
|
# @option --path! Path for the file (relative to project root)
|
||||||
# @option --content! Complete file contents to write
|
# @option --content! Complete file contents to write
|
||||||
write_file() {
|
write_file() {
|
||||||
local file_path="${argc_path}"
|
local file_path
|
||||||
|
file_path=$(_normalize_path "${argc_path}")
|
||||||
# shellcheck disable=SC2154
|
# shellcheck disable=SC2154
|
||||||
local content="${argc_content}"
|
local content="${argc_content}"
|
||||||
local project_dir
|
local project_dir
|
||||||
@@ -47,7 +65,7 @@ write_file() {
|
|||||||
local full_path="${project_dir}/${file_path}"
|
local full_path="${project_dir}/${file_path}"
|
||||||
|
|
||||||
mkdir -p "$(dirname "${full_path}")"
|
mkdir -p "$(dirname "${full_path}")"
|
||||||
echo "${content}" > "${full_path}"
|
printf '%s' "${content}" > "${full_path}"
|
||||||
|
|
||||||
green "Wrote: ${file_path}" >> "$LLM_OUTPUT"
|
green "Wrote: ${file_path}" >> "$LLM_OUTPUT"
|
||||||
}
|
}
|
||||||
@@ -55,7 +73,8 @@ write_file() {
|
|||||||
# @cmd Find files similar to a given path (for pattern matching)
|
# @cmd Find files similar to a given path (for pattern matching)
|
||||||
# @option --path! Path to find similar files for
|
# @option --path! Path to find similar files for
|
||||||
find_similar_files() {
|
find_similar_files() {
|
||||||
local file_path="${argc_path}"
|
local file_path
|
||||||
|
file_path=$(_normalize_path "${argc_path}")
|
||||||
local project_dir
|
local project_dir
|
||||||
project_dir=$(_project_dir)
|
project_dir=$(_project_dir)
|
||||||
|
|
||||||
@@ -71,14 +90,14 @@ find_similar_files() {
|
|||||||
! -name "$(basename "${file_path}")" \
|
! -name "$(basename "${file_path}")" \
|
||||||
! -name "*test*" \
|
! -name "*test*" \
|
||||||
! -name "*spec*" \
|
! -name "*spec*" \
|
||||||
2>/dev/null | head -3)
|
2>/dev/null | sed "s|^${project_dir}/||" | head -3)
|
||||||
|
|
||||||
if [[ -z "${results}" ]]; then
|
if [[ -z "${results}" ]]; then
|
||||||
results=$(find "${project_dir}/src" -type f -name "*.${ext}" \
|
results=$(find "${project_dir}/src" -type f -name "*.${ext}" \
|
||||||
! -name "*test*" \
|
! -name "*test*" \
|
||||||
! -name "*spec*" \
|
! -name "*spec*" \
|
||||||
-not -path '*/target/*' \
|
-not -path '*/target/*' \
|
||||||
2>/dev/null | head -3)
|
2>/dev/null | sed "s|^${project_dir}/||" | head -3)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n "${results}" ]]; then
|
if [[ -n "${results}" ]]; then
|
||||||
@@ -186,6 +205,7 @@ search_code() {
|
|||||||
grep -v '/target/' | \
|
grep -v '/target/' | \
|
||||||
grep -v '/node_modules/' | \
|
grep -v '/node_modules/' | \
|
||||||
grep -v '/.git/' | \
|
grep -v '/.git/' | \
|
||||||
|
sed "s|^${project_dir}/||" | \
|
||||||
head -20) || true
|
head -20) || true
|
||||||
|
|
||||||
if [[ -n "${results}" ]]; then
|
if [[ -n "${results}" ]]; then
|
||||||
|
|||||||
@@ -13,3 +13,25 @@ It can also be used as a standalone tool for understanding codebases and finding
|
|||||||
- 📂 File system navigation and content analysis
|
- 📂 File system navigation and content analysis
|
||||||
- 🧠 Context gathering for complex tasks
|
- 🧠 Context gathering for complex tasks
|
||||||
- 🛡️ Read-only operations for safe investigation
|
- 🛡️ Read-only operations for safe investigation
|
||||||
|
|
||||||
|
## Pro-Tip: Use an IDE MCP Server for Improved Performance
|
||||||
|
Many modern IDEs now include MCP servers that let LLMs perform operations within the IDE itself and use IDE tools. Using
|
||||||
|
an IDE's MCP server dramatically improves the performance of coding agents. So if you have an IDE, try adding that MCP
|
||||||
|
server to your config (see the [MCP Server docs](../../../docs/function-calling/MCP-SERVERS.md) to see how to configure
|
||||||
|
them), and modify the agent definition to look like this:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ...
|
||||||
|
|
||||||
|
mcp_servers:
|
||||||
|
- jetbrains # The name of your configured IDE MCP server
|
||||||
|
|
||||||
|
global_tools:
|
||||||
|
- fs_read.sh
|
||||||
|
- fs_grep.sh
|
||||||
|
- fs_glob.sh
|
||||||
|
- fs_ls.sh
|
||||||
|
- web_search_loki.sh
|
||||||
|
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|||||||
@@ -2,19 +2,19 @@ name: explore
|
|||||||
description: Fast codebase exploration agent - finds patterns, structures, and relevant files
|
description: Fast codebase exploration agent - finds patterns, structures, and relevant files
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
temperature: 0.1
|
temperature: 0.1
|
||||||
top_p: 0.95
|
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
- name: project_dir
|
- name: project_dir
|
||||||
description: Project directory to explore
|
description: Project directory to explore
|
||||||
default: '.'
|
default: '.'
|
||||||
|
|
||||||
|
mcp_servers:
|
||||||
|
- ddg-search
|
||||||
global_tools:
|
global_tools:
|
||||||
- fs_read.sh
|
- fs_read.sh
|
||||||
- fs_grep.sh
|
- fs_grep.sh
|
||||||
- fs_glob.sh
|
- fs_glob.sh
|
||||||
- fs_ls.sh
|
- fs_ls.sh
|
||||||
- web_search_loki.sh
|
|
||||||
|
|
||||||
instructions: |
|
instructions: |
|
||||||
You are a codebase explorer. Your job: Search, find, report. Nothing else.
|
You are a codebase explorer. Your job: Search, find, report. Nothing else.
|
||||||
@@ -69,6 +69,9 @@ instructions: |
|
|||||||
- Project: {{project_dir}}
|
- Project: {{project_dir}}
|
||||||
- CWD: {{__cwd__}}
|
- CWD: {{__cwd__}}
|
||||||
|
|
||||||
|
## Available Tools:
|
||||||
|
{{__tools__}}
|
||||||
|
|
||||||
conversation_starters:
|
conversation_starters:
|
||||||
- 'Find how authentication is implemented'
|
- 'Find how authentication is implemented'
|
||||||
- 'What patterns are used for API endpoints'
|
- 'What patterns are used for API endpoints'
|
||||||
|
|||||||
@@ -14,6 +14,21 @@ _project_dir() {
|
|||||||
(cd "${dir}" 2>/dev/null && pwd) || echo "${dir}"
|
(cd "${dir}" 2>/dev/null && pwd) || echo "${dir}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Normalize a path to be relative to project root.
|
||||||
|
# Strips the project_dir prefix if the LLM passes an absolute path.
|
||||||
|
_normalize_path() {
|
||||||
|
local input_path="$1"
|
||||||
|
local project_dir
|
||||||
|
project_dir=$(_project_dir)
|
||||||
|
|
||||||
|
if [[ "${input_path}" == /* ]]; then
|
||||||
|
input_path="${input_path#"${project_dir}"/}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
input_path="${input_path#./}"
|
||||||
|
echo "${input_path}"
|
||||||
|
}
|
||||||
|
|
||||||
# @cmd Get project structure and layout
|
# @cmd Get project structure and layout
|
||||||
get_structure() {
|
get_structure() {
|
||||||
local project_dir
|
local project_dir
|
||||||
@@ -45,7 +60,7 @@ search_files() {
|
|||||||
echo "" >> "$LLM_OUTPUT"
|
echo "" >> "$LLM_OUTPUT"
|
||||||
|
|
||||||
local results
|
local results
|
||||||
results=$(search_files "${pattern}" "${project_dir}")
|
results=$(_search_files "${pattern}" "${project_dir}")
|
||||||
|
|
||||||
if [[ -n "${results}" ]]; then
|
if [[ -n "${results}" ]]; then
|
||||||
echo "${results}" >> "$LLM_OUTPUT"
|
echo "${results}" >> "$LLM_OUTPUT"
|
||||||
@@ -78,6 +93,7 @@ search_content() {
|
|||||||
grep -v '/node_modules/' | \
|
grep -v '/node_modules/' | \
|
||||||
grep -v '/.git/' | \
|
grep -v '/.git/' | \
|
||||||
grep -v '/dist/' | \
|
grep -v '/dist/' | \
|
||||||
|
sed "s|^${project_dir}/||" | \
|
||||||
head -30) || true
|
head -30) || true
|
||||||
|
|
||||||
if [[ -n "${results}" ]]; then
|
if [[ -n "${results}" ]]; then
|
||||||
@@ -91,8 +107,9 @@ search_content() {
|
|||||||
# @option --path! Path to the file (relative to project root)
|
# @option --path! Path to the file (relative to project root)
|
||||||
# @option --lines Maximum lines to read (default: 200)
|
# @option --lines Maximum lines to read (default: 200)
|
||||||
read_file() {
|
read_file() {
|
||||||
|
local file_path
|
||||||
# shellcheck disable=SC2154
|
# shellcheck disable=SC2154
|
||||||
local file_path="${argc_path}"
|
file_path=$(_normalize_path "${argc_path}")
|
||||||
local max_lines="${argc_lines:-200}"
|
local max_lines="${argc_lines:-200}"
|
||||||
local project_dir
|
local project_dir
|
||||||
project_dir=$(_project_dir)
|
project_dir=$(_project_dir)
|
||||||
@@ -122,7 +139,8 @@ read_file() {
|
|||||||
# @cmd Find similar files to a given file (for pattern matching)
|
# @cmd Find similar files to a given file (for pattern matching)
|
||||||
# @option --path! Path to the reference file
|
# @option --path! Path to the reference file
|
||||||
find_similar() {
|
find_similar() {
|
||||||
local file_path="${argc_path}"
|
local file_path
|
||||||
|
file_path=$(_normalize_path "${argc_path}")
|
||||||
local project_dir
|
local project_dir
|
||||||
project_dir=$(_project_dir)
|
project_dir=$(_project_dir)
|
||||||
|
|
||||||
@@ -138,7 +156,7 @@ find_similar() {
|
|||||||
! -name "$(basename "${file_path}")" \
|
! -name "$(basename "${file_path}")" \
|
||||||
! -name "*test*" \
|
! -name "*test*" \
|
||||||
! -name "*spec*" \
|
! -name "*spec*" \
|
||||||
2>/dev/null | head -5)
|
2>/dev/null | sed "s|^${project_dir}/||" | head -5)
|
||||||
|
|
||||||
if [[ -n "${results}" ]]; then
|
if [[ -n "${results}" ]]; then
|
||||||
echo "${results}" >> "$LLM_OUTPUT"
|
echo "${results}" >> "$LLM_OUTPUT"
|
||||||
@@ -147,7 +165,7 @@ find_similar() {
|
|||||||
! -name "$(basename "${file_path}")" \
|
! -name "$(basename "${file_path}")" \
|
||||||
! -name "*test*" \
|
! -name "*test*" \
|
||||||
-not -path '*/target/*' \
|
-not -path '*/target/*' \
|
||||||
2>/dev/null | head -5)
|
2>/dev/null | sed "s|^${project_dir}/||" | head -5)
|
||||||
if [[ -n "${results}" ]]; then
|
if [[ -n "${results}" ]]; then
|
||||||
echo "${results}" >> "$LLM_OUTPUT"
|
echo "${results}" >> "$LLM_OUTPUT"
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# File Reviewer
|
||||||
|
|
||||||
|
A specialized worker agent that reviews a single file's diff for bugs, style issues, and cross-cutting concerns.
|
||||||
|
|
||||||
|
This agent is designed to be spawned by the **[Code Reviewer](../code-reviewer/README.md)** agent. It focuses deeply on
|
||||||
|
one file while communicating with sibling agents to catch issues that span multiple files.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🔍 **Deep Analysis**: Focuses on bugs, logic errors, security issues, and style problems in a single file.
|
||||||
|
- 🗣️ **Teammate Communication**: Sends and receives alerts to/from sibling reviewers about interface or dependency
|
||||||
|
changes.
|
||||||
|
- 🎯 **Targeted Reading**: Reads only relevant context around changed lines to stay efficient.
|
||||||
|
- 🏷️ **Structured Findings**: Categorizes issues by severity (🔴 Critical, 🟡 Warning, 🟢 Suggestion, 💡 Nitpick).
|
||||||
|
|
||||||
|
## Pro-Tip: Use an IDE MCP Server for Improved Performance
|
||||||
|
Many modern IDEs now include MCP servers that let LLMs perform operations within the IDE itself and use IDE tools. Using
|
||||||
|
an IDE's MCP server dramatically improves the performance of coding agents. So if you have an IDE, try adding that MCP
|
||||||
|
server to your config (see the [MCP Server docs](../../../docs/function-calling/MCP-SERVERS.md) to see how to configure
|
||||||
|
them), and modify the agent definition to look like this:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ...
|
||||||
|
|
||||||
|
mcp_servers:
|
||||||
|
- jetbrains # The name of your configured IDE MCP server
|
||||||
|
|
||||||
|
global_tools:
|
||||||
|
- fs_read.sh
|
||||||
|
- fs_grep.sh
|
||||||
|
- fs_glob.sh
|
||||||
|
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
@@ -2,7 +2,6 @@ name: file-reviewer
|
|||||||
description: Reviews a single file's diff for bugs, style issues, and cross-cutting concerns
|
description: Reviews a single file's diff for bugs, style issues, and cross-cutting concerns
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
temperature: 0.1
|
temperature: 0.1
|
||||||
top_p: 0.95
|
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
- name: project_dir
|
- name: project_dir
|
||||||
@@ -109,3 +108,6 @@ instructions: |
|
|||||||
## Context
|
## Context
|
||||||
- Project: {{project_dir}}
|
- Project: {{project_dir}}
|
||||||
- CWD: {{__cwd__}}
|
- CWD: {{__cwd__}}
|
||||||
|
|
||||||
|
## Available Tools:
|
||||||
|
{{__tools__}}
|
||||||
|
|||||||
@@ -15,3 +15,25 @@ It can also be used as a standalone tool for design reviews and solving difficul
|
|||||||
- ⚖️ Tradeoff analysis and technology selection
|
- ⚖️ Tradeoff analysis and technology selection
|
||||||
- 📝 Code review and best practices advice
|
- 📝 Code review and best practices advice
|
||||||
- 🧠 Deep reasoning for ambiguous problems
|
- 🧠 Deep reasoning for ambiguous problems
|
||||||
|
|
||||||
|
## Pro-Tip: Use an IDE MCP Server for Improved Performance
|
||||||
|
Many modern IDEs now include MCP servers that let LLMs perform operations within the IDE itself and use IDE tools. Using
|
||||||
|
an IDE's MCP server dramatically improves the performance of coding agents. So if you have an IDE, try adding that MCP
|
||||||
|
server to your config (see the [MCP Server docs](../../../docs/function-calling/MCP-SERVERS.md) to see how to configure
|
||||||
|
them), and modify the agent definition to look like this:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ...
|
||||||
|
|
||||||
|
mcp_servers:
|
||||||
|
- jetbrains # The name of your configured IDE MCP server
|
||||||
|
|
||||||
|
global_tools:
|
||||||
|
- fs_read.sh
|
||||||
|
- fs_grep.sh
|
||||||
|
- fs_glob.sh
|
||||||
|
- fs_ls.sh
|
||||||
|
- web_search_loki.sh
|
||||||
|
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|||||||
@@ -2,19 +2,19 @@ name: oracle
|
|||||||
description: High-IQ advisor for architecture, debugging, and complex decisions
|
description: High-IQ advisor for architecture, debugging, and complex decisions
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
temperature: 0.2
|
temperature: 0.2
|
||||||
top_p: 0.95
|
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
- name: project_dir
|
- name: project_dir
|
||||||
description: Project directory for context
|
description: Project directory for context
|
||||||
default: '.'
|
default: '.'
|
||||||
|
|
||||||
|
mcp_servers:
|
||||||
|
- ddg-search
|
||||||
global_tools:
|
global_tools:
|
||||||
- fs_read.sh
|
- fs_read.sh
|
||||||
- fs_grep.sh
|
- fs_grep.sh
|
||||||
- fs_glob.sh
|
- fs_glob.sh
|
||||||
- fs_ls.sh
|
- fs_ls.sh
|
||||||
- web_search_loki.sh
|
|
||||||
|
|
||||||
instructions: |
|
instructions: |
|
||||||
You are Oracle - a senior architect and debugger consulted for complex decisions.
|
You are Oracle - a senior architect and debugger consulted for complex decisions.
|
||||||
@@ -76,6 +76,9 @@ instructions: |
|
|||||||
- Project: {{project_dir}}
|
- Project: {{project_dir}}
|
||||||
- CWD: {{__cwd__}}
|
- CWD: {{__cwd__}}
|
||||||
|
|
||||||
|
## Available Tools:
|
||||||
|
{{__tools__}}
|
||||||
|
|
||||||
conversation_starters:
|
conversation_starters:
|
||||||
- 'Review this architecture design'
|
- 'Review this architecture design'
|
||||||
- 'Help debug this complex issue'
|
- 'Help debug this complex issue'
|
||||||
|
|||||||
@@ -14,21 +14,38 @@ _project_dir() {
|
|||||||
(cd "${dir}" 2>/dev/null && pwd) || echo "${dir}"
|
(cd "${dir}" 2>/dev/null && pwd) || echo "${dir}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Normalize a path to be relative to project root.
|
||||||
|
# Strips the project_dir prefix if the LLM passes an absolute path.
|
||||||
|
_normalize_path() {
|
||||||
|
local input_path="$1"
|
||||||
|
local project_dir
|
||||||
|
project_dir=$(_project_dir)
|
||||||
|
|
||||||
|
if [[ "${input_path}" == /* ]]; then
|
||||||
|
input_path="${input_path#"${project_dir}"/}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
input_path="${input_path#./}"
|
||||||
|
echo "${input_path}"
|
||||||
|
}
|
||||||
|
|
||||||
# @cmd Read a file for analysis
|
# @cmd Read a file for analysis
|
||||||
# @option --path! Path to the file (relative to project root)
|
# @option --path! Path to the file (relative to project root)
|
||||||
read_file() {
|
read_file() {
|
||||||
local project_dir
|
local project_dir
|
||||||
project_dir=$(_project_dir)
|
project_dir=$(_project_dir)
|
||||||
|
local file_path
|
||||||
# shellcheck disable=SC2154
|
# shellcheck disable=SC2154
|
||||||
local full_path="${project_dir}/${argc_path}"
|
file_path=$(_normalize_path "${argc_path}")
|
||||||
|
local full_path="${project_dir}/${file_path}"
|
||||||
|
|
||||||
if [[ ! -f "${full_path}" ]]; then
|
if [[ ! -f "${full_path}" ]]; then
|
||||||
error "File not found: ${argc_path}" >> "$LLM_OUTPUT"
|
error "File not found: ${file_path}" >> "$LLM_OUTPUT"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
{
|
{
|
||||||
info "Reading: ${argc_path}"
|
info "Reading: ${file_path}"
|
||||||
echo ""
|
echo ""
|
||||||
cat "${full_path}"
|
cat "${full_path}"
|
||||||
} >> "$LLM_OUTPUT"
|
} >> "$LLM_OUTPUT"
|
||||||
@@ -80,6 +97,7 @@ search_code() {
|
|||||||
grep -v '/target/' | \
|
grep -v '/target/' | \
|
||||||
grep -v '/node_modules/' | \
|
grep -v '/node_modules/' | \
|
||||||
grep -v '/.git/' | \
|
grep -v '/.git/' | \
|
||||||
|
sed "s|^${project_dir}/||" | \
|
||||||
head -30) || true
|
head -30) || true
|
||||||
|
|
||||||
if [[ -n "${results}" ]]; then
|
if [[ -n "${results}" ]]; then
|
||||||
@@ -113,7 +131,8 @@ analyze_with_command() {
|
|||||||
# @cmd List directory contents
|
# @cmd List directory contents
|
||||||
# @option --path Path to list (default: project root)
|
# @option --path Path to list (default: project root)
|
||||||
list_directory() {
|
list_directory() {
|
||||||
local dir_path="${argc_path:-.}"
|
local dir_path
|
||||||
|
dir_path=$(_normalize_path "${argc_path:-.}")
|
||||||
local project_dir
|
local project_dir
|
||||||
project_dir=$(_project_dir)
|
project_dir=$(_project_dir)
|
||||||
local full_path="${project_dir}/${dir_path}"
|
local full_path="${project_dir}/${dir_path}"
|
||||||
|
|||||||
@@ -16,3 +16,26 @@ Sisyphus acts as the primary entry point, capable of handling complex tasks by c
|
|||||||
- 💻 **CLI Coding**: Provides a natural language interface for writing and editing code.
|
- 💻 **CLI Coding**: Provides a natural language interface for writing and editing code.
|
||||||
- 🔄 **Task Management**: Tracks progress and context across complex operations.
|
- 🔄 **Task Management**: Tracks progress and context across complex operations.
|
||||||
- 🛠️ **Tool Integration**: Seamlessly uses system tools for building, testing, and file manipulation.
|
- 🛠️ **Tool Integration**: Seamlessly uses system tools for building, testing, and file manipulation.
|
||||||
|
|
||||||
|
## Pro-Tip: Use an IDE MCP Server for Improved Performance
|
||||||
|
Many modern IDEs now include MCP servers that let LLMs perform operations within the IDE itself and use IDE tools. Using
|
||||||
|
an IDE's MCP server dramatically improves the performance of coding agents. So if you have an IDE, try adding that MCP
|
||||||
|
server to your config (see the [MCP Server docs](../../../docs/function-calling/MCP-SERVERS.md) to see how to configure
|
||||||
|
them), and modify the agent definition to look like this:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ...
|
||||||
|
|
||||||
|
mcp_servers:
|
||||||
|
- jetbrains
|
||||||
|
|
||||||
|
global_tools:
|
||||||
|
- fs_read.sh
|
||||||
|
- fs_grep.sh
|
||||||
|
- fs_glob.sh
|
||||||
|
- fs_ls.sh
|
||||||
|
- web_search_loki.sh
|
||||||
|
- execute_command.sh
|
||||||
|
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ name: sisyphus
|
|||||||
description: OpenCode-style orchestrator - classifies intent, delegates to specialists, tracks progress with todos
|
description: OpenCode-style orchestrator - classifies intent, delegates to specialists, tracks progress with todos
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
temperature: 0.1
|
temperature: 0.1
|
||||||
top_p: 0.95
|
|
||||||
|
|
||||||
agent_session: temp
|
agent_session: temp
|
||||||
auto_continue: true
|
auto_continue: true
|
||||||
@@ -13,7 +12,7 @@ can_spawn_agents: true
|
|||||||
max_concurrent_agents: 4
|
max_concurrent_agents: 4
|
||||||
max_agent_depth: 3
|
max_agent_depth: 3
|
||||||
inject_spawn_instructions: true
|
inject_spawn_instructions: true
|
||||||
summarization_threshold: 4000
|
summarization_threshold: 8000
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
- name: project_dir
|
- name: project_dir
|
||||||
@@ -23,12 +22,13 @@ variables:
|
|||||||
description: Auto-confirm command execution
|
description: Auto-confirm command execution
|
||||||
default: '1'
|
default: '1'
|
||||||
|
|
||||||
|
mcp_servers:
|
||||||
|
- ddg-search
|
||||||
global_tools:
|
global_tools:
|
||||||
- fs_read.sh
|
- fs_read.sh
|
||||||
- fs_grep.sh
|
- fs_grep.sh
|
||||||
- fs_glob.sh
|
- fs_glob.sh
|
||||||
- fs_ls.sh
|
- fs_ls.sh
|
||||||
- web_search_loki.sh
|
|
||||||
- execute_command.sh
|
- execute_command.sh
|
||||||
|
|
||||||
instructions: |
|
instructions: |
|
||||||
@@ -70,6 +70,45 @@ instructions: |
|
|||||||
| coder | Write/edit files, implement features | Creates/modifies files, runs builds |
|
| coder | Write/edit files, implement features | Creates/modifies files, runs builds |
|
||||||
| oracle | Architecture decisions, complex debugging | Advisory, high-quality reasoning |
|
| oracle | Architecture decisions, complex debugging | Advisory, high-quality reasoning |
|
||||||
|
|
||||||
|
## Coder Delegation Format (MANDATORY)
|
||||||
|
|
||||||
|
When spawning the `coder` agent, your prompt MUST include these sections.
|
||||||
|
The coder has NOT seen the codebase. Your prompt IS its entire context.
|
||||||
|
|
||||||
|
### Template:
|
||||||
|
|
||||||
|
```
|
||||||
|
## Goal
|
||||||
|
[1-2 sentences: what to build/modify and where]
|
||||||
|
|
||||||
|
## Reference Files
|
||||||
|
[Files that explore found, with what each demonstrates]
|
||||||
|
- `path/to/file.ext` - what pattern this file shows
|
||||||
|
- `path/to/other.ext` - what convention this file shows
|
||||||
|
|
||||||
|
## Code Patterns to Follow
|
||||||
|
[Paste ACTUAL code snippets from explore results, not descriptions]
|
||||||
|
<code>
|
||||||
|
// From path/to/file.ext - this is the pattern to follow:
|
||||||
|
[actual code explore found, 5-20 lines]
|
||||||
|
</code>
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
[Naming, imports, error handling, file organization]
|
||||||
|
- Convention 1
|
||||||
|
- Convention 2
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
[What NOT to do, scope boundaries]
|
||||||
|
- Do NOT modify X
|
||||||
|
- Only touch files in Y/
|
||||||
|
```
|
||||||
|
|
||||||
|
**CRITICAL**: Include actual code snippets, not just file paths.
|
||||||
|
If explore returned code patterns, paste them into the coder prompt.
|
||||||
|
Vague prompts like "follow existing patterns" waste coder's tokens on
|
||||||
|
re-exploration that you already did.
|
||||||
|
|
||||||
## Workflow Examples
|
## Workflow Examples
|
||||||
|
|
||||||
### Example 1: Implementation task (explore -> coder, parallel exploration)
|
### Example 1: Implementation task (explore -> coder, parallel exploration)
|
||||||
@@ -81,12 +120,12 @@ instructions: |
|
|||||||
2. todo__add --task "Explore existing API patterns"
|
2. todo__add --task "Explore existing API patterns"
|
||||||
3. todo__add --task "Implement profile endpoint"
|
3. todo__add --task "Implement profile endpoint"
|
||||||
4. todo__add --task "Verify with build/test"
|
4. todo__add --task "Verify with build/test"
|
||||||
5. agent__spawn --agent explore --prompt "Find existing API endpoint patterns, route structures, and controller conventions"
|
5. agent__spawn --agent explore --prompt "Find existing API endpoint patterns, route structures, and controller conventions. Include code snippets."
|
||||||
6. agent__spawn --agent explore --prompt "Find existing data models and database query patterns"
|
6. agent__spawn --agent explore --prompt "Find existing data models and database query patterns. Include code snippets."
|
||||||
7. agent__collect --id <id1>
|
7. agent__collect --id <id1>
|
||||||
8. agent__collect --id <id2>
|
8. agent__collect --id <id2>
|
||||||
9. todo__done --id 1
|
9. todo__done --id 1
|
||||||
10. agent__spawn --agent coder --prompt "Create user profiles endpoint following existing patterns. [Include context from explore results]"
|
10. agent__spawn --agent coder --prompt "<structured prompt using Coder Delegation Format above, including code snippets from explore results>"
|
||||||
11. agent__collect --id <coder_id>
|
11. agent__collect --id <coder_id>
|
||||||
12. todo__done --id 2
|
12. todo__done --id 2
|
||||||
13. run_build
|
13. run_build
|
||||||
@@ -135,7 +174,6 @@ instructions: |
|
|||||||
|
|
||||||
## When to Do It Yourself
|
## When to Do It Yourself
|
||||||
|
|
||||||
- Single-file reads/writes
|
|
||||||
- Simple command execution
|
- Simple command execution
|
||||||
- Trivial changes (typos, renames)
|
- Trivial changes (typos, renames)
|
||||||
- Quick file searches
|
- Quick file searches
|
||||||
|
|||||||
@@ -16,11 +16,15 @@
|
|||||||
},
|
},
|
||||||
"atlassian": {
|
"atlassian": {
|
||||||
"command": "npx",
|
"command": "npx",
|
||||||
"args": ["-y", "mcp-remote@0.1.13", "https://mcp.atlassian.com/v1/sse"]
|
"args": ["-y", "mcp-remote@0.1.13", "https://mcp.atlassian.com/v1/mcp"]
|
||||||
},
|
},
|
||||||
"docker": {
|
"docker": {
|
||||||
"command": "uvx",
|
"command": "uvx",
|
||||||
"args": ["mcp-server-docker"]
|
"args": ["mcp-server-docker"]
|
||||||
|
},
|
||||||
|
"ddg-search": {
|
||||||
|
"command": "uvx",
|
||||||
|
"args": ["duckduckgo-mcp-server"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ max_concurrent_agents: 4 # Maximum number of agents that can run simulta
|
|||||||
max_agent_depth: 3 # Maximum nesting depth for sub-agents (prevents runaway spawning)
|
max_agent_depth: 3 # Maximum nesting depth for sub-agents (prevents runaway spawning)
|
||||||
inject_spawn_instructions: true # Inject the default agent spawning instructions into the agent's system prompt
|
inject_spawn_instructions: true # Inject the default agent spawning instructions into the agent's system prompt
|
||||||
summarization_model: null # Model to use for summarizing sub-agent output (e.g. 'openai:gpt-4o-mini'); defaults to current model
|
summarization_model: null # Model to use for summarizing sub-agent output (e.g. 'openai:gpt-4o-mini'); defaults to current model
|
||||||
summarization_threshold: 4000 # Character threshold above which sub-agent output is summarized before returning to parent
|
summarization_threshold: 4000 # Character threshold above which sub-agent output is summarized before returning to parent
|
||||||
escalation_timeout: 300 # Seconds a sub-agent waits for a user interaction response before timing out (default: 5 minutes)
|
escalation_timeout: 300 # Seconds a sub-agent waits for a user interaction response before timing out (default: 5 minutes)
|
||||||
mcp_servers: # Optional list of MCP servers that the agent utilizes
|
mcp_servers: # Optional list of MCP servers that the agent utilizes
|
||||||
- github # Corresponds to the name of an MCP server in the `<loki-config-dir>/functions/mcp.json` file
|
- github # Corresponds to the name of an MCP server in the `<loki-config-dir>/functions/mcp.json` file
|
||||||
|
|||||||
+5
-1
@@ -77,7 +77,7 @@ visible_tools: # Which tools are visible to be compiled (and a
|
|||||||
mcp_server_support: true # Enables or disables MCP servers (globally).
|
mcp_server_support: true # Enables or disables MCP servers (globally).
|
||||||
mapping_mcp_servers: # Alias for an MCP server or set of servers
|
mapping_mcp_servers: # Alias for an MCP server or set of servers
|
||||||
git: github,gitmcp
|
git: github,gitmcp
|
||||||
enabled_mcp_servers: null # Which MCP servers to enable by default (e.g. 'github,slack')
|
enabled_mcp_servers: null # Which MCP servers to enable by default (e.g. 'github,slack,ddg-search')
|
||||||
|
|
||||||
# ---- Session ----
|
# ---- Session ----
|
||||||
# See the [Session documentation](./docs/SESSIONS.md) for more information
|
# See the [Session documentation](./docs/SESSIONS.md) for more information
|
||||||
@@ -192,6 +192,8 @@ clients:
|
|||||||
- type: gemini
|
- type: gemini
|
||||||
api_base: https://generativelanguage.googleapis.com/v1beta
|
api_base: https://generativelanguage.googleapis.com/v1beta
|
||||||
api_key: '{{GEMINI_API_KEY}}' # You can either hard-code or inject secrets from the Loki vault
|
api_key: '{{GEMINI_API_KEY}}' # You can either hard-code or inject secrets from the Loki vault
|
||||||
|
auth: null # When set to 'oauth', Loki will use OAuth instead of an API key
|
||||||
|
# Authenticate with `loki --authenticate` or `.authenticate` in the REPL
|
||||||
patch:
|
patch:
|
||||||
chat_completions:
|
chat_completions:
|
||||||
'.*':
|
'.*':
|
||||||
@@ -210,6 +212,8 @@ clients:
|
|||||||
- type: claude
|
- type: claude
|
||||||
api_base: https://api.anthropic.com/v1 # Optional
|
api_base: https://api.anthropic.com/v1 # Optional
|
||||||
api_key: '{{ANTHROPIC_API_KEY}}' # You can either hard-code or inject secrets from the Loki vault
|
api_key: '{{ANTHROPIC_API_KEY}}' # You can either hard-code or inject secrets from the Loki vault
|
||||||
|
auth: null # When set to 'oauth', Loki will use OAuth instead of an API key
|
||||||
|
# Authenticate with `loki --authenticate` or `.authenticate` in the REPL
|
||||||
|
|
||||||
# See https://docs.mistral.ai/
|
# See https://docs.mistral.ai/
|
||||||
- type: openai-compatible
|
- type: openai-compatible
|
||||||
|
|||||||
+3
-1
@@ -467,11 +467,12 @@ inject_todo_instructions: true # Include the default todo instructions into pr
|
|||||||
|
|
||||||
### How It Works
|
### How It Works
|
||||||
|
|
||||||
1. When `inject_todo_instructions` is enabled, agents receive instructions on using four built-in tools:
|
1. When `inject_todo_instructions` is enabled, agents receive instructions on using five built-in tools:
|
||||||
- `todo__init`: Initialize a todo list with a goal
|
- `todo__init`: Initialize a todo list with a goal
|
||||||
- `todo__add`: Add a task to the list
|
- `todo__add`: Add a task to the list
|
||||||
- `todo__done`: Mark a task complete
|
- `todo__done`: Mark a task complete
|
||||||
- `todo__list`: View current todo state
|
- `todo__list`: View current todo state
|
||||||
|
- `todo__clear`: Clear the entire todo list and reset the goal
|
||||||
|
|
||||||
These instructions are a reasonable default that detail how to use Loki's To-Do System. If you wish,
|
These instructions are a reasonable default that detail how to use Loki's To-Do System. If you wish,
|
||||||
you can disable the injection of the default instructions and specify your own instructions for how
|
you can disable the injection of the default instructions and specify your own instructions for how
|
||||||
@@ -714,6 +715,7 @@ Loki comes packaged with some useful built-in agents:
|
|||||||
* `code-reviewer`: A [CodeRabbit](https://coderabbit.ai)-style code reviewer that spawns per-file reviewers using the teammate messaging pattern
|
* `code-reviewer`: A [CodeRabbit](https://coderabbit.ai)-style code reviewer that spawns per-file reviewers using the teammate messaging pattern
|
||||||
* `demo`: An example agent to use for reference when learning to create your own agents
|
* `demo`: An example agent to use for reference when learning to create your own agents
|
||||||
* `explore`: An agent designed to help you explore and understand your codebase
|
* `explore`: An agent designed to help you explore and understand your codebase
|
||||||
|
* `file-reviewer`: An agent designed to perform code-review on a single file (used by the `code-reviewer` agent)
|
||||||
* `jira-helper`: An agent that assists you with all your Jira-related tasks
|
* `jira-helper`: An agent that assists you with all your Jira-related tasks
|
||||||
* `oracle`: An agent for high-level architecture, design decisions, and complex debugging
|
* `oracle`: An agent for high-level architecture, design decisions, and complex debugging
|
||||||
* `sisyphus`: A powerhouse orchestrator agent for writing complex code and acting as a natural language interface for your codebase (similar to ClaudeCode, Gemini CLI, Codex, or OpenCode). Uses sub-agent spawning to delegate to `explore`, `coder`, and `oracle`.
|
* `sisyphus`: A powerhouse orchestrator agent for writing complex code and acting as a natural language interface for your codebase (similar to ClaudeCode, Gemini CLI, Codex, or OpenCode). Uses sub-agent spawning to delegate to `explore`, `coder`, and `oracle`.
|
||||||
|
|||||||
+14
-7
@@ -23,6 +23,7 @@ You can enter the REPL by simply typing `loki` without any follow-up flags or ar
|
|||||||
- [`.edit` - Modify configuration files](#edit---modify-configuration-files)
|
- [`.edit` - Modify configuration files](#edit---modify-configuration-files)
|
||||||
- [`.delete` - Delete configurations from Loki](#delete---delete-configurations-from-loki)
|
- [`.delete` - Delete configurations from Loki](#delete---delete-configurations-from-loki)
|
||||||
- [`.info` - Display information about the current mode](#info---display-information-about-the-current-mode)
|
- [`.info` - Display information about the current mode](#info---display-information-about-the-current-mode)
|
||||||
|
- [`.authenticate` - Authenticate the current model client via OAuth](#authenticate---authenticate-the-current-model-client-via-oauth)
|
||||||
- [`.exit` - Exit an agent/role/session/rag or the Loki REPL itself](#exit---exit-an-agentrolesessionrag-or-the-loki-repl-itself)
|
- [`.exit` - Exit an agent/role/session/rag or the Loki REPL itself](#exit---exit-an-agentrolesessionrag-or-the-loki-repl-itself)
|
||||||
- [`.help` - Show the help guide](#help---show-the-help-guide)
|
- [`.help` - Show the help guide](#help---show-the-help-guide)
|
||||||
<!--toc:end-->
|
<!--toc:end-->
|
||||||
@@ -119,13 +120,14 @@ For more information on sessions and how to use them in Loki, refer to the [sess
|
|||||||
Loki lets you build OpenAI GPT-style agents. The following commands let you interact with and manage your agents in
|
Loki lets you build OpenAI GPT-style agents. The following commands let you interact with and manage your agents in
|
||||||
Loki:
|
Loki:
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|----------------------|------------------------------------------------------------|
|
|----------------------|-----------------------------------------------------------------------------------------------|
|
||||||
| `.agent` | Use an agent |
|
| `.agent` | Use an agent |
|
||||||
| `.starter` | Display and use conversation starters for the active agent |
|
| `.starter` | Display and use conversation starters for the active agent |
|
||||||
| `.edit agent-config` | Open the agent configuration in your preferred text editor |
|
| `.clear todo` | Clear the todo list and stop auto-continuation (requires `auto_continue: true` on the agent) |
|
||||||
| `.info agent` | Display information about the active agent |
|
| `.edit agent-config` | Open the agent configuration in your preferred text editor |
|
||||||
| `.exit agent` | Leave the active agent |
|
| `.info agent` | Display information about the active agent |
|
||||||
|
| `.exit agent` | Leave the active agent |
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -237,6 +239,11 @@ The following entities are supported:
|
|||||||
| `.info agent` | Display information about the active agent |
|
| `.info agent` | Display information about the active agent |
|
||||||
| `.info rag` | Display information about the active RAG |
|
| `.info rag` | Display information about the active RAG |
|
||||||
|
|
||||||
|
### `.authenticate` - Authenticate the current model client via OAuth
|
||||||
|
The `.authenticate` command will start the OAuth flow for the current model client if
|
||||||
|
* The client supports OAuth (See the [clients documentation](./clients/CLIENTS.md#providers-that-support-oauth) for supported clients)
|
||||||
|
* The client is configured in your Loki configuration to use OAuth via the `auth: oauth` property
|
||||||
|
|
||||||
### `.exit` - Exit an agent/role/session/rag or the Loki REPL itself
|
### `.exit` - Exit an agent/role/session/rag or the Loki REPL itself
|
||||||
The `.exit` command is used to move between modes in the Loki REPL.
|
The `.exit` command is used to move between modes in the Loki REPL.
|
||||||
|
|
||||||
|
|||||||
@@ -117,6 +117,22 @@ Display the current todo list with status of each item.
|
|||||||
|
|
||||||
**Returns:** The full todo list with goal, progress, and item statuses
|
**Returns:** The full todo list with goal, progress, and item statuses
|
||||||
|
|
||||||
|
### `todo__clear`
|
||||||
|
Clear the entire todo list and reset the goal. Use when the current task has been canceled or invalidated.
|
||||||
|
|
||||||
|
**Parameters:** None
|
||||||
|
|
||||||
|
**Returns:** Confirmation that the todo list was cleared
|
||||||
|
|
||||||
|
### REPL Command: `.clear todo`
|
||||||
|
You can also clear the todo list manually from the REPL by typing `.clear todo`. This is useful when:
|
||||||
|
- You gave a custom response that changes or cancels the current task
|
||||||
|
- The agent is stuck in auto-continuation with stale todos
|
||||||
|
- You want to start fresh without leaving and re-entering the agent
|
||||||
|
|
||||||
|
**Note:** This command is only available when an agent with `auto_continue: true` is active. If the todo
|
||||||
|
system isn't enabled for the current agent, the command will display an error message.
|
||||||
|
|
||||||
## Auto-Continuation
|
## Auto-Continuation
|
||||||
When `auto_continue` is enabled, Loki automatically sends a continuation prompt if:
|
When `auto_continue` is enabled, Loki automatically sends a continuation prompt if:
|
||||||
|
|
||||||
|
|||||||
+99
-6
@@ -14,6 +14,7 @@ loki --info | grep 'config_file' | awk '{print $2}'
|
|||||||
<!--toc:start-->
|
<!--toc:start-->
|
||||||
- [Supported Clients](#supported-clients)
|
- [Supported Clients](#supported-clients)
|
||||||
- [Client Configuration](#client-configuration)
|
- [Client Configuration](#client-configuration)
|
||||||
|
- [Authentication](#authentication)
|
||||||
- [Extra Settings](#extra-settings)
|
- [Extra Settings](#extra-settings)
|
||||||
<!--toc:end-->
|
<!--toc:end-->
|
||||||
|
|
||||||
@@ -51,12 +52,13 @@ clients:
|
|||||||
The client metadata uniquely identifies the client in Loki so you can reference it across your configurations. The
|
The client metadata uniquely identifies the client in Loki so you can reference it across your configurations. The
|
||||||
available settings are listed below:
|
available settings are listed below:
|
||||||
|
|
||||||
| Setting | Description |
|
| Setting | Description |
|
||||||
|----------|-----------------------------------------------------------------------------------------------|
|
|----------|------------------------------------------------------------------------------------------------------------|
|
||||||
| `name` | The name of the client (e.g. `openai`, `gemini`, etc.) |
|
| `name` | The name of the client (e.g. `openai`, `gemini`, etc.) |
|
||||||
| `models` | See the [model settings](#model-settings) documentation below |
|
| `auth` | Authentication method: `oauth` for OAuth, or omit to use `api_key` (see [Authentication](#authentication)) |
|
||||||
| `patch` | See the [client patch configuration](./PATCHES.md#client-configuration-patches) documentation |
|
| `models` | See the [model settings](#model-settings) documentation below |
|
||||||
| `extra` | See the [extra settings](#extra-settings) documentation below |
|
| `patch` | See the [client patch configuration](./PATCHES.md#client-configuration-patches) documentation |
|
||||||
|
| `extra` | See the [extra settings](#extra-settings) documentation below |
|
||||||
|
|
||||||
Be sure to also check provider-specific configurations for any extra fields that are added for authentication purposes.
|
Be sure to also check provider-specific configurations for any extra fields that are added for authentication purposes.
|
||||||
|
|
||||||
@@ -83,6 +85,97 @@ The `models` array lists the available models from the model client. Each one ha
|
|||||||
| `default_chunk_size` | | `embedding` | The default chunk size to use with the given model |
|
| `default_chunk_size` | | `embedding` | The default chunk size to use with the given model |
|
||||||
| `max_batch_size` | | `embedding` | The maximum batch size that the given embedding model supports |
|
| `max_batch_size` | | `embedding` | The maximum batch size that the given embedding model supports |
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
Loki clients support two authentication methods: **API keys** and **OAuth**. Each client entry in your configuration
|
||||||
|
must use one or the other.
|
||||||
|
|
||||||
|
### API Key Authentication
|
||||||
|
|
||||||
|
Most clients authenticate using an API key. Simply set the `api_key` field directly or inject it from the
|
||||||
|
[Loki vault](../VAULT.md):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
clients:
|
||||||
|
- type: claude
|
||||||
|
api_key: '{{ANTHROPIC_API_KEY}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
API keys can also be provided via environment variables named `{CLIENT_NAME}_API_KEY` (e.g. `OPENAI_API_KEY`,
|
||||||
|
`GEMINI_API_KEY`). See the [environment variables documentation](../ENVIRONMENT-VARIABLES.md#client-related-variables)
|
||||||
|
for details.
|
||||||
|
|
||||||
|
### OAuth Authentication
|
||||||
|
|
||||||
|
For [providers that support OAuth](#providers-that-support-oauth), you can authenticate using your existing subscription instead of an API key. This uses
|
||||||
|
the OAuth 2.0 PKCE flow.
|
||||||
|
|
||||||
|
**Step 1: Configure the client**
|
||||||
|
|
||||||
|
Add a client entry with `auth: oauth` and no `api_key`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
clients:
|
||||||
|
- type: claude
|
||||||
|
name: my-claude-oauth
|
||||||
|
auth: oauth
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Authenticate**
|
||||||
|
|
||||||
|
Run the `--authenticate` flag with the client name:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
loki --authenticate my-claude-oauth
|
||||||
|
```
|
||||||
|
|
||||||
|
Or if you have only one OAuth-configured client, you can omit the name:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
loki --authenticate
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, you can use the REPL command `.authenticate`.
|
||||||
|
|
||||||
|
This opens your browser for the OAuth authorization flow. Depending on the provider, Loki will either start a
|
||||||
|
temporary localhost server to capture the callback automatically (e.g. Gemini) or ask you to paste the authorization
|
||||||
|
code back into the terminal (e.g. Claude). Loki stores the tokens in `~/.cache/loki/oauth` and automatically refreshes
|
||||||
|
them when they expire.
|
||||||
|
|
||||||
|
#### Gemini OAuth Note
|
||||||
|
Loki uses the following scopes for OAuth with Gemini:
|
||||||
|
* https://www.googleapis.com/auth/generative-language.peruserquota
|
||||||
|
* https://www.googleapis.com/auth/userinfo.email
|
||||||
|
* https://www.googleapis.com/auth/generative-language.retriever (Sensitive)
|
||||||
|
|
||||||
|
Since the `generative-language.retriever` scope is a sensitive scope, Google needs to verify Loki, which requires full
|
||||||
|
branding (logo, official website, privacy policy, terms of service, etc.). The Loki app is open-source and is designed
|
||||||
|
to be used as a simple CLI. As such, there's no terms of service or privacy policy associated with it, and thus Google
|
||||||
|
cannot verify Loki.
|
||||||
|
|
||||||
|
So, when you kick off OAuth with Gemini, you may see a page similar to the following:
|
||||||
|

|
||||||
|
|
||||||
|
Simply click the `Advanced` link and click `Go to Loki (unsafe)` to continue the OAuth flow.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
**Step 3: Use normally**
|
||||||
|
|
||||||
|
Once authenticated, the client works like any other. Loki uses the stored OAuth tokens automatically:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
loki -m my-claude-oauth:claude-sonnet-4-20250514 "Hello!"
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** You can have multiple clients for the same provider. For example: you can have one with an API key and
|
||||||
|
> another with OAuth. Use the `name` field to distinguish them.
|
||||||
|
|
||||||
|
### Providers That Support OAuth
|
||||||
|
* Claude
|
||||||
|
* Gemini
|
||||||
|
|
||||||
## Extra Settings
|
## Extra Settings
|
||||||
Loki also lets you customize some extra settings for interacting with APIs:
|
Loki also lets you customize some extra settings for interacting with APIs:
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ Loki ships with a `functions/mcp.json` file that includes some useful MCP server
|
|||||||
* [github](https://github.com/github/github-mcp-server) - Interact with GitHub repositories, issues, pull requests, and more.
|
* [github](https://github.com/github/github-mcp-server) - Interact with GitHub repositories, issues, pull requests, and more.
|
||||||
* [docker](https://github.com/ckreiling/mcp-server-docker) - Manage your local Docker containers with natural language
|
* [docker](https://github.com/ckreiling/mcp-server-docker) - Manage your local Docker containers with natural language
|
||||||
* [slack](https://github.com/korotovsky/slack-mcp-server) - Interact with Slack
|
* [slack](https://github.com/korotovsky/slack-mcp-server) - Interact with Slack
|
||||||
|
* [ddg-search](https://github.com/nickclyde/duckduckgo-mcp-server) - Perform web searches with the DuckDuckGo search engine
|
||||||
|
|
||||||
## Loki Configuration
|
## Loki Configuration
|
||||||
MCP servers, like tools, can be used in a handful of contexts:
|
MCP servers, like tools, can be used in a handful of contexts:
|
||||||
|
|||||||
+259
-136
@@ -3,6 +3,13 @@
|
|||||||
# - https://platform.openai.com/docs/api-reference/chat
|
# - https://platform.openai.com/docs/api-reference/chat
|
||||||
- provider: openai
|
- provider: openai
|
||||||
models:
|
models:
|
||||||
|
- name: gpt-5.2
|
||||||
|
max_input_tokens: 400000
|
||||||
|
max_output_tokens: 128000
|
||||||
|
input_price: 1.75
|
||||||
|
output_price: 14
|
||||||
|
supports_vision: true
|
||||||
|
supports_function_calling: true
|
||||||
- name: gpt-5.1
|
- name: gpt-5.1
|
||||||
max_input_tokens: 400000
|
max_input_tokens: 400000
|
||||||
max_output_tokens: 128000
|
max_output_tokens: 128000
|
||||||
@@ -81,6 +88,7 @@
|
|||||||
supports_vision: true
|
supports_vision: true
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: o4-mini
|
- name: o4-mini
|
||||||
|
max_output_tokens: 100000
|
||||||
max_input_tokens: 200000
|
max_input_tokens: 200000
|
||||||
input_price: 1.1
|
input_price: 1.1
|
||||||
output_price: 4.4
|
output_price: 4.4
|
||||||
@@ -93,6 +101,7 @@
|
|||||||
temperature: null
|
temperature: null
|
||||||
top_p: null
|
top_p: null
|
||||||
- name: o4-mini-high
|
- name: o4-mini-high
|
||||||
|
max_output_tokens: 100000
|
||||||
real_name: o4-mini
|
real_name: o4-mini
|
||||||
max_input_tokens: 200000
|
max_input_tokens: 200000
|
||||||
input_price: 1.1
|
input_price: 1.1
|
||||||
@@ -107,6 +116,7 @@
|
|||||||
temperature: null
|
temperature: null
|
||||||
top_p: null
|
top_p: null
|
||||||
- name: o3
|
- name: o3
|
||||||
|
max_output_tokens: 100000
|
||||||
max_input_tokens: 200000
|
max_input_tokens: 200000
|
||||||
input_price: 2
|
input_price: 2
|
||||||
output_price: 8
|
output_price: 8
|
||||||
@@ -133,6 +143,7 @@
|
|||||||
temperature: null
|
temperature: null
|
||||||
top_p: null
|
top_p: null
|
||||||
- name: o3-mini
|
- name: o3-mini
|
||||||
|
max_output_tokens: 100000
|
||||||
max_input_tokens: 200000
|
max_input_tokens: 200000
|
||||||
input_price: 1.1
|
input_price: 1.1
|
||||||
output_price: 4.4
|
output_price: 4.4
|
||||||
@@ -145,6 +156,7 @@
|
|||||||
temperature: null
|
temperature: null
|
||||||
top_p: null
|
top_p: null
|
||||||
- name: o3-mini-high
|
- name: o3-mini-high
|
||||||
|
max_output_tokens: 100000
|
||||||
real_name: o3-mini
|
real_name: o3-mini
|
||||||
max_input_tokens: 200000
|
max_input_tokens: 200000
|
||||||
input_price: 1.1
|
input_price: 1.1
|
||||||
@@ -190,25 +202,32 @@
|
|||||||
# - https://ai.google.dev/api/rest/v1beta/models/streamGenerateContent
|
# - https://ai.google.dev/api/rest/v1beta/models/streamGenerateContent
|
||||||
- provider: gemini
|
- provider: gemini
|
||||||
models:
|
models:
|
||||||
|
- name: gemini-3.1-pro-preview
|
||||||
|
max_input_tokens: 1048576
|
||||||
|
max_output_tokens: 65535
|
||||||
|
input_price: 0.3
|
||||||
|
output_price: 2.5
|
||||||
|
supports_vision: true
|
||||||
|
supports_function_calling: true
|
||||||
- name: gemini-2.5-flash
|
- name: gemini-2.5-flash
|
||||||
max_input_tokens: 1048576
|
max_input_tokens: 1048576
|
||||||
max_output_tokens: 65536
|
max_output_tokens: 65535
|
||||||
input_price: 0
|
input_price: 0.3
|
||||||
output_price: 0
|
output_price: 2.5
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: gemini-2.5-pro
|
- name: gemini-2.5-pro
|
||||||
max_input_tokens: 1048576
|
max_input_tokens: 1048576
|
||||||
max_output_tokens: 65536
|
max_output_tokens: 65536
|
||||||
input_price: 0
|
input_price: 1.25
|
||||||
output_price: 0
|
output_price: 10
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: gemini-2.5-flash-lite
|
- name: gemini-2.5-flash-lite
|
||||||
max_input_tokens: 1000000
|
max_input_tokens: 1048576
|
||||||
max_output_tokens: 64000
|
max_output_tokens: 65535
|
||||||
input_price: 0
|
input_price: 0.1
|
||||||
output_price: 0
|
output_price: 0.4
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: gemini-2.0-flash
|
- name: gemini-2.0-flash
|
||||||
@@ -226,10 +245,11 @@
|
|||||||
supports_vision: true
|
supports_vision: true
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: gemma-3-27b-it
|
- name: gemma-3-27b-it
|
||||||
max_input_tokens: 131072
|
supports_vision: true
|
||||||
max_output_tokens: 8192
|
max_input_tokens: 128000
|
||||||
input_price: 0
|
max_output_tokens: 65536
|
||||||
output_price: 0
|
input_price: 0.04
|
||||||
|
output_price: 0.15
|
||||||
- name: text-embedding-004
|
- name: text-embedding-004
|
||||||
type: embedding
|
type: embedding
|
||||||
input_price: 0
|
input_price: 0
|
||||||
@@ -242,6 +262,54 @@
|
|||||||
# - https://docs.anthropic.com/en/api/messages
|
# - https://docs.anthropic.com/en/api/messages
|
||||||
- provider: claude
|
- provider: claude
|
||||||
models:
|
models:
|
||||||
|
- name: claude-opus-4-6
|
||||||
|
max_input_tokens: 200000
|
||||||
|
max_output_tokens: 8192
|
||||||
|
require_max_tokens: true
|
||||||
|
input_price: 5
|
||||||
|
output_price: 25
|
||||||
|
supports_vision: true
|
||||||
|
supports_function_calling: true
|
||||||
|
- name: claude-opus-4-6:thinking
|
||||||
|
real_name: claude-opus-4-6
|
||||||
|
max_input_tokens: 200000
|
||||||
|
max_output_tokens: 24000
|
||||||
|
require_max_tokens: true
|
||||||
|
input_price: 5
|
||||||
|
output_price: 25
|
||||||
|
supports_vision: true
|
||||||
|
supports_function_calling: true
|
||||||
|
patch:
|
||||||
|
body:
|
||||||
|
temperature: null
|
||||||
|
top_p: null
|
||||||
|
thinking:
|
||||||
|
type: enabled
|
||||||
|
budget_tokens: 16000
|
||||||
|
- name: claude-sonnet-4-6
|
||||||
|
max_input_tokens: 200000
|
||||||
|
max_output_tokens: 8192
|
||||||
|
require_max_tokens: true
|
||||||
|
input_price: 3
|
||||||
|
output_price: 15
|
||||||
|
supports_vision: true
|
||||||
|
supports_function_calling: true
|
||||||
|
- name: claude-sonnet-4-6:thinking
|
||||||
|
real_name: claude-sonnet-4-6
|
||||||
|
max_input_tokens: 200000
|
||||||
|
max_output_tokens: 24000
|
||||||
|
require_max_tokens: true
|
||||||
|
input_price: 3
|
||||||
|
output_price: 15
|
||||||
|
supports_vision: true
|
||||||
|
supports_function_calling: true
|
||||||
|
patch:
|
||||||
|
body:
|
||||||
|
temperature: null
|
||||||
|
top_p: null
|
||||||
|
thinking:
|
||||||
|
type: enabled
|
||||||
|
budget_tokens: 16000
|
||||||
- name: claude-sonnet-4-5-20250929
|
- name: claude-sonnet-4-5-20250929
|
||||||
max_input_tokens: 200000
|
max_input_tokens: 200000
|
||||||
max_output_tokens: 8192
|
max_output_tokens: 8192
|
||||||
@@ -509,8 +577,8 @@
|
|||||||
output_price: 10
|
output_price: 10
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
- name: command-r7b-12-2024
|
- name: command-r7b-12-2024
|
||||||
max_input_tokens: 131072
|
max_input_tokens: 128000
|
||||||
max_output_tokens: 4096
|
max_output_tokens: 4000
|
||||||
input_price: 0.0375
|
input_price: 0.0375
|
||||||
output_price: 0.15
|
output_price: 0.15
|
||||||
- name: embed-v4.0
|
- name: embed-v4.0
|
||||||
@@ -547,6 +615,7 @@
|
|||||||
- provider: xai
|
- provider: xai
|
||||||
models:
|
models:
|
||||||
- name: grok-4
|
- name: grok-4
|
||||||
|
supports_vision: true
|
||||||
max_input_tokens: 256000
|
max_input_tokens: 256000
|
||||||
input_price: 3
|
input_price: 3
|
||||||
output_price: 15
|
output_price: 15
|
||||||
@@ -583,14 +652,18 @@
|
|||||||
- provider: perplexity
|
- provider: perplexity
|
||||||
models:
|
models:
|
||||||
- name: sonar-pro
|
- name: sonar-pro
|
||||||
|
max_output_tokens: 8000
|
||||||
|
supports_vision: true
|
||||||
max_input_tokens: 200000
|
max_input_tokens: 200000
|
||||||
input_price: 3
|
input_price: 3
|
||||||
output_price: 15
|
output_price: 15
|
||||||
- name: sonar
|
- name: sonar
|
||||||
max_input_tokens: 128000
|
supports_vision: true
|
||||||
|
max_input_tokens: 127072
|
||||||
input_price: 1
|
input_price: 1
|
||||||
output_price: 1
|
output_price: 1
|
||||||
- name: sonar-reasoning-pro
|
- name: sonar-reasoning-pro
|
||||||
|
supports_vision: true
|
||||||
max_input_tokens: 128000
|
max_input_tokens: 128000
|
||||||
input_price: 2
|
input_price: 2
|
||||||
output_price: 8
|
output_price: 8
|
||||||
@@ -659,17 +732,16 @@
|
|||||||
# - https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/gemini
|
# - https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/gemini
|
||||||
- provider: vertexai
|
- provider: vertexai
|
||||||
models:
|
models:
|
||||||
- name: gemini-3-pro-preview
|
- name: gemini-3.1-pro-preview
|
||||||
hipaa_safe: true
|
|
||||||
max_input_tokens: 1048576
|
max_input_tokens: 1048576
|
||||||
max_output_tokens: 65536
|
max_output_tokens: 65536
|
||||||
input_price: 0
|
input_price: 2
|
||||||
output_price: 0
|
output_price: 12
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: gemini-2.5-flash
|
- name: gemini-2.5-flash
|
||||||
max_input_tokens: 1048576
|
max_input_tokens: 1048576
|
||||||
max_output_tokens: 65536
|
max_output_tokens: 65535
|
||||||
input_price: 0.3
|
input_price: 0.3
|
||||||
output_price: 2.5
|
output_price: 2.5
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
@@ -683,16 +755,16 @@
|
|||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: gemini-2.5-flash-lite
|
- name: gemini-2.5-flash-lite
|
||||||
max_input_tokens: 1048576
|
max_input_tokens: 1048576
|
||||||
max_output_tokens: 65536
|
max_output_tokens: 65535
|
||||||
input_price: 0.3
|
input_price: 0.1
|
||||||
output_price: 0.4
|
output_price: 0.4
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: gemini-2.0-flash-001
|
- name: gemini-2.0-flash-001
|
||||||
max_input_tokens: 1048576
|
max_input_tokens: 1048576
|
||||||
max_output_tokens: 8192
|
max_output_tokens: 8192
|
||||||
input_price: 0.15
|
input_price: 0.1
|
||||||
output_price: 0.6
|
output_price: 0.4
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: gemini-2.0-flash-lite-001
|
- name: gemini-2.0-flash-lite-001
|
||||||
@@ -1187,17 +1259,22 @@
|
|||||||
max_input_tokens: 1024
|
max_input_tokens: 1024
|
||||||
input_price: 0.07
|
input_price: 0.07
|
||||||
|
|
||||||
|
|
||||||
# Links:
|
# Links:
|
||||||
# - https://help.aliyun.com/zh/model-studio/getting-started/models
|
# - https://help.aliyun.com/zh/model-studio/getting-started/models
|
||||||
# - https://help.aliyun.com/zh/model-studio/developer-reference/use-qwen-by-calling-api
|
# - https://help.aliyun.com/zh/model-studio/developer-reference/use-qwen-by-calling-api
|
||||||
- provider: qianwen
|
- provider: qianwen
|
||||||
models:
|
models:
|
||||||
- name: qwen3-max
|
- name: qwen3-max
|
||||||
|
input_price: 1.2
|
||||||
|
output_price: 6
|
||||||
|
max_output_tokens: 32768
|
||||||
max_input_tokens: 262144
|
max_input_tokens: 262144
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: qwen-plus
|
- name: qwen-plus
|
||||||
max_input_tokens: 131072
|
input_price: 0.4
|
||||||
|
output_price: 1.2
|
||||||
|
max_output_tokens: 32768
|
||||||
|
max_input_tokens: 1000000
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: qwen-flash
|
- name: qwen-flash
|
||||||
max_input_tokens: 1000000
|
max_input_tokens: 1000000
|
||||||
@@ -1213,14 +1290,14 @@
|
|||||||
- name: qwen-coder-flash
|
- name: qwen-coder-flash
|
||||||
max_input_tokens: 1000000
|
max_input_tokens: 1000000
|
||||||
- name: qwen3-next-80b-a3b-instruct
|
- name: qwen3-next-80b-a3b-instruct
|
||||||
max_input_tokens: 131072
|
max_input_tokens: 262144
|
||||||
input_price: 0.14
|
input_price: 0.09
|
||||||
output_price: 0.56
|
output_price: 1.1
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: qwen3-next-80b-a3b-thinking
|
- name: qwen3-next-80b-a3b-thinking
|
||||||
max_input_tokens: 131072
|
max_input_tokens: 128000
|
||||||
input_price: 0.14
|
input_price: 0.15
|
||||||
output_price: 1.4
|
output_price: 1.2
|
||||||
- name: qwen3-235b-a22b-instruct-2507
|
- name: qwen3-235b-a22b-instruct-2507
|
||||||
max_input_tokens: 131072
|
max_input_tokens: 131072
|
||||||
input_price: 0.28
|
input_price: 0.28
|
||||||
@@ -1228,35 +1305,39 @@
|
|||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: qwen3-235b-a22b-thinking-2507
|
- name: qwen3-235b-a22b-thinking-2507
|
||||||
max_input_tokens: 131072
|
max_input_tokens: 131072
|
||||||
input_price: 0.28
|
input_price: 0
|
||||||
output_price: 2.8
|
output_price: 0
|
||||||
- name: qwen3-30b-a3b-instruct-2507
|
- name: qwen3-30b-a3b-instruct-2507
|
||||||
max_input_tokens: 131072
|
max_output_tokens: 262144
|
||||||
input_price: 0.105
|
max_input_tokens: 262144
|
||||||
output_price: 0.42
|
input_price: 0.09
|
||||||
|
output_price: 0.3
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: qwen3-30b-a3b-thinking-2507
|
- name: qwen3-30b-a3b-thinking-2507
|
||||||
max_input_tokens: 131072
|
max_input_tokens: 32768
|
||||||
input_price: 0.105
|
input_price: 0.051
|
||||||
output_price: 1.05
|
output_price: 0.34
|
||||||
- name: qwen3-vl-32b-instruct
|
- name: qwen3-vl-32b-instruct
|
||||||
|
max_output_tokens: 32768
|
||||||
max_input_tokens: 131072
|
max_input_tokens: 131072
|
||||||
input_price: 0.28
|
input_price: 0.104
|
||||||
output_price: 1.12
|
output_price: 0.416
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
- name: qwen3-vl-8b-instruct
|
- name: qwen3-vl-8b-instruct
|
||||||
|
max_output_tokens: 32768
|
||||||
max_input_tokens: 131072
|
max_input_tokens: 131072
|
||||||
input_price: 0.07
|
input_price: 0.08
|
||||||
output_price: 0.28
|
output_price: 0.5
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
- name: qwen3-coder-480b-a35b-instruct
|
- name: qwen3-coder-480b-a35b-instruct
|
||||||
max_input_tokens: 262144
|
max_input_tokens: 262144
|
||||||
input_price: 1.26
|
input_price: 1.26
|
||||||
output_price: 5.04
|
output_price: 5.04
|
||||||
- name: qwen3-coder-30b-a3b-instruct
|
- name: qwen3-coder-30b-a3b-instruct
|
||||||
max_input_tokens: 262144
|
max_output_tokens: 32768
|
||||||
input_price: 0.315
|
max_input_tokens: 160000
|
||||||
output_price: 1.26
|
input_price: 0.07
|
||||||
|
output_price: 0.27
|
||||||
- name: deepseek-v3.2-exp
|
- name: deepseek-v3.2-exp
|
||||||
max_input_tokens: 131072
|
max_input_tokens: 131072
|
||||||
input_price: 0.28
|
input_price: 0.28
|
||||||
@@ -1332,9 +1413,9 @@
|
|||||||
output_price: 8.12
|
output_price: 8.12
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
- name: kimi-k2-thinking
|
- name: kimi-k2-thinking
|
||||||
max_input_tokens: 262144
|
max_input_tokens: 131072
|
||||||
input_price: 0.56
|
input_price: 0.47
|
||||||
output_price: 2.24
|
output_price: 2
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
|
|
||||||
# Links:
|
# Links:
|
||||||
@@ -1343,10 +1424,10 @@
|
|||||||
- provider: deepseek
|
- provider: deepseek
|
||||||
models:
|
models:
|
||||||
- name: deepseek-chat
|
- name: deepseek-chat
|
||||||
max_input_tokens: 64000
|
max_input_tokens: 163840
|
||||||
max_output_tokens: 8192
|
max_output_tokens: 163840
|
||||||
input_price: 0.56
|
input_price: 0.32
|
||||||
output_price: 1.68
|
output_price: 0.89
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: deepseek-reasoner
|
- name: deepseek-reasoner
|
||||||
max_input_tokens: 64000
|
max_input_tokens: 64000
|
||||||
@@ -1424,9 +1505,10 @@
|
|||||||
- provider: minimax
|
- provider: minimax
|
||||||
models:
|
models:
|
||||||
- name: minimax-m2
|
- name: minimax-m2
|
||||||
max_input_tokens: 204800
|
max_output_tokens: 65536
|
||||||
input_price: 0.294
|
max_input_tokens: 196608
|
||||||
output_price: 1.176
|
input_price: 0.255
|
||||||
|
output_price: 1
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
|
|
||||||
# Links:
|
# Links:
|
||||||
@@ -1442,8 +1524,8 @@
|
|||||||
supports_vision: true
|
supports_vision: true
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: openai/gpt-5.1-chat
|
- name: openai/gpt-5.1-chat
|
||||||
max_input_tokens: 400000
|
max_input_tokens: 128000
|
||||||
max_output_tokens: 128000
|
max_output_tokens: 16384
|
||||||
input_price: 1.25
|
input_price: 1.25
|
||||||
output_price: 10
|
output_price: 10
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
@@ -1456,8 +1538,8 @@
|
|||||||
supports_vision: true
|
supports_vision: true
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: openai/gpt-5-chat
|
- name: openai/gpt-5-chat
|
||||||
max_input_tokens: 400000
|
max_input_tokens: 128000
|
||||||
max_output_tokens: 128000
|
max_output_tokens: 16384
|
||||||
input_price: 1.25
|
input_price: 1.25
|
||||||
output_price: 10
|
output_price: 10
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
@@ -1498,18 +1580,21 @@
|
|||||||
supports_vision: true
|
supports_vision: true
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: openai/gpt-4o
|
- name: openai/gpt-4o
|
||||||
|
max_output_tokens: 16384
|
||||||
max_input_tokens: 128000
|
max_input_tokens: 128000
|
||||||
input_price: 2.5
|
input_price: 2.5
|
||||||
output_price: 10
|
output_price: 10
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: openai/gpt-4o-mini
|
- name: openai/gpt-4o-mini
|
||||||
|
max_output_tokens: 16384
|
||||||
max_input_tokens: 128000
|
max_input_tokens: 128000
|
||||||
input_price: 0.15
|
input_price: 0.15
|
||||||
output_price: 0.6
|
output_price: 0.6
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: openai/o4-mini
|
- name: openai/o4-mini
|
||||||
|
max_output_tokens: 100000
|
||||||
max_input_tokens: 200000
|
max_input_tokens: 200000
|
||||||
input_price: 1.1
|
input_price: 1.1
|
||||||
output_price: 4.4
|
output_price: 4.4
|
||||||
@@ -1522,6 +1607,7 @@
|
|||||||
temperature: null
|
temperature: null
|
||||||
top_p: null
|
top_p: null
|
||||||
- name: openai/o4-mini-high
|
- name: openai/o4-mini-high
|
||||||
|
max_output_tokens: 100000
|
||||||
max_input_tokens: 200000
|
max_input_tokens: 200000
|
||||||
input_price: 1.1
|
input_price: 1.1
|
||||||
output_price: 4.4
|
output_price: 4.4
|
||||||
@@ -1535,6 +1621,7 @@
|
|||||||
temperature: null
|
temperature: null
|
||||||
top_p: null
|
top_p: null
|
||||||
- name: openai/o3
|
- name: openai/o3
|
||||||
|
max_output_tokens: 100000
|
||||||
max_input_tokens: 200000
|
max_input_tokens: 200000
|
||||||
input_price: 2
|
input_price: 2
|
||||||
output_price: 8
|
output_price: 8
|
||||||
@@ -1560,6 +1647,7 @@
|
|||||||
temperature: null
|
temperature: null
|
||||||
top_p: null
|
top_p: null
|
||||||
- name: openai/o3-mini
|
- name: openai/o3-mini
|
||||||
|
max_output_tokens: 100000
|
||||||
max_input_tokens: 200000
|
max_input_tokens: 200000
|
||||||
input_price: 1.1
|
input_price: 1.1
|
||||||
output_price: 4.4
|
output_price: 4.4
|
||||||
@@ -1571,6 +1659,7 @@
|
|||||||
temperature: null
|
temperature: null
|
||||||
top_p: null
|
top_p: null
|
||||||
- name: openai/o3-mini-high
|
- name: openai/o3-mini-high
|
||||||
|
max_output_tokens: 100000
|
||||||
max_input_tokens: 200000
|
max_input_tokens: 200000
|
||||||
input_price: 1.1
|
input_price: 1.1
|
||||||
output_price: 4.4
|
output_price: 4.4
|
||||||
@@ -1583,50 +1672,57 @@
|
|||||||
top_p: null
|
top_p: null
|
||||||
- name: openai/gpt-oss-120b
|
- name: openai/gpt-oss-120b
|
||||||
max_input_tokens: 131072
|
max_input_tokens: 131072
|
||||||
input_price: 0.09
|
input_price: 0.039
|
||||||
output_price: 0.45
|
output_price: 0.19
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: openai/gpt-oss-20b
|
- name: openai/gpt-oss-20b
|
||||||
max_input_tokens: 131072
|
max_input_tokens: 131072
|
||||||
input_price: 0.04
|
input_price: 0.03
|
||||||
output_price: 0.16
|
output_price: 0.14
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: google/gemini-2.5-flash
|
- name: google/gemini-2.5-flash
|
||||||
|
max_output_tokens: 65535
|
||||||
max_input_tokens: 1048576
|
max_input_tokens: 1048576
|
||||||
input_price: 0.3
|
input_price: 0.3
|
||||||
output_price: 2.5
|
output_price: 2.5
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: google/gemini-2.5-pro
|
- name: google/gemini-2.5-pro
|
||||||
|
max_output_tokens: 65536
|
||||||
max_input_tokens: 1048576
|
max_input_tokens: 1048576
|
||||||
input_price: 1.25
|
input_price: 1.25
|
||||||
output_price: 10
|
output_price: 10
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: google/gemini-2.5-flash-lite
|
- name: google/gemini-2.5-flash-lite
|
||||||
|
max_output_tokens: 65535
|
||||||
max_input_tokens: 1048576
|
max_input_tokens: 1048576
|
||||||
input_price: 0.3
|
input_price: 0.1
|
||||||
output_price: 0.4
|
output_price: 0.4
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
- name: google/gemini-2.0-flash-001
|
- name: google/gemini-2.0-flash-001
|
||||||
max_input_tokens: 1000000
|
max_output_tokens: 8192
|
||||||
input_price: 0.15
|
max_input_tokens: 1048576
|
||||||
output_price: 0.6
|
input_price: 0.1
|
||||||
|
output_price: 0.4
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: google/gemini-2.0-flash-lite-001
|
- name: google/gemini-2.0-flash-lite-001
|
||||||
|
max_output_tokens: 8192
|
||||||
max_input_tokens: 1048576
|
max_input_tokens: 1048576
|
||||||
input_price: 0.075
|
input_price: 0.075
|
||||||
output_price: 0.3
|
output_price: 0.3
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: google/gemma-3-27b-it
|
- name: google/gemma-3-27b-it
|
||||||
max_input_tokens: 131072
|
max_output_tokens: 65536
|
||||||
input_price: 0.1
|
supports_vision: true
|
||||||
output_price: 0.2
|
max_input_tokens: 128000
|
||||||
|
input_price: 0.04
|
||||||
|
output_price: 0.15
|
||||||
- name: anthropic/claude-sonnet-4.5
|
- name: anthropic/claude-sonnet-4.5
|
||||||
max_input_tokens: 200000
|
max_input_tokens: 1000000
|
||||||
max_output_tokens: 8192
|
max_output_tokens: 64000
|
||||||
require_max_tokens: true
|
require_max_tokens: true
|
||||||
input_price: 3
|
input_price: 3
|
||||||
output_price: 15
|
output_price: 15
|
||||||
@@ -1634,7 +1730,7 @@
|
|||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: anthropic/claude-haiku-4.5
|
- name: anthropic/claude-haiku-4.5
|
||||||
max_input_tokens: 200000
|
max_input_tokens: 200000
|
||||||
max_output_tokens: 8192
|
max_output_tokens: 64000
|
||||||
require_max_tokens: true
|
require_max_tokens: true
|
||||||
input_price: 1
|
input_price: 1
|
||||||
output_price: 5
|
output_price: 5
|
||||||
@@ -1642,7 +1738,7 @@
|
|||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: anthropic/claude-opus-4.1
|
- name: anthropic/claude-opus-4.1
|
||||||
max_input_tokens: 200000
|
max_input_tokens: 200000
|
||||||
max_output_tokens: 8192
|
max_output_tokens: 32000
|
||||||
require_max_tokens: true
|
require_max_tokens: true
|
||||||
input_price: 15
|
input_price: 15
|
||||||
output_price: 75
|
output_price: 75
|
||||||
@@ -1650,15 +1746,15 @@
|
|||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: anthropic/claude-opus-4
|
- name: anthropic/claude-opus-4
|
||||||
max_input_tokens: 200000
|
max_input_tokens: 200000
|
||||||
max_output_tokens: 8192
|
max_output_tokens: 32000
|
||||||
require_max_tokens: true
|
require_max_tokens: true
|
||||||
input_price: 15
|
input_price: 15
|
||||||
output_price: 75
|
output_price: 75
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: anthropic/claude-sonnet-4
|
- name: anthropic/claude-sonnet-4
|
||||||
max_input_tokens: 200000
|
max_input_tokens: 1000000
|
||||||
max_output_tokens: 8192
|
max_output_tokens: 64000
|
||||||
require_max_tokens: true
|
require_max_tokens: true
|
||||||
input_price: 3
|
input_price: 3
|
||||||
output_price: 15
|
output_price: 15
|
||||||
@@ -1666,7 +1762,7 @@
|
|||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: anthropic/claude-3.7-sonnet
|
- name: anthropic/claude-3.7-sonnet
|
||||||
max_input_tokens: 200000
|
max_input_tokens: 200000
|
||||||
max_output_tokens: 8192
|
max_output_tokens: 64000
|
||||||
require_max_tokens: true
|
require_max_tokens: true
|
||||||
input_price: 3
|
input_price: 3
|
||||||
output_price: 15
|
output_price: 15
|
||||||
@@ -1681,21 +1777,24 @@
|
|||||||
supports_vision: true
|
supports_vision: true
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: meta-llama/llama-4-maverick
|
- name: meta-llama/llama-4-maverick
|
||||||
|
max_output_tokens: 16384
|
||||||
max_input_tokens: 1048576
|
max_input_tokens: 1048576
|
||||||
input_price: 0.18
|
input_price: 0.15
|
||||||
output_price: 0.6
|
output_price: 0.6
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: meta-llama/llama-4-scout
|
- name: meta-llama/llama-4-scout
|
||||||
|
max_output_tokens: 16384
|
||||||
max_input_tokens: 327680
|
max_input_tokens: 327680
|
||||||
input_price: 0.08
|
input_price: 0.08
|
||||||
output_price: 0.3
|
output_price: 0.3
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: meta-llama/llama-3.3-70b-instruct
|
- name: meta-llama/llama-3.3-70b-instruct
|
||||||
|
max_output_tokens: 16384
|
||||||
max_input_tokens: 131072
|
max_input_tokens: 131072
|
||||||
input_price: 0.12
|
input_price: 0.1
|
||||||
output_price: 0.3
|
output_price: 0.32
|
||||||
- name: mistralai/mistral-medium-3.1
|
- name: mistralai/mistral-medium-3.1
|
||||||
max_input_tokens: 131072
|
max_input_tokens: 131072
|
||||||
input_price: 0.4
|
input_price: 0.4
|
||||||
@@ -1703,9 +1802,10 @@
|
|||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
- name: mistralai/mistral-small-3.2-24b-instruct
|
- name: mistralai/mistral-small-3.2-24b-instruct
|
||||||
|
max_output_tokens: 131072
|
||||||
max_input_tokens: 131072
|
max_input_tokens: 131072
|
||||||
input_price: 0.1
|
input_price: 0.06
|
||||||
output_price: 0.3
|
output_price: 0.18
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
- name: mistralai/magistral-medium-2506
|
- name: mistralai/magistral-medium-2506
|
||||||
max_input_tokens: 40960
|
max_input_tokens: 40960
|
||||||
@@ -1726,8 +1826,8 @@
|
|||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: mistralai/devstral-small
|
- name: mistralai/devstral-small
|
||||||
max_input_tokens: 131072
|
max_input_tokens: 131072
|
||||||
input_price: 0.07
|
input_price: 0.1
|
||||||
output_price: 0.28
|
output_price: 0.3
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: mistralai/codestral-2508
|
- name: mistralai/codestral-2508
|
||||||
max_input_tokens: 256000
|
max_input_tokens: 256000
|
||||||
@@ -1735,6 +1835,7 @@
|
|||||||
output_price: 0.9
|
output_price: 0.9
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: ai21/jamba-large-1.7
|
- name: ai21/jamba-large-1.7
|
||||||
|
max_output_tokens: 4096
|
||||||
max_input_tokens: 256000
|
max_input_tokens: 256000
|
||||||
input_price: 2
|
input_price: 2
|
||||||
output_price: 8
|
output_price: 8
|
||||||
@@ -1745,110 +1846,121 @@
|
|||||||
output_price: 0.4
|
output_price: 0.4
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: cohere/command-a
|
- name: cohere/command-a
|
||||||
|
max_output_tokens: 8192
|
||||||
max_input_tokens: 256000
|
max_input_tokens: 256000
|
||||||
input_price: 2.5
|
input_price: 2.5
|
||||||
output_price: 10
|
output_price: 10
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: cohere/command-r7b-12-2024
|
- name: cohere/command-r7b-12-2024
|
||||||
max_input_tokens: 128000
|
max_input_tokens: 128000
|
||||||
max_output_tokens: 4096
|
max_output_tokens: 4000
|
||||||
input_price: 0.0375
|
input_price: 0.0375
|
||||||
output_price: 0.15
|
output_price: 0.15
|
||||||
- name: deepseek/deepseek-v3.2-exp
|
- name: deepseek/deepseek-v3.2-exp
|
||||||
|
max_output_tokens: 65536
|
||||||
max_input_tokens: 163840
|
max_input_tokens: 163840
|
||||||
input_price: 0.27
|
input_price: 0.27
|
||||||
output_price: 0.40
|
output_price: 0.41
|
||||||
- name: deepseek/deepseek-v3.1-terminus
|
- name: deepseek/deepseek-v3.1-terminus
|
||||||
max_input_tokens: 163840
|
max_input_tokens: 163840
|
||||||
input_price: 0.23
|
input_price: 0.21
|
||||||
output_price: 0.90
|
output_price: 0.79
|
||||||
- name: deepseek/deepseek-chat-v3.1
|
- name: deepseek/deepseek-chat-v3.1
|
||||||
max_input_tokens: 163840
|
max_output_tokens: 7168
|
||||||
input_price: 0.2
|
max_input_tokens: 32768
|
||||||
output_price: 0.8
|
input_price: 0.15
|
||||||
|
output_price: 0.75
|
||||||
- name: deepseek/deepseek-r1-0528
|
- name: deepseek/deepseek-r1-0528
|
||||||
max_input_tokens: 128000
|
max_output_tokens: 65536
|
||||||
input_price: 0.50
|
max_input_tokens: 163840
|
||||||
output_price: 2.15
|
input_price: 0.4
|
||||||
|
output_price: 1.75
|
||||||
patch:
|
patch:
|
||||||
body:
|
body:
|
||||||
include_reasoning: true
|
include_reasoning: true
|
||||||
- name: qwen/qwen3-max
|
- name: qwen/qwen3-max
|
||||||
|
max_output_tokens: 32768
|
||||||
max_input_tokens: 262144
|
max_input_tokens: 262144
|
||||||
input_price: 1.2
|
input_price: 1.2
|
||||||
output_price: 6
|
output_price: 6
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: qwen/qwen-plus
|
- name: qwen/qwen-plus
|
||||||
max_input_tokens: 131072
|
max_input_tokens: 1000000
|
||||||
max_output_tokens: 8192
|
max_output_tokens: 32768
|
||||||
input_price: 0.4
|
input_price: 0.4
|
||||||
output_price: 1.2
|
output_price: 1.2
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: qwen/qwen3-next-80b-a3b-instruct
|
- name: qwen/qwen3-next-80b-a3b-instruct
|
||||||
max_input_tokens: 262144
|
max_input_tokens: 262144
|
||||||
input_price: 0.1
|
input_price: 0.09
|
||||||
output_price: 0.8
|
output_price: 1.1
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: qwen/qwen3-next-80b-a3b-thinking
|
- name: qwen/qwen3-next-80b-a3b-thinking
|
||||||
max_input_tokens: 262144
|
max_input_tokens: 128000
|
||||||
input_price: 0.1
|
input_price: 0.15
|
||||||
output_price: 0.8
|
output_price: 1.2
|
||||||
- name: qwen/qwen5-235b-a22b-2507 # Qwen3 235B A22B Instruct 2507
|
- name: qwen/qwen5-235b-a22b-2507 # Qwen3 235B A22B Instruct 2507
|
||||||
max_input_tokens: 262144
|
max_input_tokens: 262144
|
||||||
input_price: 0.12
|
input_price: 0.12
|
||||||
output_price: 0.59
|
output_price: 0.59
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: qwen/qwen3-235b-a22b-thinking-2507
|
- name: qwen/qwen3-235b-a22b-thinking-2507
|
||||||
max_input_tokens: 262144
|
|
||||||
input_price: 0.118
|
|
||||||
output_price: 0.118
|
|
||||||
- name: qwen/qwen3-30b-a3b-instruct-2507
|
|
||||||
max_input_tokens: 131072
|
max_input_tokens: 131072
|
||||||
input_price: 0.2
|
input_price: 0
|
||||||
output_price: 0.8
|
output_price: 0
|
||||||
|
- name: qwen/qwen3-30b-a3b-instruct-2507
|
||||||
|
max_output_tokens: 262144
|
||||||
|
max_input_tokens: 262144
|
||||||
|
input_price: 0.09
|
||||||
|
output_price: 0.3
|
||||||
- name: qwen/qwen3-30b-a3b-thinking-2507
|
- name: qwen/qwen3-30b-a3b-thinking-2507
|
||||||
max_input_tokens: 262144
|
max_input_tokens: 32768
|
||||||
input_price: 0.071
|
input_price: 0.051
|
||||||
output_price: 0.285
|
output_price: 0.34
|
||||||
- name: qwen/qwen3-vl-32b-instruct
|
- name: qwen/qwen3-vl-32b-instruct
|
||||||
max_input_tokens: 262144
|
max_output_tokens: 32768
|
||||||
input_price: 0.35
|
max_input_tokens: 131072
|
||||||
output_price: 1.1
|
input_price: 0.104
|
||||||
|
output_price: 0.416
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
- name: qwen/qwen3-vl-8b-instruct
|
- name: qwen/qwen3-vl-8b-instruct
|
||||||
max_input_tokens: 262144
|
max_output_tokens: 32768
|
||||||
|
max_input_tokens: 131072
|
||||||
input_price: 0.08
|
input_price: 0.08
|
||||||
output_price: 0.50
|
output_price: 0.5
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
- name: qwen/qwen3-coder-plus
|
- name: qwen/qwen3-coder-plus
|
||||||
max_input_tokens: 128000
|
max_output_tokens: 65536
|
||||||
|
max_input_tokens: 1000000
|
||||||
input_price: 1
|
input_price: 1
|
||||||
output_price: 5
|
output_price: 5
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: qwen/qwen3-coder-flash
|
- name: qwen/qwen3-coder-flash
|
||||||
max_input_tokens: 128000
|
max_output_tokens: 65536
|
||||||
|
max_input_tokens: 1000000
|
||||||
input_price: 0.3
|
input_price: 0.3
|
||||||
output_price: 1.5
|
output_price: 1.5
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: qwen/qwen3-coder # Qwen3 Coder 480B A35B
|
- name: qwen/qwen3-coder # Qwen3 Coder 480B A35B
|
||||||
max_input_tokens: 262144
|
max_input_tokens: 262144
|
||||||
input_price: 0.22
|
input_price: 0.22
|
||||||
output_price: 0.95
|
output_price: 0.95
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: qwen/qwen3-coder-30b-a3b-instruct
|
- name: qwen/qwen3-coder-30b-a3b-instruct
|
||||||
max_input_tokens: 262144
|
max_output_tokens: 32768
|
||||||
input_price: 0.052
|
max_input_tokens: 160000
|
||||||
output_price: 0.207
|
input_price: 0.07
|
||||||
|
output_price: 0.27
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: moonshotai/kimi-k2-0905
|
- name: moonshotai/kimi-k2-0905
|
||||||
max_input_tokens: 262144
|
max_input_tokens: 131072
|
||||||
input_price: 0.296
|
input_price: 0.4
|
||||||
output_price: 1.185
|
output_price: 2
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: moonshotai/kimi-k2-thinking
|
- name: moonshotai/kimi-k2-thinking
|
||||||
max_input_tokens: 262144
|
max_input_tokens: 131072
|
||||||
input_price: 0.45
|
input_price: 0.47
|
||||||
output_price: 2.35
|
output_price: 2
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: moonshotai/kimi-dev-72b
|
- name: moonshotai/kimi-dev-72b
|
||||||
max_input_tokens: 131072
|
max_input_tokens: 131072
|
||||||
@@ -1856,21 +1968,26 @@
|
|||||||
output_price: 1.15
|
output_price: 1.15
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: x-ai/grok-4
|
- name: x-ai/grok-4
|
||||||
|
supports_vision: true
|
||||||
max_input_tokens: 256000
|
max_input_tokens: 256000
|
||||||
input_price: 3
|
input_price: 3
|
||||||
output_price: 15
|
output_price: 15
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: x-ai/grok-4-fast
|
- name: x-ai/grok-4-fast
|
||||||
|
max_output_tokens: 30000
|
||||||
|
supports_vision: true
|
||||||
max_input_tokens: 2000000
|
max_input_tokens: 2000000
|
||||||
input_price: 0.2
|
input_price: 0.2
|
||||||
output_price: 0.5
|
output_price: 0.5
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: x-ai/grok-code-fast-1
|
- name: x-ai/grok-code-fast-1
|
||||||
|
max_output_tokens: 10000
|
||||||
max_input_tokens: 256000
|
max_input_tokens: 256000
|
||||||
input_price: 0.2
|
input_price: 0.2
|
||||||
output_price: 1.5
|
output_price: 1.5
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: amazon/nova-premier-v1
|
- name: amazon/nova-premier-v1
|
||||||
|
max_output_tokens: 32000
|
||||||
max_input_tokens: 1000000
|
max_input_tokens: 1000000
|
||||||
input_price: 2.5
|
input_price: 2.5
|
||||||
output_price: 12.5
|
output_price: 12.5
|
||||||
@@ -1893,14 +2010,18 @@
|
|||||||
input_price: 0.035
|
input_price: 0.035
|
||||||
output_price: 0.14
|
output_price: 0.14
|
||||||
- name: perplexity/sonar-pro
|
- name: perplexity/sonar-pro
|
||||||
|
max_output_tokens: 8000
|
||||||
|
supports_vision: true
|
||||||
max_input_tokens: 200000
|
max_input_tokens: 200000
|
||||||
input_price: 3
|
input_price: 3
|
||||||
output_price: 15
|
output_price: 15
|
||||||
- name: perplexity/sonar
|
- name: perplexity/sonar
|
||||||
|
supports_vision: true
|
||||||
max_input_tokens: 127072
|
max_input_tokens: 127072
|
||||||
input_price: 1
|
input_price: 1
|
||||||
output_price: 1
|
output_price: 1
|
||||||
- name: perplexity/sonar-reasoning-pro
|
- name: perplexity/sonar-reasoning-pro
|
||||||
|
supports_vision: true
|
||||||
max_input_tokens: 128000
|
max_input_tokens: 128000
|
||||||
input_price: 2
|
input_price: 2
|
||||||
output_price: 8
|
output_price: 8
|
||||||
@@ -1915,20 +2036,22 @@
|
|||||||
body:
|
body:
|
||||||
include_reasoning: true
|
include_reasoning: true
|
||||||
- name: perplexity/sonar-deep-research
|
- name: perplexity/sonar-deep-research
|
||||||
max_input_tokens: 200000
|
max_input_tokens: 128000
|
||||||
input_price: 2
|
input_price: 2
|
||||||
output_price: 8
|
output_price: 8
|
||||||
patch:
|
patch:
|
||||||
body:
|
body:
|
||||||
include_reasoning: true
|
include_reasoning: true
|
||||||
- name: minimax/minimax-m2
|
- name: minimax/minimax-m2
|
||||||
|
max_output_tokens: 65536
|
||||||
max_input_tokens: 196608
|
max_input_tokens: 196608
|
||||||
input_price: 0.15
|
input_price: 0.255
|
||||||
output_price: 0.45
|
output_price: 1
|
||||||
- name: z-ai/glm-4.6
|
- name: z-ai/glm-4.6
|
||||||
|
max_output_tokens: 131072
|
||||||
max_input_tokens: 202752
|
max_input_tokens: 202752
|
||||||
input_price: 0.5
|
input_price: 0.35
|
||||||
output_price: 1.75
|
output_price: 1.71
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
|
|
||||||
# Links:
|
# Links:
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
requests
|
||||||
|
ruamel.yaml
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
import requests
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Provider mapping from models.yaml to OpenRouter prefixes
|
||||||
|
PROVIDER_MAPPING = {
|
||||||
|
"openai": "openai",
|
||||||
|
"claude": "anthropic",
|
||||||
|
"gemini": "google",
|
||||||
|
"mistral": "mistralai",
|
||||||
|
"cohere": "cohere",
|
||||||
|
"perplexity": "perplexity",
|
||||||
|
"xai": "x-ai",
|
||||||
|
"openrouter": "openrouter",
|
||||||
|
"ai21": "ai21",
|
||||||
|
"deepseek": "deepseek",
|
||||||
|
"moonshot": "moonshotai",
|
||||||
|
"qianwen": "qwen",
|
||||||
|
"zhipuai": "zhipuai",
|
||||||
|
"minimax": "minimax",
|
||||||
|
"vertexai": "google",
|
||||||
|
"groq": "groq",
|
||||||
|
"bedrock": "amazon",
|
||||||
|
"hunyuan": "tencent",
|
||||||
|
"ernie": "baidu",
|
||||||
|
"github": "github",
|
||||||
|
}
|
||||||
|
|
||||||
|
def fetch_openrouter_models():
|
||||||
|
print("Fetching models from OpenRouter...")
|
||||||
|
try:
|
||||||
|
response = requests.get("https://openrouter.ai/api/v1/models")
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()["data"]
|
||||||
|
print(f"Fetched {len(data)} models.")
|
||||||
|
return data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching models: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def get_openrouter_model(models_data, provider_prefix, model_name, is_openrouter_provider=False):
|
||||||
|
if is_openrouter_provider:
|
||||||
|
# For openrouter provider, the model_name in yaml is usually the full ID
|
||||||
|
for model in models_data:
|
||||||
|
if model["id"] == model_name:
|
||||||
|
return model
|
||||||
|
return None
|
||||||
|
|
||||||
|
expected_id = f"{provider_prefix}/{model_name}"
|
||||||
|
|
||||||
|
# 1. Try exact match on ID
|
||||||
|
for model in models_data:
|
||||||
|
if model["id"] == expected_id:
|
||||||
|
return model
|
||||||
|
|
||||||
|
# 2. Try match by suffix
|
||||||
|
for model in models_data:
|
||||||
|
if model["id"].split("/")[-1] == model_name:
|
||||||
|
if model["id"].startswith(f"{provider_prefix}/"):
|
||||||
|
return model
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def format_price(price_per_token):
|
||||||
|
if price_per_token is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
price_per_1m = float(price_per_token) * 1_000_000
|
||||||
|
if price_per_1m.is_integer():
|
||||||
|
return str(int(price_per_1m))
|
||||||
|
else:
|
||||||
|
return str(round(price_per_1m, 4))
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_indentation(line):
|
||||||
|
return len(line) - len(line.lstrip())
|
||||||
|
|
||||||
|
def process_model_block(block_lines, current_provider, or_models):
|
||||||
|
if not block_lines:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# 1. Identify model name and indentation
|
||||||
|
name_line = block_lines[0]
|
||||||
|
name_match = re.match(r"^(\s*)-\s*name:\s*(.+)$", name_line)
|
||||||
|
if not name_match:
|
||||||
|
return block_lines
|
||||||
|
|
||||||
|
name_indent_str = name_match.group(1)
|
||||||
|
model_name = name_match.group(2).strip()
|
||||||
|
|
||||||
|
# 2. Find OpenRouter model
|
||||||
|
or_prefix = PROVIDER_MAPPING.get(current_provider)
|
||||||
|
is_openrouter_provider = (current_provider == "openrouter")
|
||||||
|
|
||||||
|
if not or_prefix and not is_openrouter_provider:
|
||||||
|
return block_lines
|
||||||
|
|
||||||
|
or_model = get_openrouter_model(or_models, or_prefix, model_name, is_openrouter_provider)
|
||||||
|
if not or_model:
|
||||||
|
return block_lines
|
||||||
|
|
||||||
|
print(f" Updating {model_name}...")
|
||||||
|
|
||||||
|
# 3. Prepare updates
|
||||||
|
updates = {}
|
||||||
|
|
||||||
|
# Pricing
|
||||||
|
pricing = or_model.get("pricing", {})
|
||||||
|
p_in = format_price(pricing.get("prompt"))
|
||||||
|
p_out = format_price(pricing.get("completion"))
|
||||||
|
if p_in: updates["input_price"] = p_in
|
||||||
|
if p_out: updates["output_price"] = p_out
|
||||||
|
|
||||||
|
# Context
|
||||||
|
ctx = or_model.get("context_length")
|
||||||
|
if ctx: updates["max_input_tokens"] = str(ctx)
|
||||||
|
|
||||||
|
max_out = None
|
||||||
|
if "top_provider" in or_model and or_model["top_provider"]:
|
||||||
|
max_out = or_model["top_provider"].get("max_completion_tokens")
|
||||||
|
if max_out: updates["max_output_tokens"] = str(max_out)
|
||||||
|
|
||||||
|
# Capabilities
|
||||||
|
arch = or_model.get("architecture", {})
|
||||||
|
modality = arch.get("modality", "")
|
||||||
|
if "image" in modality:
|
||||||
|
updates["supports_vision"] = "true"
|
||||||
|
|
||||||
|
# 4. Detect field indentation
|
||||||
|
field_indent_str = None
|
||||||
|
existing_fields = {} # key -> line_index
|
||||||
|
|
||||||
|
for i, line in enumerate(block_lines):
|
||||||
|
if i == 0: continue # Skip name line
|
||||||
|
|
||||||
|
# Skip comments
|
||||||
|
if line.strip().startswith("#"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Look for "key: value"
|
||||||
|
m = re.match(r"^(\s*)([\w_-]+):", line)
|
||||||
|
if m:
|
||||||
|
indent = m.group(1)
|
||||||
|
key = m.group(2)
|
||||||
|
# Must be deeper than name line
|
||||||
|
if len(indent) > len(name_indent_str):
|
||||||
|
if field_indent_str is None:
|
||||||
|
field_indent_str = indent
|
||||||
|
existing_fields[key] = i
|
||||||
|
|
||||||
|
if field_indent_str is None:
|
||||||
|
field_indent_str = name_indent_str + " "
|
||||||
|
|
||||||
|
# 5. Apply updates
|
||||||
|
new_block = list(block_lines)
|
||||||
|
|
||||||
|
# Update existing fields
|
||||||
|
for key, value in updates.items():
|
||||||
|
if key in existing_fields:
|
||||||
|
idx = existing_fields[key]
|
||||||
|
# Preserve original key indentation exactly
|
||||||
|
original_line = new_block[idx]
|
||||||
|
m = re.match(r"^(\s*)([\w_-]+):", original_line)
|
||||||
|
if m:
|
||||||
|
current_indent = m.group(1)
|
||||||
|
new_block[idx] = f"{current_indent}{key}: {value}\n"
|
||||||
|
|
||||||
|
# Insert missing fields
|
||||||
|
# Insert after the name line
|
||||||
|
insertion_idx = 1
|
||||||
|
|
||||||
|
for key, value in updates.items():
|
||||||
|
if key not in existing_fields:
|
||||||
|
new_line = f"{field_indent_str}{key}: {value}\n"
|
||||||
|
new_block.insert(insertion_idx, new_line)
|
||||||
|
insertion_idx += 1
|
||||||
|
|
||||||
|
return new_block
|
||||||
|
|
||||||
|
def main():
|
||||||
|
or_models = fetch_openrouter_models()
|
||||||
|
|
||||||
|
print("Reading models.yaml...")
|
||||||
|
with open("models.yaml", "r") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
new_lines = []
|
||||||
|
current_provider = None
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
while i < len(lines):
|
||||||
|
line = lines[i]
|
||||||
|
|
||||||
|
# Check for provider
|
||||||
|
# - provider: name
|
||||||
|
p_match = re.match(r"^\s*-?\s*provider:\s*(.+)$", line)
|
||||||
|
if p_match:
|
||||||
|
current_provider = p_match.group(1).strip()
|
||||||
|
new_lines.append(line)
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check for model start
|
||||||
|
# - name: ...
|
||||||
|
m_match = re.match(r"^(\s*)-\s*name:\s*.+$", line)
|
||||||
|
if m_match:
|
||||||
|
# Start of a model block
|
||||||
|
start_indent = len(m_match.group(1))
|
||||||
|
|
||||||
|
# Collect block lines
|
||||||
|
block_lines = [line]
|
||||||
|
j = i + 1
|
||||||
|
while j < len(lines):
|
||||||
|
next_line = lines[j]
|
||||||
|
stripped = next_line.strip()
|
||||||
|
|
||||||
|
# If empty or comment, include it
|
||||||
|
if not stripped or stripped.startswith("#"):
|
||||||
|
block_lines.append(next_line)
|
||||||
|
j += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check indentation
|
||||||
|
next_indent = get_indentation(next_line)
|
||||||
|
|
||||||
|
# If indentation is greater, it's part of the block (property)
|
||||||
|
if next_indent > start_indent:
|
||||||
|
block_lines.append(next_line)
|
||||||
|
j += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If indentation is equal or less, it's the end of the block
|
||||||
|
break
|
||||||
|
|
||||||
|
# Process the block
|
||||||
|
processed_block = process_model_block(block_lines, current_provider, or_models)
|
||||||
|
new_lines.extend(processed_block)
|
||||||
|
|
||||||
|
# Advance i
|
||||||
|
i = j
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Otherwise, just a regular line
|
||||||
|
new_lines.append(line)
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
print("Saving models.yaml...")
|
||||||
|
with open("models.yaml", "w") as f:
|
||||||
|
f.writelines(new_lines)
|
||||||
|
print("Done.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -127,6 +127,9 @@ pub struct Cli {
|
|||||||
/// List all secrets stored in the Loki vault
|
/// List all secrets stored in the Loki vault
|
||||||
#[arg(long, exclusive = true)]
|
#[arg(long, exclusive = true)]
|
||||||
pub list_secrets: bool,
|
pub list_secrets: bool,
|
||||||
|
/// Authenticate with an LLM provider using OAuth (e.g., --authenticate client_name)
|
||||||
|
#[arg(long, exclusive = true, value_name = "CLIENT_NAME")]
|
||||||
|
pub authenticate: Option<Option<String>>,
|
||||||
/// Generate static shell completion scripts
|
/// Generate static shell completion scripts
|
||||||
#[arg(long, value_name = "SHELL", value_enum)]
|
#[arg(long, value_name = "SHELL", value_enum)]
|
||||||
pub completions: Option<ShellCompletion>,
|
pub completions: Option<ShellCompletion>,
|
||||||
|
|||||||
@@ -19,15 +19,15 @@ impl AzureOpenAIClient {
|
|||||||
config_get_fn!(api_base, get_api_base);
|
config_get_fn!(api_base, get_api_base);
|
||||||
config_get_fn!(api_key, get_api_key);
|
config_get_fn!(api_key, get_api_key);
|
||||||
|
|
||||||
pub const PROMPTS: [PromptAction<'static>; 2] = [
|
create_client_config!([
|
||||||
(
|
(
|
||||||
"api_base",
|
"api_base",
|
||||||
"API Base",
|
"API Base",
|
||||||
Some("e.g. https://{RESOURCE}.openai.azure.com"),
|
Some("e.g. https://{RESOURCE}.openai.azure.com"),
|
||||||
false
|
false,
|
||||||
),
|
),
|
||||||
("api_key", "API Key", None, true),
|
("api_key", "API Key", None, true),
|
||||||
];
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_client_trait!(
|
impl_client_trait!(
|
||||||
|
|||||||
@@ -32,11 +32,11 @@ impl BedrockClient {
|
|||||||
config_get_fn!(region, get_region);
|
config_get_fn!(region, get_region);
|
||||||
config_get_fn!(session_token, get_session_token);
|
config_get_fn!(session_token, get_session_token);
|
||||||
|
|
||||||
pub const PROMPTS: [PromptAction<'static>; 3] = [
|
create_client_config!([
|
||||||
("access_key_id", "AWS Access Key ID", None, true),
|
("access_key_id", "AWS Access Key ID", None, true),
|
||||||
("secret_access_key", "AWS Secret Access Key", None, true),
|
("secret_access_key", "AWS Secret Access Key", None, true),
|
||||||
("region", "AWS Region", None, false),
|
("region", "AWS Region", None, false),
|
||||||
];
|
]);
|
||||||
|
|
||||||
fn chat_completions_builder(
|
fn chat_completions_builder(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
+103
-15
@@ -1,19 +1,24 @@
|
|||||||
|
use super::access_token::get_access_token;
|
||||||
|
use super::claude_oauth::ClaudeOAuthProvider;
|
||||||
|
use super::oauth::{self, OAuthProvider};
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use crate::utils::strip_think_tag;
|
use crate::utils::strip_think_tag;
|
||||||
|
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use reqwest::RequestBuilder;
|
use reqwest::{Client as ReqwestClient, RequestBuilder};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
const API_BASE: &str = "https://api.anthropic.com/v1";
|
const API_BASE: &str = "https://api.anthropic.com/v1";
|
||||||
|
const CLAUDE_CODE_PREFIX: &str = "You are Claude Code, Anthropic's official CLI for Claude.";
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct ClaudeConfig {
|
pub struct ClaudeConfig {
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub api_key: Option<String>,
|
pub api_key: Option<String>,
|
||||||
pub api_base: Option<String>,
|
pub api_base: Option<String>,
|
||||||
|
pub auth: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub models: Vec<ModelData>,
|
pub models: Vec<ModelData>,
|
||||||
pub patch: Option<RequestPatch>,
|
pub patch: Option<RequestPatch>,
|
||||||
@@ -24,25 +29,44 @@ impl ClaudeClient {
|
|||||||
config_get_fn!(api_key, get_api_key);
|
config_get_fn!(api_key, get_api_key);
|
||||||
config_get_fn!(api_base, get_api_base);
|
config_get_fn!(api_base, get_api_base);
|
||||||
|
|
||||||
pub const PROMPTS: [PromptAction<'static>; 1] = [("api_key", "API Key", None, true)];
|
create_oauth_supported_client_config!();
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_client_trait!(
|
#[async_trait::async_trait]
|
||||||
ClaudeClient,
|
impl Client for ClaudeClient {
|
||||||
(
|
client_common_fns!();
|
||||||
prepare_chat_completions,
|
|
||||||
claude_chat_completions,
|
|
||||||
claude_chat_completions_streaming
|
|
||||||
),
|
|
||||||
(noop_prepare_embeddings, noop_embeddings),
|
|
||||||
(noop_prepare_rerank, noop_rerank),
|
|
||||||
);
|
|
||||||
|
|
||||||
fn prepare_chat_completions(
|
fn supports_oauth(&self) -> bool {
|
||||||
|
self.config.auth.as_deref() == Some("oauth")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn chat_completions_inner(
|
||||||
|
&self,
|
||||||
|
client: &ReqwestClient,
|
||||||
|
data: ChatCompletionsData,
|
||||||
|
) -> Result<ChatCompletionsOutput> {
|
||||||
|
let request_data = prepare_chat_completions(self, client, data).await?;
|
||||||
|
let builder = self.request_builder(client, request_data);
|
||||||
|
claude_chat_completions(builder, self.model()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn chat_completions_streaming_inner(
|
||||||
|
&self,
|
||||||
|
client: &ReqwestClient,
|
||||||
|
handler: &mut SseHandler,
|
||||||
|
data: ChatCompletionsData,
|
||||||
|
) -> Result<()> {
|
||||||
|
let request_data = prepare_chat_completions(self, client, data).await?;
|
||||||
|
let builder = self.request_builder(client, request_data);
|
||||||
|
claude_chat_completions_streaming(builder, handler, self.model()).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn prepare_chat_completions(
|
||||||
self_: &ClaudeClient,
|
self_: &ClaudeClient,
|
||||||
|
client: &ReqwestClient,
|
||||||
data: ChatCompletionsData,
|
data: ChatCompletionsData,
|
||||||
) -> Result<RequestData> {
|
) -> Result<RequestData> {
|
||||||
let api_key = self_.get_api_key()?;
|
|
||||||
let api_base = self_
|
let api_base = self_
|
||||||
.get_api_base()
|
.get_api_base()
|
||||||
.unwrap_or_else(|_| API_BASE.to_string());
|
.unwrap_or_else(|_| API_BASE.to_string());
|
||||||
@@ -53,11 +77,75 @@ fn prepare_chat_completions(
|
|||||||
let mut request_data = RequestData::new(url, body);
|
let mut request_data = RequestData::new(url, body);
|
||||||
|
|
||||||
request_data.header("anthropic-version", "2023-06-01");
|
request_data.header("anthropic-version", "2023-06-01");
|
||||||
request_data.header("x-api-key", api_key);
|
|
||||||
|
let uses_oauth = self_.config.auth.as_deref() == Some("oauth");
|
||||||
|
|
||||||
|
if uses_oauth {
|
||||||
|
let provider = ClaudeOAuthProvider;
|
||||||
|
let ready = oauth::prepare_oauth_access_token(client, &provider, self_.name()).await?;
|
||||||
|
if !ready {
|
||||||
|
bail!(
|
||||||
|
"OAuth configured but no tokens found for '{}'. Run: 'loki --authenticate {}' or '.authenticate' in the REPL",
|
||||||
|
self_.name(),
|
||||||
|
self_.name()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let token = get_access_token(self_.name())?;
|
||||||
|
request_data.bearer_auth(token);
|
||||||
|
for (key, value) in provider.extra_request_headers() {
|
||||||
|
request_data.header(key, value);
|
||||||
|
}
|
||||||
|
inject_oauth_system_prompt(&mut request_data.body);
|
||||||
|
} else if let Ok(api_key) = self_.get_api_key() {
|
||||||
|
request_data.header("x-api-key", api_key);
|
||||||
|
} else {
|
||||||
|
bail!(
|
||||||
|
"No authentication configured for '{}'. Set `api_key` or use `auth: oauth` with `loki --authenticate {}`.",
|
||||||
|
self_.name(),
|
||||||
|
self_.name()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(request_data)
|
Ok(request_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Anthropic requires OAuth-authenticated requests to include a Claude Code
|
||||||
|
/// system prompt prefix in order to consider a request body as "valid".
|
||||||
|
///
|
||||||
|
/// This behavior was discovered 2026-03-17.
|
||||||
|
///
|
||||||
|
/// So this function injects the Claude Code system prompt into the request
|
||||||
|
/// body to make it a valid request.
|
||||||
|
fn inject_oauth_system_prompt(body: &mut Value) {
|
||||||
|
let prefix_block = json!({
|
||||||
|
"type": "text",
|
||||||
|
"text": CLAUDE_CODE_PREFIX,
|
||||||
|
});
|
||||||
|
|
||||||
|
match body.get("system") {
|
||||||
|
Some(Value::String(existing)) => {
|
||||||
|
let existing_block = json!({
|
||||||
|
"type": "text",
|
||||||
|
"text": existing,
|
||||||
|
});
|
||||||
|
body["system"] = json!([prefix_block, existing_block]);
|
||||||
|
}
|
||||||
|
Some(Value::Array(_)) => {
|
||||||
|
if let Some(arr) = body["system"].as_array_mut() {
|
||||||
|
let already_injected = arr
|
||||||
|
.iter()
|
||||||
|
.any(|block| block["text"].as_str() == Some(CLAUDE_CODE_PREFIX));
|
||||||
|
if !already_injected {
|
||||||
|
arr.insert(0, prefix_block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
body["system"] = json!([prefix_block]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn claude_chat_completions(
|
pub async fn claude_chat_completions(
|
||||||
builder: RequestBuilder,
|
builder: RequestBuilder,
|
||||||
_model: &Model,
|
_model: &Model,
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
use super::oauth::OAuthProvider;
|
||||||
|
|
||||||
|
pub const BETA_HEADER: &str = "oauth-2025-04-20";
|
||||||
|
|
||||||
|
pub struct ClaudeOAuthProvider;
|
||||||
|
|
||||||
|
impl OAuthProvider for ClaudeOAuthProvider {
|
||||||
|
fn provider_name(&self) -> &str {
|
||||||
|
"claude"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn client_id(&self) -> &str {
|
||||||
|
"9d1c250a-e61b-44d9-88ed-5944d1962f5e"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn authorize_url(&self) -> &str {
|
||||||
|
"https://claude.ai/oauth/authorize"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn token_url(&self) -> &str {
|
||||||
|
"https://console.anthropic.com/v1/oauth/token"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn redirect_uri(&self) -> &str {
|
||||||
|
"https://console.anthropic.com/oauth/code/callback"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scopes(&self) -> &str {
|
||||||
|
"org:create_api_key user:profile user:inference"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extra_authorize_params(&self) -> Vec<(&str, &str)> {
|
||||||
|
vec![("code", "true")]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extra_token_headers(&self) -> Vec<(&str, &str)> {
|
||||||
|
vec![("anthropic-beta", BETA_HEADER)]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extra_request_headers(&self) -> Vec<(&str, &str)> {
|
||||||
|
vec![("anthropic-beta", BETA_HEADER)]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ impl CohereClient {
|
|||||||
config_get_fn!(api_key, get_api_key);
|
config_get_fn!(api_key, get_api_key);
|
||||||
config_get_fn!(api_base, get_api_base);
|
config_get_fn!(api_base, get_api_base);
|
||||||
|
|
||||||
pub const PROMPTS: [PromptAction<'static>; 1] = [("api_key", "API Key", None, true)];
|
create_client_config!([("api_key", "API Key", None, true)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_client_trait!(
|
impl_client_trait!(
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ pub trait Client: Sync + Send {
|
|||||||
|
|
||||||
fn model(&self) -> &Model;
|
fn model(&self) -> &Model;
|
||||||
|
|
||||||
|
fn supports_oauth(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
fn build_client(&self) -> Result<ReqwestClient> {
|
fn build_client(&self) -> Result<ReqwestClient> {
|
||||||
let mut builder = ReqwestClient::builder();
|
let mut builder = ReqwestClient::builder();
|
||||||
let extra = self.extra_config();
|
let extra = self.extra_config();
|
||||||
@@ -489,14 +493,6 @@ pub async fn call_chat_completions_streaming(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn noop_prepare_embeddings<T>(_client: &T, _data: &EmbeddingsData) -> Result<RequestData> {
|
|
||||||
bail!("The client doesn't support embeddings api")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn noop_embeddings(_builder: RequestBuilder, _model: &Model) -> Result<EmbeddingsOutput> {
|
|
||||||
bail!("The client doesn't support embeddings api")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn noop_prepare_rerank<T>(_client: &T, _data: &RerankData) -> Result<RequestData> {
|
pub fn noop_prepare_rerank<T>(_client: &T, _data: &RerankData) -> Result<RequestData> {
|
||||||
bail!("The client doesn't support rerank api")
|
bail!("The client doesn't support rerank api")
|
||||||
}
|
}
|
||||||
@@ -554,7 +550,7 @@ pub fn json_str_from_map<'a>(
|
|||||||
map.get(field_name).and_then(|v| v.as_str())
|
map.get(field_name).and_then(|v| v.as_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_client_models_config(client_config: &mut Value, client: &str) -> Result<String> {
|
pub async fn set_client_models_config(client_config: &mut Value, client: &str) -> Result<String> {
|
||||||
if let Some(provider) = ALL_PROVIDER_MODELS.iter().find(|v| v.provider == client) {
|
if let Some(provider) = ALL_PROVIDER_MODELS.iter().find(|v| v.provider == client) {
|
||||||
let models: Vec<String> = provider
|
let models: Vec<String> = provider
|
||||||
.models
|
.models
|
||||||
|
|||||||
+120
-35
@@ -1,10 +1,13 @@
|
|||||||
|
use super::access_token::get_access_token;
|
||||||
|
use super::gemini_oauth::GeminiOAuthProvider;
|
||||||
|
use super::oauth;
|
||||||
use super::vertexai::*;
|
use super::vertexai::*;
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result, bail};
|
||||||
use reqwest::RequestBuilder;
|
use reqwest::{Client as ReqwestClient, RequestBuilder};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::{json, Value};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
const API_BASE: &str = "https://generativelanguage.googleapis.com/v1beta";
|
const API_BASE: &str = "https://generativelanguage.googleapis.com/v1beta";
|
||||||
|
|
||||||
@@ -13,6 +16,7 @@ pub struct GeminiConfig {
|
|||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub api_key: Option<String>,
|
pub api_key: Option<String>,
|
||||||
pub api_base: Option<String>,
|
pub api_base: Option<String>,
|
||||||
|
pub auth: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub models: Vec<ModelData>,
|
pub models: Vec<ModelData>,
|
||||||
pub patch: Option<RequestPatch>,
|
pub patch: Option<RequestPatch>,
|
||||||
@@ -23,25 +27,64 @@ impl GeminiClient {
|
|||||||
config_get_fn!(api_key, get_api_key);
|
config_get_fn!(api_key, get_api_key);
|
||||||
config_get_fn!(api_base, get_api_base);
|
config_get_fn!(api_base, get_api_base);
|
||||||
|
|
||||||
pub const PROMPTS: [PromptAction<'static>; 1] = [("api_key", "API Key", None, true)];
|
create_oauth_supported_client_config!();
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_client_trait!(
|
#[async_trait::async_trait]
|
||||||
GeminiClient,
|
impl Client for GeminiClient {
|
||||||
(
|
client_common_fns!();
|
||||||
prepare_chat_completions,
|
|
||||||
gemini_chat_completions,
|
|
||||||
gemini_chat_completions_streaming
|
|
||||||
),
|
|
||||||
(prepare_embeddings, embeddings),
|
|
||||||
(noop_prepare_rerank, noop_rerank),
|
|
||||||
);
|
|
||||||
|
|
||||||
fn prepare_chat_completions(
|
fn supports_oauth(&self) -> bool {
|
||||||
|
self.config.auth.as_deref() == Some("oauth")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn chat_completions_inner(
|
||||||
|
&self,
|
||||||
|
client: &ReqwestClient,
|
||||||
|
data: ChatCompletionsData,
|
||||||
|
) -> Result<ChatCompletionsOutput> {
|
||||||
|
let request_data = prepare_chat_completions(self, client, data).await?;
|
||||||
|
let builder = self.request_builder(client, request_data);
|
||||||
|
gemini_chat_completions(builder, self.model()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn chat_completions_streaming_inner(
|
||||||
|
&self,
|
||||||
|
client: &ReqwestClient,
|
||||||
|
handler: &mut SseHandler,
|
||||||
|
data: ChatCompletionsData,
|
||||||
|
) -> Result<()> {
|
||||||
|
let request_data = prepare_chat_completions(self, client, data).await?;
|
||||||
|
let builder = self.request_builder(client, request_data);
|
||||||
|
gemini_chat_completions_streaming(builder, handler, self.model()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn embeddings_inner(
|
||||||
|
&self,
|
||||||
|
client: &ReqwestClient,
|
||||||
|
data: &EmbeddingsData,
|
||||||
|
) -> Result<EmbeddingsOutput> {
|
||||||
|
let request_data = prepare_embeddings(self, client, data).await?;
|
||||||
|
let builder = self.request_builder(client, request_data);
|
||||||
|
embeddings(builder, self.model()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn rerank_inner(
|
||||||
|
&self,
|
||||||
|
client: &ReqwestClient,
|
||||||
|
data: &RerankData,
|
||||||
|
) -> Result<RerankOutput> {
|
||||||
|
let request_data = noop_prepare_rerank(self, data)?;
|
||||||
|
let builder = self.request_builder(client, request_data);
|
||||||
|
noop_rerank(builder, self.model()).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn prepare_chat_completions(
|
||||||
self_: &GeminiClient,
|
self_: &GeminiClient,
|
||||||
|
client: &ReqwestClient,
|
||||||
data: ChatCompletionsData,
|
data: ChatCompletionsData,
|
||||||
) -> Result<RequestData> {
|
) -> Result<RequestData> {
|
||||||
let api_key = self_.get_api_key()?;
|
|
||||||
let api_base = self_
|
let api_base = self_
|
||||||
.get_api_base()
|
.get_api_base()
|
||||||
.unwrap_or_else(|_| API_BASE.to_string());
|
.unwrap_or_else(|_| API_BASE.to_string());
|
||||||
@@ -59,26 +102,61 @@ fn prepare_chat_completions(
|
|||||||
);
|
);
|
||||||
|
|
||||||
let body = gemini_build_chat_completions_body(data, &self_.model)?;
|
let body = gemini_build_chat_completions_body(data, &self_.model)?;
|
||||||
|
|
||||||
let mut request_data = RequestData::new(url, body);
|
let mut request_data = RequestData::new(url, body);
|
||||||
|
|
||||||
request_data.header("x-goog-api-key", api_key);
|
let uses_oauth = self_.config.auth.as_deref() == Some("oauth");
|
||||||
|
|
||||||
|
if uses_oauth {
|
||||||
|
let provider = GeminiOAuthProvider;
|
||||||
|
let ready = oauth::prepare_oauth_access_token(client, &provider, self_.name()).await?;
|
||||||
|
if !ready {
|
||||||
|
bail!(
|
||||||
|
"OAuth configured but no tokens found for '{}'. Run: 'loki --authenticate {}' or '.authenticate' in the REPL",
|
||||||
|
self_.name(),
|
||||||
|
self_.name()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let token = get_access_token(self_.name())?;
|
||||||
|
request_data.bearer_auth(token);
|
||||||
|
} else if let Ok(api_key) = self_.get_api_key() {
|
||||||
|
request_data.header("x-goog-api-key", api_key);
|
||||||
|
} else {
|
||||||
|
bail!(
|
||||||
|
"No authentication configured for '{}'. Set `api_key` or use `auth: oauth` with `loki --authenticate {}`.",
|
||||||
|
self_.name(),
|
||||||
|
self_.name()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(request_data)
|
Ok(request_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_embeddings(self_: &GeminiClient, data: &EmbeddingsData) -> Result<RequestData> {
|
async fn prepare_embeddings(
|
||||||
let api_key = self_.get_api_key()?;
|
self_: &GeminiClient,
|
||||||
|
client: &ReqwestClient,
|
||||||
|
data: &EmbeddingsData,
|
||||||
|
) -> Result<RequestData> {
|
||||||
let api_base = self_
|
let api_base = self_
|
||||||
.get_api_base()
|
.get_api_base()
|
||||||
.unwrap_or_else(|_| API_BASE.to_string());
|
.unwrap_or_else(|_| API_BASE.to_string());
|
||||||
|
|
||||||
let url = format!(
|
let uses_oauth = self_.config.auth.as_deref() == Some("oauth");
|
||||||
"{}/models/{}:batchEmbedContents?key={}",
|
|
||||||
api_base.trim_end_matches('/'),
|
let url = if uses_oauth {
|
||||||
self_.model.real_name(),
|
format!(
|
||||||
api_key
|
"{}/models/{}:batchEmbedContents",
|
||||||
);
|
api_base.trim_end_matches('/'),
|
||||||
|
self_.model.real_name(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
let api_key = self_.get_api_key()?;
|
||||||
|
format!(
|
||||||
|
"{}/models/{}:batchEmbedContents?key={}",
|
||||||
|
api_base.trim_end_matches('/'),
|
||||||
|
self_.model.real_name(),
|
||||||
|
api_key
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
let model_id = format!("models/{}", self_.model.real_name());
|
let model_id = format!("models/{}", self_.model.real_name());
|
||||||
|
|
||||||
@@ -89,21 +167,28 @@ fn prepare_embeddings(self_: &GeminiClient, data: &EmbeddingsData) -> Result<Req
|
|||||||
json!({
|
json!({
|
||||||
"model": model_id,
|
"model": model_id,
|
||||||
"content": {
|
"content": {
|
||||||
"parts": [
|
"parts": [{ "text": text }]
|
||||||
{
|
|
||||||
"text": text
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let body = json!({
|
let body = json!({ "requests": requests });
|
||||||
"requests": requests,
|
let mut request_data = RequestData::new(url, body);
|
||||||
});
|
|
||||||
|
|
||||||
let request_data = RequestData::new(url, body);
|
if uses_oauth {
|
||||||
|
let provider = GeminiOAuthProvider;
|
||||||
|
let ready = oauth::prepare_oauth_access_token(client, &provider, self_.name()).await?;
|
||||||
|
if !ready {
|
||||||
|
bail!(
|
||||||
|
"OAuth configured but no tokens found for '{}'. Run: 'loki --authenticate {}' or '.authenticate' in the REPL",
|
||||||
|
self_.name(),
|
||||||
|
self_.name()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let token = get_access_token(self_.name())?;
|
||||||
|
request_data.bearer_auth(token);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(request_data)
|
Ok(request_data)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
use super::oauth::{OAuthProvider, TokenRequestFormat};
|
||||||
|
|
||||||
|
pub struct GeminiOAuthProvider;
|
||||||
|
|
||||||
|
const GEMINI_CLIENT_ID: &str =
|
||||||
|
"50826443741-upqcebrs4gctqht1f08ku46qlbirkdsj.apps.googleusercontent.com";
|
||||||
|
const GEMINI_CLIENT_SECRET: &str = "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";
|
||||||
|
|
||||||
|
impl OAuthProvider for GeminiOAuthProvider {
|
||||||
|
fn provider_name(&self) -> &str {
|
||||||
|
"gemini"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn client_id(&self) -> &str {
|
||||||
|
GEMINI_CLIENT_ID
|
||||||
|
}
|
||||||
|
|
||||||
|
fn authorize_url(&self) -> &str {
|
||||||
|
"https://accounts.google.com/o/oauth2/v2/auth"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn token_url(&self) -> &str {
|
||||||
|
"https://oauth2.googleapis.com/token"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn redirect_uri(&self) -> &str {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scopes(&self) -> &str {
|
||||||
|
"https://www.googleapis.com/auth/generative-language.peruserquota https://www.googleapis.com/auth/generative-language.retriever https://www.googleapis.com/auth/userinfo.email"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn client_secret(&self) -> Option<&str> {
|
||||||
|
Some(GEMINI_CLIENT_SECRET)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extra_authorize_params(&self) -> Vec<(&str, &str)> {
|
||||||
|
vec![("access_type", "offline"), ("prompt", "consent")]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn token_request_format(&self) -> TokenRequestFormat {
|
||||||
|
TokenRequestFormat::FormUrlEncoded
|
||||||
|
}
|
||||||
|
|
||||||
|
fn uses_localhost_redirect(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
+39
-1
@@ -90,7 +90,7 @@ macro_rules! register_client {
|
|||||||
pub async fn create_client_config(client: &str, vault: &$crate::vault::Vault) -> anyhow::Result<(String, serde_json::Value)> {
|
pub async fn create_client_config(client: &str, vault: &$crate::vault::Vault) -> anyhow::Result<(String, serde_json::Value)> {
|
||||||
$(
|
$(
|
||||||
if client == $client::NAME && client != $crate::client::OpenAICompatibleClient::NAME {
|
if client == $client::NAME && client != $crate::client::OpenAICompatibleClient::NAME {
|
||||||
return create_config(&$client::PROMPTS, $client::NAME, vault).await
|
return $client::create_client_config(vault).await
|
||||||
}
|
}
|
||||||
)+
|
)+
|
||||||
if let Some(ret) = create_openai_compatible_client_config(client).await? {
|
if let Some(ret) = create_openai_compatible_client_config(client).await? {
|
||||||
@@ -218,6 +218,44 @@ macro_rules! impl_client_trait {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! create_client_config {
|
||||||
|
($prompts:expr) => {
|
||||||
|
pub async fn create_client_config(
|
||||||
|
vault: &$crate::vault::Vault,
|
||||||
|
) -> anyhow::Result<(String, serde_json::Value)> {
|
||||||
|
$crate::client::create_config(&$prompts, Self::NAME, vault).await
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! create_oauth_supported_client_config {
|
||||||
|
() => {
|
||||||
|
pub async fn create_client_config(vault: &$crate::vault::Vault) -> anyhow::Result<(String, serde_json::Value)> {
|
||||||
|
let mut config = serde_json::json!({ "type": Self::NAME });
|
||||||
|
|
||||||
|
let auth_method = inquire::Select::new(
|
||||||
|
"Authentication method:",
|
||||||
|
vec!["API Key", "OAuth"],
|
||||||
|
)
|
||||||
|
.prompt()?;
|
||||||
|
|
||||||
|
if auth_method == "API Key" {
|
||||||
|
let env_name = format!("{}_API_KEY", Self::NAME).to_ascii_uppercase();
|
||||||
|
vault.add_secret(&env_name)?;
|
||||||
|
config["api_key"] = format!("{{{{{env_name}}}}}").into();
|
||||||
|
} else {
|
||||||
|
config["auth"] = "oauth".into();
|
||||||
|
}
|
||||||
|
|
||||||
|
let model = $crate::client::set_client_models_config(&mut config, Self::NAME).await?;
|
||||||
|
let clients = json!(vec![config]);
|
||||||
|
Ok((model, clients))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! config_get_fn {
|
macro_rules! config_get_fn {
|
||||||
($field_name:ident, $fn_name:ident) => {
|
($field_name:ident, $fn_name:ident) => {
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
mod access_token;
|
mod access_token;
|
||||||
|
mod claude_oauth;
|
||||||
mod common;
|
mod common;
|
||||||
|
mod gemini_oauth;
|
||||||
mod message;
|
mod message;
|
||||||
|
pub mod oauth;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod macros;
|
mod macros;
|
||||||
mod model;
|
mod model;
|
||||||
|
|||||||
@@ -177,6 +177,10 @@ impl Model {
|
|||||||
self.data.max_output_tokens
|
self.data.max_output_tokens
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn supports_function_calling(&self) -> bool {
|
||||||
|
self.data.supports_function_calling
|
||||||
|
}
|
||||||
|
|
||||||
pub fn no_stream(&self) -> bool {
|
pub fn no_stream(&self) -> bool {
|
||||||
self.data.no_stream
|
self.data.no_stream
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,429 @@
|
|||||||
|
use super::ClientConfig;
|
||||||
|
use super::access_token::{is_valid_access_token, set_access_token};
|
||||||
|
use crate::config::Config;
|
||||||
|
use anyhow::{Result, anyhow, bail};
|
||||||
|
use base64::Engine;
|
||||||
|
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||||
|
use chrono::Utc;
|
||||||
|
use inquire::Text;
|
||||||
|
use reqwest::{Client as ReqwestClient, RequestBuilder};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs;
|
||||||
|
use std::io::{BufRead, BufReader, Write};
|
||||||
|
use std::net::TcpListener;
|
||||||
|
use url::Url;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub enum TokenRequestFormat {
|
||||||
|
Json,
|
||||||
|
FormUrlEncoded,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait OAuthProvider: Send + Sync {
|
||||||
|
fn provider_name(&self) -> &str;
|
||||||
|
fn client_id(&self) -> &str;
|
||||||
|
fn authorize_url(&self) -> &str;
|
||||||
|
fn token_url(&self) -> &str;
|
||||||
|
fn redirect_uri(&self) -> &str;
|
||||||
|
fn scopes(&self) -> &str;
|
||||||
|
|
||||||
|
fn client_secret(&self) -> Option<&str> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extra_authorize_params(&self) -> Vec<(&str, &str)> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn token_request_format(&self) -> TokenRequestFormat {
|
||||||
|
TokenRequestFormat::Json
|
||||||
|
}
|
||||||
|
|
||||||
|
fn uses_localhost_redirect(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extra_token_headers(&self) -> Vec<(&str, &str)> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extra_request_headers(&self) -> Vec<(&str, &str)> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct OAuthTokens {
|
||||||
|
pub access_token: String,
|
||||||
|
pub refresh_token: String,
|
||||||
|
pub expires_at: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run_oauth_flow(provider: &dyn OAuthProvider, client_name: &str) -> Result<()> {
|
||||||
|
let random_bytes: [u8; 32] = rand::random::<[u8; 32]>();
|
||||||
|
let code_verifier = URL_SAFE_NO_PAD.encode(random_bytes);
|
||||||
|
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(code_verifier.as_bytes());
|
||||||
|
let code_challenge = URL_SAFE_NO_PAD.encode(hasher.finalize());
|
||||||
|
|
||||||
|
let state = Uuid::new_v4().to_string();
|
||||||
|
|
||||||
|
let redirect_uri = if provider.uses_localhost_redirect() {
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:0")?;
|
||||||
|
let port = listener.local_addr()?.port();
|
||||||
|
let uri = format!("http://127.0.0.1:{port}/callback");
|
||||||
|
drop(listener);
|
||||||
|
uri
|
||||||
|
} else {
|
||||||
|
provider.redirect_uri().to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let encoded_scopes = urlencoding::encode(provider.scopes());
|
||||||
|
let encoded_redirect = urlencoding::encode(&redirect_uri);
|
||||||
|
|
||||||
|
let mut authorize_url = format!(
|
||||||
|
"{}?client_id={}&response_type=code&scope={}&redirect_uri={}&code_challenge={}&code_challenge_method=S256&state={}",
|
||||||
|
provider.authorize_url(),
|
||||||
|
provider.client_id(),
|
||||||
|
encoded_scopes,
|
||||||
|
encoded_redirect,
|
||||||
|
code_challenge,
|
||||||
|
state
|
||||||
|
);
|
||||||
|
|
||||||
|
for (key, value) in provider.extra_authorize_params() {
|
||||||
|
authorize_url.push_str(&format!(
|
||||||
|
"&{}={}",
|
||||||
|
urlencoding::encode(key),
|
||||||
|
urlencoding::encode(value)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"\nOpen this URL to authenticate with {} (client '{}'):\n",
|
||||||
|
provider.provider_name(),
|
||||||
|
client_name
|
||||||
|
);
|
||||||
|
println!(" {authorize_url}\n");
|
||||||
|
|
||||||
|
let _ = open::that(&authorize_url);
|
||||||
|
|
||||||
|
let (code, returned_state) = if provider.uses_localhost_redirect() {
|
||||||
|
listen_for_oauth_callback(&redirect_uri)?
|
||||||
|
} else {
|
||||||
|
let input = Text::new("Paste the authorization code:").prompt()?;
|
||||||
|
let parts: Vec<&str> = input.splitn(2, '#').collect();
|
||||||
|
if parts.len() != 2 {
|
||||||
|
bail!("Invalid authorization code format. Expected format: <code>#<state>");
|
||||||
|
}
|
||||||
|
(parts[0].to_string(), parts[1].to_string())
|
||||||
|
};
|
||||||
|
|
||||||
|
if returned_state != state {
|
||||||
|
bail!(
|
||||||
|
"OAuth state mismatch: expected '{state}', got '{returned_state}'. \
|
||||||
|
This may indicate a CSRF attack or a stale authorization attempt."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = ReqwestClient::new();
|
||||||
|
let request = build_token_request(
|
||||||
|
&client,
|
||||||
|
provider,
|
||||||
|
&[
|
||||||
|
("grant_type", "authorization_code"),
|
||||||
|
("client_id", provider.client_id()),
|
||||||
|
("code", &code),
|
||||||
|
("code_verifier", &code_verifier),
|
||||||
|
("redirect_uri", &redirect_uri),
|
||||||
|
("state", &state),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
let response: Value = request.send().await?.json().await?;
|
||||||
|
|
||||||
|
let access_token = response["access_token"]
|
||||||
|
.as_str()
|
||||||
|
.ok_or_else(|| anyhow!("Missing access_token in response: {response}"))?
|
||||||
|
.to_string();
|
||||||
|
let refresh_token = response["refresh_token"]
|
||||||
|
.as_str()
|
||||||
|
.ok_or_else(|| anyhow!("Missing refresh_token in response: {response}"))?
|
||||||
|
.to_string();
|
||||||
|
let expires_in = response["expires_in"]
|
||||||
|
.as_i64()
|
||||||
|
.ok_or_else(|| anyhow!("Missing expires_in in response: {response}"))?;
|
||||||
|
|
||||||
|
let expires_at = Utc::now().timestamp() + expires_in;
|
||||||
|
|
||||||
|
let tokens = OAuthTokens {
|
||||||
|
access_token,
|
||||||
|
refresh_token,
|
||||||
|
expires_at,
|
||||||
|
};
|
||||||
|
|
||||||
|
save_oauth_tokens(client_name, &tokens)?;
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Successfully authenticated client '{}' with {} via OAuth. Tokens saved.",
|
||||||
|
client_name,
|
||||||
|
provider.provider_name()
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_oauth_tokens(client_name: &str) -> Option<OAuthTokens> {
|
||||||
|
let path = Config::token_file(client_name);
|
||||||
|
let content = fs::read_to_string(path).ok()?;
|
||||||
|
serde_json::from_str(&content).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_oauth_tokens(client_name: &str, tokens: &OAuthTokens) -> Result<()> {
|
||||||
|
let path = Config::token_file(client_name);
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
let json = serde_json::to_string_pretty(tokens)?;
|
||||||
|
fs::write(path, json)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn refresh_oauth_token(
|
||||||
|
client: &ReqwestClient,
|
||||||
|
provider: &impl OAuthProvider,
|
||||||
|
client_name: &str,
|
||||||
|
tokens: &OAuthTokens,
|
||||||
|
) -> Result<OAuthTokens> {
|
||||||
|
let request = build_token_request(
|
||||||
|
client,
|
||||||
|
provider,
|
||||||
|
&[
|
||||||
|
("grant_type", "refresh_token"),
|
||||||
|
("client_id", provider.client_id()),
|
||||||
|
("refresh_token", &tokens.refresh_token),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
let response: Value = request.send().await?.json().await?;
|
||||||
|
|
||||||
|
let access_token = response["access_token"]
|
||||||
|
.as_str()
|
||||||
|
.ok_or_else(|| anyhow!("Missing access_token in refresh response: {response}"))?
|
||||||
|
.to_string();
|
||||||
|
let refresh_token = response["refresh_token"]
|
||||||
|
.as_str()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.unwrap_or_else(|| tokens.refresh_token.clone());
|
||||||
|
let expires_in = response["expires_in"]
|
||||||
|
.as_i64()
|
||||||
|
.ok_or_else(|| anyhow!("Missing expires_in in refresh response: {response}"))?;
|
||||||
|
|
||||||
|
let expires_at = Utc::now().timestamp() + expires_in;
|
||||||
|
|
||||||
|
let new_tokens = OAuthTokens {
|
||||||
|
access_token,
|
||||||
|
refresh_token,
|
||||||
|
expires_at,
|
||||||
|
};
|
||||||
|
|
||||||
|
save_oauth_tokens(client_name, &new_tokens)?;
|
||||||
|
|
||||||
|
Ok(new_tokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn prepare_oauth_access_token(
|
||||||
|
client: &ReqwestClient,
|
||||||
|
provider: &impl OAuthProvider,
|
||||||
|
client_name: &str,
|
||||||
|
) -> Result<bool> {
|
||||||
|
if is_valid_access_token(client_name) {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
let tokens = match load_oauth_tokens(client_name) {
|
||||||
|
Some(t) => t,
|
||||||
|
None => return Ok(false),
|
||||||
|
};
|
||||||
|
|
||||||
|
let tokens = if Utc::now().timestamp() >= tokens.expires_at {
|
||||||
|
refresh_oauth_token(client, provider, client_name, &tokens).await?
|
||||||
|
} else {
|
||||||
|
tokens
|
||||||
|
};
|
||||||
|
|
||||||
|
set_access_token(client_name, tokens.access_token.clone(), tokens.expires_at);
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_token_request(
|
||||||
|
client: &ReqwestClient,
|
||||||
|
provider: &(impl OAuthProvider + ?Sized),
|
||||||
|
params: &[(&str, &str)],
|
||||||
|
) -> RequestBuilder {
|
||||||
|
let mut request = match provider.token_request_format() {
|
||||||
|
TokenRequestFormat::Json => {
|
||||||
|
let body: serde_json::Map<String, Value> = params
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| (k.to_string(), Value::String(v.to_string())))
|
||||||
|
.collect();
|
||||||
|
if let Some(secret) = provider.client_secret() {
|
||||||
|
let mut body = body;
|
||||||
|
body.insert(
|
||||||
|
"client_secret".to_string(),
|
||||||
|
Value::String(secret.to_string()),
|
||||||
|
);
|
||||||
|
client.post(provider.token_url()).json(&body)
|
||||||
|
} else {
|
||||||
|
client.post(provider.token_url()).json(&body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TokenRequestFormat::FormUrlEncoded => {
|
||||||
|
let mut form: HashMap<String, String> = params
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| (k.to_string(), v.to_string()))
|
||||||
|
.collect();
|
||||||
|
if let Some(secret) = provider.client_secret() {
|
||||||
|
form.insert("client_secret".to_string(), secret.to_string());
|
||||||
|
}
|
||||||
|
client.post(provider.token_url()).form(&form)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (key, value) in provider.extra_token_headers() {
|
||||||
|
request = request.header(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
request
|
||||||
|
}
|
||||||
|
|
||||||
|
fn listen_for_oauth_callback(redirect_uri: &str) -> Result<(String, String)> {
|
||||||
|
let url: Url = redirect_uri.parse()?;
|
||||||
|
let host = url.host_str().unwrap_or("127.0.0.1");
|
||||||
|
let port = url
|
||||||
|
.port()
|
||||||
|
.ok_or_else(|| anyhow!("No port in redirect URI"))?;
|
||||||
|
let path = url.path();
|
||||||
|
|
||||||
|
println!("Waiting for OAuth callback on {redirect_uri} ...\n");
|
||||||
|
|
||||||
|
let listener = TcpListener::bind(format!("{host}:{port}"))?;
|
||||||
|
let (mut stream, _) = listener.accept()?;
|
||||||
|
|
||||||
|
let mut reader = BufReader::new(&stream);
|
||||||
|
let mut request_line = String::new();
|
||||||
|
reader.read_line(&mut request_line)?;
|
||||||
|
|
||||||
|
let request_path = request_line
|
||||||
|
.split_whitespace()
|
||||||
|
.nth(1)
|
||||||
|
.ok_or_else(|| anyhow!("Malformed HTTP request from OAuth callback"))?;
|
||||||
|
|
||||||
|
let full_url = format!("http://{host}:{port}{request_path}");
|
||||||
|
let parsed: Url = full_url.parse()?;
|
||||||
|
|
||||||
|
if !parsed.path().starts_with(path) {
|
||||||
|
bail!("Unexpected callback path: {}", parsed.path());
|
||||||
|
}
|
||||||
|
|
||||||
|
let code = parsed
|
||||||
|
.query_pairs()
|
||||||
|
.find(|(k, _)| k == "code")
|
||||||
|
.map(|(_, v)| v.to_string())
|
||||||
|
.ok_or_else(|| {
|
||||||
|
let error = parsed
|
||||||
|
.query_pairs()
|
||||||
|
.find(|(k, _)| k == "error")
|
||||||
|
.map(|(_, v)| v.to_string())
|
||||||
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
|
anyhow!("OAuth callback returned error: {error}")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let returned_state = parsed
|
||||||
|
.query_pairs()
|
||||||
|
.find(|(k, _)| k == "state")
|
||||||
|
.map(|(_, v)| v.to_string())
|
||||||
|
.ok_or_else(|| anyhow!("Missing state parameter in OAuth callback"))?;
|
||||||
|
|
||||||
|
let response_body = "<html><body><h2>Authentication successful!</h2><p>You can close this tab and return to your terminal.</p></body></html>";
|
||||||
|
let response = format!(
|
||||||
|
"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||||
|
response_body.len(),
|
||||||
|
response_body
|
||||||
|
);
|
||||||
|
stream.write_all(response.as_bytes())?;
|
||||||
|
|
||||||
|
Ok((code, returned_state))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_oauth_provider(provider_type: &str) -> Option<Box<dyn OAuthProvider>> {
|
||||||
|
match provider_type {
|
||||||
|
"claude" => Some(Box::new(super::claude_oauth::ClaudeOAuthProvider)),
|
||||||
|
"gemini" => Some(Box::new(super::gemini_oauth::GeminiOAuthProvider)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve_provider_type(client_name: &str, clients: &[ClientConfig]) -> Option<&'static str> {
|
||||||
|
for client_config in clients {
|
||||||
|
let (config_name, provider_type, auth) = client_config_info(client_config);
|
||||||
|
if config_name == client_name {
|
||||||
|
if auth == Some("oauth") && get_oauth_provider(provider_type).is_some() {
|
||||||
|
return Some(provider_type);
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_oauth_capable_clients(clients: &[ClientConfig]) -> Vec<String> {
|
||||||
|
clients
|
||||||
|
.iter()
|
||||||
|
.filter_map(|client_config| {
|
||||||
|
let (name, provider_type, auth) = client_config_info(client_config);
|
||||||
|
if auth == Some("oauth") && get_oauth_provider(provider_type).is_some() {
|
||||||
|
Some(name.to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn client_config_info(client_config: &ClientConfig) -> (&str, &'static str, Option<&str>) {
|
||||||
|
match client_config {
|
||||||
|
ClientConfig::ClaudeConfig(c) => (
|
||||||
|
c.name.as_deref().unwrap_or("claude"),
|
||||||
|
"claude",
|
||||||
|
c.auth.as_deref(),
|
||||||
|
),
|
||||||
|
ClientConfig::OpenAIConfig(c) => (c.name.as_deref().unwrap_or("openai"), "openai", None),
|
||||||
|
ClientConfig::OpenAICompatibleConfig(c) => (
|
||||||
|
c.name.as_deref().unwrap_or("openai-compatible"),
|
||||||
|
"openai-compatible",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
ClientConfig::GeminiConfig(c) => (
|
||||||
|
c.name.as_deref().unwrap_or("gemini"),
|
||||||
|
"gemini",
|
||||||
|
c.auth.as_deref(),
|
||||||
|
),
|
||||||
|
ClientConfig::CohereConfig(c) => (c.name.as_deref().unwrap_or("cohere"), "cohere", None),
|
||||||
|
ClientConfig::AzureOpenAIConfig(c) => (
|
||||||
|
c.name.as_deref().unwrap_or("azure-openai"),
|
||||||
|
"azure-openai",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
ClientConfig::VertexAIConfig(c) => {
|
||||||
|
(c.name.as_deref().unwrap_or("vertexai"), "vertexai", None)
|
||||||
|
}
|
||||||
|
ClientConfig::BedrockConfig(c) => (c.name.as_deref().unwrap_or("bedrock"), "bedrock", None),
|
||||||
|
ClientConfig::Unknown => ("unknown", "unknown", None),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,10 +2,10 @@ use super::*;
|
|||||||
|
|
||||||
use crate::utils::strip_think_tag;
|
use crate::utils::strip_think_tag;
|
||||||
|
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{Context, Result, bail};
|
||||||
use reqwest::RequestBuilder;
|
use reqwest::RequestBuilder;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::{json, Value};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
const API_BASE: &str = "https://api.openai.com/v1";
|
const API_BASE: &str = "https://api.openai.com/v1";
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ impl OpenAIClient {
|
|||||||
config_get_fn!(api_key, get_api_key);
|
config_get_fn!(api_key, get_api_key);
|
||||||
config_get_fn!(api_base, get_api_base);
|
config_get_fn!(api_base, get_api_base);
|
||||||
|
|
||||||
pub const PROMPTS: [PromptAction<'static>; 1] = [("api_key", "API Key", None, true)];
|
create_client_config!([("api_key", "API Key", None, true)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_client_trait!(
|
impl_client_trait!(
|
||||||
@@ -114,7 +114,9 @@ pub async fn openai_chat_completions_streaming(
|
|||||||
function_arguments = String::from("{}");
|
function_arguments = String::from("{}");
|
||||||
}
|
}
|
||||||
let arguments: Value = function_arguments.parse().with_context(|| {
|
let arguments: Value = function_arguments.parse().with_context(|| {
|
||||||
format!("Tool call '{function_name}' has non-JSON arguments '{function_arguments}'")
|
format!(
|
||||||
|
"Tool call '{function_name}' has non-JSON arguments '{function_arguments}'"
|
||||||
|
)
|
||||||
})?;
|
})?;
|
||||||
handler.tool_call(ToolCall::new(
|
handler.tool_call(ToolCall::new(
|
||||||
function_name.clone(),
|
function_name.clone(),
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ impl OpenAICompatibleClient {
|
|||||||
config_get_fn!(api_base, get_api_base);
|
config_get_fn!(api_base, get_api_base);
|
||||||
config_get_fn!(api_key, get_api_key);
|
config_get_fn!(api_key, get_api_key);
|
||||||
|
|
||||||
pub const PROMPTS: [PromptAction<'static>; 0] = [];
|
create_client_config!([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_client_trait!(
|
impl_client_trait!(
|
||||||
|
|||||||
+24
-16
@@ -3,11 +3,11 @@ use super::claude::*;
|
|||||||
use super::openai::*;
|
use super::openai::*;
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, Context, Result};
|
use anyhow::{Context, Result, anyhow, bail};
|
||||||
use chrono::{Duration, Utc};
|
use chrono::{Duration, Utc};
|
||||||
use reqwest::{Client as ReqwestClient, RequestBuilder};
|
use reqwest::{Client as ReqwestClient, RequestBuilder};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::{json, Value};
|
use serde_json::{Value, json};
|
||||||
use std::{path::PathBuf, str::FromStr};
|
use std::{path::PathBuf, str::FromStr};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Default)]
|
#[derive(Debug, Clone, Deserialize, Default)]
|
||||||
@@ -26,10 +26,10 @@ impl VertexAIClient {
|
|||||||
config_get_fn!(project_id, get_project_id);
|
config_get_fn!(project_id, get_project_id);
|
||||||
config_get_fn!(location, get_location);
|
config_get_fn!(location, get_location);
|
||||||
|
|
||||||
pub const PROMPTS: [PromptAction<'static>; 2] = [
|
create_client_config!([
|
||||||
("project_id", "Project ID", None, false),
|
("project_id", "Project ID", None, false),
|
||||||
("location", "Location", None, false),
|
("location", "Location", None, false),
|
||||||
];
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
@@ -99,9 +99,13 @@ fn prepare_chat_completions(
|
|||||||
let access_token = get_access_token(self_.name())?;
|
let access_token = get_access_token(self_.name())?;
|
||||||
|
|
||||||
let base_url = if location == "global" {
|
let base_url = if location == "global" {
|
||||||
format!("https://aiplatform.googleapis.com/v1/projects/{project_id}/locations/global/publishers")
|
format!(
|
||||||
|
"https://aiplatform.googleapis.com/v1/projects/{project_id}/locations/global/publishers"
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
format!("https://{location}-aiplatform.googleapis.com/v1/projects/{project_id}/locations/{location}/publishers")
|
format!(
|
||||||
|
"https://{location}-aiplatform.googleapis.com/v1/projects/{project_id}/locations/{location}/publishers"
|
||||||
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
let model_name = self_.model.real_name();
|
let model_name = self_.model.real_name();
|
||||||
@@ -158,9 +162,13 @@ fn prepare_embeddings(self_: &VertexAIClient, data: &EmbeddingsData) -> Result<R
|
|||||||
let access_token = get_access_token(self_.name())?;
|
let access_token = get_access_token(self_.name())?;
|
||||||
|
|
||||||
let base_url = if location == "global" {
|
let base_url = if location == "global" {
|
||||||
format!("https://aiplatform.googleapis.com/v1/projects/{project_id}/locations/global/publishers")
|
format!(
|
||||||
|
"https://aiplatform.googleapis.com/v1/projects/{project_id}/locations/global/publishers"
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
format!("https://{location}-aiplatform.googleapis.com/v1/projects/{project_id}/locations/{location}/publishers")
|
format!(
|
||||||
|
"https://{location}-aiplatform.googleapis.com/v1/projects/{project_id}/locations/{location}/publishers"
|
||||||
|
)
|
||||||
};
|
};
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"{base_url}/google/models/{}:predict",
|
"{base_url}/google/models/{}:predict",
|
||||||
@@ -220,12 +228,12 @@ pub async fn gemini_chat_completions_streaming(
|
|||||||
part["functionCall"]["args"].as_object(),
|
part["functionCall"]["args"].as_object(),
|
||||||
) {
|
) {
|
||||||
let thought_signature = part["thoughtSignature"]
|
let thought_signature = part["thoughtSignature"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.or_else(|| part["thought_signature"].as_str())
|
.or_else(|| part["thought_signature"].as_str())
|
||||||
.map(|s| s.to_string());
|
.map(|s| s.to_string());
|
||||||
handler.tool_call(
|
handler.tool_call(
|
||||||
ToolCall::new(name.to_string(), json!(args), None)
|
ToolCall::new(name.to_string(), json!(args), None)
|
||||||
.with_thought_signature(thought_signature),
|
.with_thought_signature(thought_signature),
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -288,12 +296,12 @@ fn gemini_extract_chat_completions_text(data: &Value) -> Result<ChatCompletionsO
|
|||||||
part["functionCall"]["args"].as_object(),
|
part["functionCall"]["args"].as_object(),
|
||||||
) {
|
) {
|
||||||
let thought_signature = part["thoughtSignature"]
|
let thought_signature = part["thoughtSignature"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.or_else(|| part["thought_signature"].as_str())
|
.or_else(|| part["thought_signature"].as_str())
|
||||||
.map(|s| s.to_string());
|
.map(|s| s.to_string());
|
||||||
tool_calls.push(
|
tool_calls.push(
|
||||||
ToolCall::new(name.to_string(), json!(args), None)
|
ToolCall::new(name.to_string(), json!(args), None)
|
||||||
.with_thought_signature(thought_signature),
|
.with_thought_signature(thought_signature),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -476,6 +476,11 @@ impl Agent {
|
|||||||
self.todo_list.mark_done(id)
|
self.todo_list.mark_done(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn clear_todo_list(&mut self) {
|
||||||
|
self.todo_list.clear();
|
||||||
|
self.reset_continuation();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn continuation_prompt(&self) -> String {
|
pub fn continuation_prompt(&self) -> String {
|
||||||
self.config.continuation_prompt.clone().unwrap_or_else(|| {
|
self.config.continuation_prompt.clone().unwrap_or_else(|| {
|
||||||
formatdoc! {"
|
formatdoc! {"
|
||||||
|
|||||||
+10
-5
@@ -239,12 +239,17 @@ impl Input {
|
|||||||
patch_messages(&mut messages, model);
|
patch_messages(&mut messages, model);
|
||||||
model.guard_max_input_tokens(&messages)?;
|
model.guard_max_input_tokens(&messages)?;
|
||||||
let (temperature, top_p) = (self.role().temperature(), self.role().top_p());
|
let (temperature, top_p) = (self.role().temperature(), self.role().top_p());
|
||||||
let functions = self.config.read().select_functions(self.role());
|
let functions = if model.supports_function_calling() {
|
||||||
if let Some(vec) = &functions {
|
let fns = self.config.read().select_functions(self.role());
|
||||||
for def in vec {
|
if let Some(vec) = &fns {
|
||||||
debug!("Function definition: {:?}", def.name);
|
for def in vec {
|
||||||
|
debug!("Function definition: {:?}", def.name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
fns
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
Ok(ChatCompletionsData {
|
Ok(ChatCompletionsData {
|
||||||
messages,
|
messages,
|
||||||
temperature,
|
temperature,
|
||||||
|
|||||||
@@ -428,6 +428,14 @@ impl Config {
|
|||||||
base_dir.join(env!("CARGO_CRATE_NAME"))
|
base_dir.join(env!("CARGO_CRATE_NAME"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn oauth_tokens_path() -> PathBuf {
|
||||||
|
Self::cache_path().join("oauth")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn token_file(client_name: &str) -> PathBuf {
|
||||||
|
Self::oauth_tokens_path().join(format!("{client_name}_oauth_tokens.json"))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn log_path() -> PathBuf {
|
pub fn log_path() -> PathBuf {
|
||||||
Config::cache_path().join(format!("{}.log", env!("CARGO_CRATE_NAME")))
|
Config::cache_path().join(format!("{}.log", env!("CARGO_CRATE_NAME")))
|
||||||
}
|
}
|
||||||
@@ -1834,6 +1842,12 @@ impl Config {
|
|||||||
bail!("Already in an agent, please run '.exit agent' first to exit the current agent.");
|
bail!("Already in an agent, please run '.exit agent' first to exit the current agent.");
|
||||||
}
|
}
|
||||||
let agent = Agent::init(config, agent_name, abort_signal.clone()).await?;
|
let agent = Agent::init(config, agent_name, abort_signal.clone()).await?;
|
||||||
|
if !agent.model().supports_function_calling() {
|
||||||
|
eprintln!(
|
||||||
|
"Warning: The model '{}' does not support function calling. Agent tools (including todo, spawning, and user interaction) will not be available.",
|
||||||
|
agent.model().id()
|
||||||
|
);
|
||||||
|
}
|
||||||
let session = session_name.map(|v| v.to_string()).or_else(|| {
|
let session = session_name.map(|v| v.to_string()).or_else(|| {
|
||||||
if config.read().macro_flag {
|
if config.read().macro_flag {
|
||||||
None
|
None
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ pub(in crate::config) const DEFAULT_TODO_INSTRUCTIONS: &str = indoc! {"
|
|||||||
- `todo__add`: Add individual tasks. Add all planned steps before starting work.
|
- `todo__add`: Add individual tasks. Add all planned steps before starting work.
|
||||||
- `todo__done`: Mark a task done by id. Call this immediately after completing each step.
|
- `todo__done`: Mark a task done by id. Call this immediately after completing each step.
|
||||||
- `todo__list`: Show the current todo list.
|
- `todo__list`: Show the current todo list.
|
||||||
|
- `todo__clear`: Clear the entire todo list and reset the goal. Use when the user cancels or changes direction.
|
||||||
|
|
||||||
RULES:
|
RULES:
|
||||||
- Always create a todo list before starting work.
|
- Always create a todo list before starting work.
|
||||||
- Mark each task done as soon as you finish it; do not batch.
|
- Mark each task done as soon as you finish it; do not batch.
|
||||||
|
- If the user cancels the current task or changes direction, call `todo__clear` immediately.
|
||||||
- If you stop with incomplete tasks, the system will automatically prompt you to continue."
|
- If you stop with incomplete tasks, the system will automatically prompt you to continue."
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,11 @@ impl TodoList {
|
|||||||
self.todos.is_empty()
|
self.todos.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.goal.clear();
|
||||||
|
self.todos.clear();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render_for_model(&self) -> String {
|
pub fn render_for_model(&self) -> String {
|
||||||
let mut lines = Vec::new();
|
let mut lines = Vec::new();
|
||||||
if !self.goal.is_empty() {
|
if !self.goal.is_empty() {
|
||||||
@@ -149,6 +154,21 @@ mod tests {
|
|||||||
assert!(rendered.contains("○ 2. Map"));
|
assert!(rendered.contains("○ 2. Map"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_clear() {
|
||||||
|
let mut list = TodoList::new("Some goal");
|
||||||
|
list.add("Task 1");
|
||||||
|
list.add("Task 2");
|
||||||
|
list.mark_done(1);
|
||||||
|
assert!(!list.is_empty());
|
||||||
|
|
||||||
|
list.clear();
|
||||||
|
assert!(list.is_empty());
|
||||||
|
assert!(list.goal.is_empty());
|
||||||
|
assert_eq!(list.todos.len(), 0);
|
||||||
|
assert!(!list.has_incomplete());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_serialization_roundtrip() {
|
fn test_serialization_roundtrip() {
|
||||||
let mut list = TodoList::new("Roundtrip");
|
let mut list = TodoList::new("Roundtrip");
|
||||||
|
|||||||
+48
-7
@@ -22,7 +22,7 @@ use serde_json::{Value, json};
|
|||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Write;
|
use std::io::{Read, Write};
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
env, fs, io,
|
env, fs, io,
|
||||||
@@ -1064,7 +1064,7 @@ impl ToolCall {
|
|||||||
function_name.clone(),
|
function_name.clone(),
|
||||||
function_name,
|
function_name,
|
||||||
vec![],
|
vec![],
|
||||||
Default::default(),
|
agent.variable_envs(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1117,18 +1117,59 @@ pub fn run_llm_function(
|
|||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
let cmd_name = polyfill_cmd_name(&cmd_name, &bin_dirs);
|
let cmd_name = polyfill_cmd_name(&cmd_name, &bin_dirs);
|
||||||
|
|
||||||
let output = Command::new(&cmd_name)
|
envs.insert("CLICOLOR_FORCE".into(), "1".into());
|
||||||
|
envs.insert("FORCE_COLOR".into(), "1".into());
|
||||||
|
|
||||||
|
let mut child = Command::new(&cmd_name)
|
||||||
.args(&cmd_args)
|
.args(&cmd_args)
|
||||||
.envs(envs)
|
.envs(envs)
|
||||||
.stdout(Stdio::inherit())
|
.stdout(Stdio::piped())
|
||||||
.stderr(Stdio::piped())
|
.stderr(Stdio::piped())
|
||||||
.spawn()
|
.spawn()
|
||||||
.and_then(|child| child.wait_with_output())
|
|
||||||
.map_err(|err| anyhow!("Unable to run {command_name}, {err}"))?;
|
.map_err(|err| anyhow!("Unable to run {command_name}, {err}"))?;
|
||||||
|
|
||||||
let exit_code = output.status.code().unwrap_or_default();
|
let stdout = child.stdout.take().expect("Failed to capture stdout");
|
||||||
|
let mut stderr = child.stderr.take().expect("Failed to capture stderr");
|
||||||
|
|
||||||
|
let stdout_thread = std::thread::spawn(move || {
|
||||||
|
let mut buffer = [0; 1024];
|
||||||
|
let mut reader = stdout;
|
||||||
|
let mut out = io::stdout();
|
||||||
|
while let Ok(n) = reader.read(&mut buffer) {
|
||||||
|
if n == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let chunk = &buffer[0..n];
|
||||||
|
let mut last_pos = 0;
|
||||||
|
for (i, &byte) in chunk.iter().enumerate() {
|
||||||
|
if byte == b'\n' {
|
||||||
|
let _ = out.write_all(&chunk[last_pos..i]);
|
||||||
|
let _ = out.write_all(b"\r\n");
|
||||||
|
last_pos = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if last_pos < n {
|
||||||
|
let _ = out.write_all(&chunk[last_pos..n]);
|
||||||
|
}
|
||||||
|
let _ = out.flush();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let stderr_thread = std::thread::spawn(move || {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
let _ = stderr.read_to_end(&mut buf);
|
||||||
|
buf
|
||||||
|
});
|
||||||
|
|
||||||
|
let status = child
|
||||||
|
.wait()
|
||||||
|
.map_err(|err| anyhow!("Unable to run {command_name}, {err}"))?;
|
||||||
|
let _ = stdout_thread.join();
|
||||||
|
let stderr_bytes = stderr_thread.join().unwrap_or_default();
|
||||||
|
|
||||||
|
let exit_code = status.code().unwrap_or_default();
|
||||||
if exit_code != 0 {
|
if exit_code != 0 {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
let stderr = String::from_utf8_lossy(&stderr_bytes).trim().to_string();
|
||||||
if !stderr.is_empty() {
|
if !stderr.is_empty() {
|
||||||
eprintln!("{stderr}");
|
eprintln!("{stderr}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,16 @@ pub fn todo_function_declarations() -> Vec<FunctionDeclaration> {
|
|||||||
},
|
},
|
||||||
agent: false,
|
agent: false,
|
||||||
},
|
},
|
||||||
|
FunctionDeclaration {
|
||||||
|
name: format!("{TODO_FUNCTION_PREFIX}clear"),
|
||||||
|
description: "Clear the entire todo list and reset the goal. Use when the current task has been canceled or invalidated.".to_string(),
|
||||||
|
parameters: JsonSchema {
|
||||||
|
type_value: Some("object".to_string()),
|
||||||
|
properties: Some(IndexMap::new()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
agent: false,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,6 +166,17 @@ pub fn handle_todo_tool(config: &GlobalConfig, cmd_name: &str, args: &Value) ->
|
|||||||
None => bail!("No active agent"),
|
None => bail!("No active agent"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"clear" => {
|
||||||
|
let mut cfg = config.write();
|
||||||
|
let agent = cfg.agent.as_mut();
|
||||||
|
match agent {
|
||||||
|
Some(agent) => {
|
||||||
|
agent.clear_todo_list();
|
||||||
|
Ok(json!({"status": "ok", "message": "Todo list cleared"}))
|
||||||
|
}
|
||||||
|
None => bail!("No active agent"),
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => bail!("Unknown todo action: {action}"),
|
_ => bail!("Unknown todo action: {action}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use tokio::sync::oneshot;
|
|||||||
pub const USER_FUNCTION_PREFIX: &str = "user__";
|
pub const USER_FUNCTION_PREFIX: &str = "user__";
|
||||||
|
|
||||||
const DEFAULT_ESCALATION_TIMEOUT_SECS: u64 = 300;
|
const DEFAULT_ESCALATION_TIMEOUT_SECS: u64 = 300;
|
||||||
|
const CUSTOM_MULTI_CHOICE_ANSWER_OPTION: &str = "Other (custom)";
|
||||||
|
|
||||||
pub fn user_interaction_function_declarations() -> Vec<FunctionDeclaration> {
|
pub fn user_interaction_function_declarations() -> Vec<FunctionDeclaration> {
|
||||||
vec![
|
vec![
|
||||||
@@ -151,9 +152,14 @@ fn handle_direct_ask(args: &Value) -> Result<Value> {
|
|||||||
.get("question")
|
.get("question")
|
||||||
.and_then(Value::as_str)
|
.and_then(Value::as_str)
|
||||||
.ok_or_else(|| anyhow!("'question' is required"))?;
|
.ok_or_else(|| anyhow!("'question' is required"))?;
|
||||||
let options = parse_options(args)?;
|
let mut options = parse_options(args)?;
|
||||||
|
options.push(CUSTOM_MULTI_CHOICE_ANSWER_OPTION.to_string());
|
||||||
|
|
||||||
let answer = Select::new(question, options).prompt()?;
|
let mut answer = Select::new(question, options).prompt()?;
|
||||||
|
|
||||||
|
if answer == CUSTOM_MULTI_CHOICE_ANSWER_OPTION {
|
||||||
|
answer = Text::new("Custom response:").prompt()?
|
||||||
|
}
|
||||||
|
|
||||||
Ok(json!({ "answer": answer }))
|
Ok(json!({ "answer": answer }))
|
||||||
}
|
}
|
||||||
@@ -175,7 +181,7 @@ fn handle_direct_input(args: &Value) -> Result<Value> {
|
|||||||
.and_then(Value::as_str)
|
.and_then(Value::as_str)
|
||||||
.ok_or_else(|| anyhow!("'question' is required"))?;
|
.ok_or_else(|| anyhow!("'question' is required"))?;
|
||||||
|
|
||||||
let answer = Text::new(question).prompt()?;
|
let answer = Text::new(&format!("{question}\nYour answer: ")).prompt()?;
|
||||||
|
|
||||||
Ok(json!({ "answer": answer }))
|
Ok(json!({ "answer": answer }))
|
||||||
}
|
}
|
||||||
|
|||||||
+51
-4
@@ -16,28 +16,30 @@ mod vault;
|
|||||||
extern crate log;
|
extern crate log;
|
||||||
|
|
||||||
use crate::client::{
|
use crate::client::{
|
||||||
ModelType, call_chat_completions, call_chat_completions_streaming, list_models,
|
ModelType, call_chat_completions, call_chat_completions_streaming, list_models, oauth,
|
||||||
};
|
};
|
||||||
use crate::config::{
|
use crate::config::{
|
||||||
Agent, CODE_ROLE, Config, EXPLAIN_SHELL_ROLE, GlobalConfig, Input, SHELL_ROLE,
|
Agent, CODE_ROLE, Config, EXPLAIN_SHELL_ROLE, GlobalConfig, Input, SHELL_ROLE,
|
||||||
TEMP_SESSION_NAME, WorkingMode, ensure_parent_exists, list_agents, load_env_file,
|
TEMP_SESSION_NAME, WorkingMode, ensure_parent_exists, list_agents, load_env_file,
|
||||||
macro_execute,
|
macro_execute,
|
||||||
};
|
};
|
||||||
use crate::render::render_error;
|
use crate::render::{prompt_theme, render_error};
|
||||||
use crate::repl::Repl;
|
use crate::repl::Repl;
|
||||||
use crate::utils::*;
|
use crate::utils::*;
|
||||||
|
|
||||||
use crate::cli::Cli;
|
use crate::cli::Cli;
|
||||||
use crate::vault::Vault;
|
use crate::vault::Vault;
|
||||||
use anyhow::{Result, bail};
|
use anyhow::{Result, anyhow, bail};
|
||||||
use clap::{CommandFactory, Parser};
|
use clap::{CommandFactory, Parser};
|
||||||
use clap_complete::CompleteEnv;
|
use clap_complete::CompleteEnv;
|
||||||
use inquire::Text;
|
use client::ClientConfig;
|
||||||
|
use inquire::{Select, Text, set_global_render_config};
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
use log4rs::append::console::ConsoleAppender;
|
use log4rs::append::console::ConsoleAppender;
|
||||||
use log4rs::append::file::FileAppender;
|
use log4rs::append::file::FileAppender;
|
||||||
use log4rs::config::{Appender, Logger, Root};
|
use log4rs::config::{Appender, Logger, Root};
|
||||||
use log4rs::encode::pattern::PatternEncoder;
|
use log4rs::encode::pattern::PatternEncoder;
|
||||||
|
use oauth::OAuthProvider;
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::{env, mem, process, sync::Arc};
|
use std::{env, mem, process, sync::Arc};
|
||||||
@@ -81,6 +83,13 @@ async fn main() -> Result<()> {
|
|||||||
|
|
||||||
let log_path = setup_logger()?;
|
let log_path = setup_logger()?;
|
||||||
|
|
||||||
|
if let Some(client_arg) = &cli.authenticate {
|
||||||
|
let config = Config::init_bare()?;
|
||||||
|
let (client_name, provider) = resolve_oauth_client(client_arg.as_deref(), &config.clients)?;
|
||||||
|
oauth::run_oauth_flow(&*provider, &client_name).await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
if vault_flags {
|
if vault_flags {
|
||||||
return Vault::handle_vault_flags(cli, Config::init_bare()?);
|
return Vault::handle_vault_flags(cli, Config::init_bare()?);
|
||||||
}
|
}
|
||||||
@@ -97,6 +106,14 @@ async fn main() -> Result<()> {
|
|||||||
)
|
)
|
||||||
.await?,
|
.await?,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
{
|
||||||
|
let cfg = config.read();
|
||||||
|
if cfg.highlight {
|
||||||
|
set_global_render_config(prompt_theme(cfg.render_options()?)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Err(err) = run(config, cli, text, abort_signal).await {
|
if let Err(err) = run(config, cli, text, abort_signal).await {
|
||||||
render_error(err);
|
render_error(err);
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
@@ -504,3 +521,33 @@ fn init_console_logger(
|
|||||||
.build(Root::builder().appender("console").build(root_log_level))
|
.build(Root::builder().appender("console").build(root_log_level))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_oauth_client(
|
||||||
|
explicit: Option<&str>,
|
||||||
|
clients: &[ClientConfig],
|
||||||
|
) -> Result<(String, Box<dyn OAuthProvider>)> {
|
||||||
|
if let Some(name) = explicit {
|
||||||
|
let provider_type = oauth::resolve_provider_type(name, clients)
|
||||||
|
.ok_or_else(|| anyhow!("Client '{name}' not found or doesn't support OAuth"))?;
|
||||||
|
let provider = oauth::get_oauth_provider(provider_type).unwrap();
|
||||||
|
return Ok((name.to_string(), provider));
|
||||||
|
}
|
||||||
|
|
||||||
|
let candidates = oauth::list_oauth_capable_clients(clients);
|
||||||
|
match candidates.len() {
|
||||||
|
0 => bail!("No OAuth-capable clients configured."),
|
||||||
|
1 => {
|
||||||
|
let name = &candidates[0];
|
||||||
|
let provider_type = oauth::resolve_provider_type(name, clients).unwrap();
|
||||||
|
let provider = oauth::get_oauth_provider(provider_type).unwrap();
|
||||||
|
Ok((name.clone(), provider))
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let choice =
|
||||||
|
Select::new("Select a client to authenticate:", candidates.clone()).prompt()?;
|
||||||
|
let provider_type = oauth::resolve_provider_type(&choice, clients).unwrap();
|
||||||
|
let provider = oauth::get_oauth_provider(provider_type).unwrap();
|
||||||
|
Ok((choice, provider))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+76
-51
@@ -158,27 +158,31 @@ impl McpRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn reinit(
|
pub async fn reinit(
|
||||||
registry: McpRegistry,
|
mut registry: McpRegistry,
|
||||||
enabled_mcp_servers: Option<String>,
|
enabled_mcp_servers: Option<String>,
|
||||||
abort_signal: AbortSignal,
|
abort_signal: AbortSignal,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
debug!("Reinitializing MCP registry");
|
debug!("Reinitializing MCP registry");
|
||||||
debug!("Stopping all MCP servers");
|
|
||||||
let mut new_registry = abortable_run_with_spinner(
|
let desired_ids = registry.resolve_server_ids(enabled_mcp_servers.clone());
|
||||||
registry.stop_all_servers(),
|
let desired_set: HashSet<String> = desired_ids.iter().cloned().collect();
|
||||||
"Stopping MCP servers",
|
|
||||||
|
debug!("Stopping unused MCP servers");
|
||||||
|
abortable_run_with_spinner(
|
||||||
|
registry.stop_unused_servers(&desired_set),
|
||||||
|
"Stopping unused MCP servers",
|
||||||
abort_signal.clone(),
|
abort_signal.clone(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
abortable_run_with_spinner(
|
abortable_run_with_spinner(
|
||||||
new_registry.start_select_mcp_servers(enabled_mcp_servers),
|
registry.start_select_mcp_servers(enabled_mcp_servers),
|
||||||
"Loading MCP servers",
|
"Loading MCP servers",
|
||||||
abort_signal,
|
abort_signal,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(new_registry)
|
Ok(registry)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn start_select_mcp_servers(
|
async fn start_select_mcp_servers(
|
||||||
@@ -192,43 +196,30 @@ impl McpRegistry {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(servers) = enabled_mcp_servers {
|
let desired_ids = self.resolve_server_ids(enabled_mcp_servers);
|
||||||
debug!("Starting selected MCP servers: {:?}", servers);
|
let ids_to_start: Vec<String> = desired_ids
|
||||||
let config = self
|
.into_iter()
|
||||||
.config
|
.filter(|id| !self.servers.contains_key(id))
|
||||||
.as_ref()
|
.collect();
|
||||||
.with_context(|| "MCP Config not defined. Cannot start servers")?;
|
|
||||||
let mcp_servers = config.mcp_servers.clone();
|
|
||||||
|
|
||||||
let enabled_servers: HashSet<String> =
|
if ids_to_start.is_empty() {
|
||||||
servers.split(',').map(|s| s.trim().to_string()).collect();
|
return Ok(());
|
||||||
let server_ids: Vec<String> = if servers == "all" {
|
}
|
||||||
mcp_servers.into_keys().collect()
|
|
||||||
} else {
|
|
||||||
mcp_servers
|
|
||||||
.into_keys()
|
|
||||||
.filter(|id| enabled_servers.contains(id))
|
|
||||||
.collect()
|
|
||||||
};
|
|
||||||
|
|
||||||
let results: Vec<(String, Arc<_>, ServerCatalog)> = stream::iter(
|
debug!("Starting selected MCP servers: {:?}", ids_to_start);
|
||||||
server_ids
|
|
||||||
.into_iter()
|
|
||||||
.map(|id| async { self.start_server(id).await }),
|
|
||||||
)
|
|
||||||
.buffer_unordered(num_cpus::get())
|
|
||||||
.try_collect()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
self.servers = results
|
let results: Vec<(String, Arc<_>, ServerCatalog)> = stream::iter(
|
||||||
.clone()
|
ids_to_start
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(id, server, _)| (id, server))
|
.map(|id| async { self.start_server(id).await }),
|
||||||
.collect();
|
)
|
||||||
self.catalogs = results
|
.buffer_unordered(num_cpus::get())
|
||||||
.into_iter()
|
.try_collect()
|
||||||
.map(|(id, _, catalog)| (id, catalog))
|
.await?;
|
||||||
.collect();
|
|
||||||
|
for (id, server, catalog) in results {
|
||||||
|
self.servers.insert(id.clone(), server);
|
||||||
|
self.catalogs.insert(id, catalog);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -309,19 +300,53 @@ impl McpRegistry {
|
|||||||
Ok((id.to_string(), service, catalog))
|
Ok((id.to_string(), service, catalog))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn stop_all_servers(mut self) -> Result<Self> {
|
fn resolve_server_ids(&self, enabled_mcp_servers: Option<String>) -> Vec<String> {
|
||||||
for (id, server) in self.servers {
|
if let Some(config) = &self.config
|
||||||
Arc::try_unwrap(server)
|
&& let Some(servers) = enabled_mcp_servers
|
||||||
.map_err(|_| anyhow!("Failed to unwrap Arc for MCP server: {id}"))?
|
{
|
||||||
.cancel()
|
if servers == "all" {
|
||||||
.await
|
config.mcp_servers.keys().cloned().collect()
|
||||||
.with_context(|| format!("Failed to stop MCP server: {id}"))?;
|
} else {
|
||||||
info!("Stopped MCP server: {id}");
|
let enabled_servers: HashSet<String> =
|
||||||
|
servers.split(',').map(|s| s.trim().to_string()).collect();
|
||||||
|
config
|
||||||
|
.mcp_servers
|
||||||
|
.keys()
|
||||||
|
.filter(|id| enabled_servers.contains(*id))
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn stop_unused_servers(&mut self, keep_ids: &HashSet<String>) -> Result<()> {
|
||||||
|
let mut ids_to_remove = Vec::new();
|
||||||
|
for (id, _) in self.servers.iter() {
|
||||||
|
if !keep_ids.contains(id) {
|
||||||
|
ids_to_remove.push(id.clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.servers = HashMap::new();
|
for id in ids_to_remove {
|
||||||
|
if let Some(server) = self.servers.remove(&id) {
|
||||||
Ok(self)
|
match Arc::try_unwrap(server) {
|
||||||
|
Ok(server_inner) => {
|
||||||
|
server_inner
|
||||||
|
.cancel()
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("Failed to stop MCP server: {id}"))?;
|
||||||
|
info!("Stopped MCP server: {id}");
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
info!("Detaching from MCP server: {id} (still in use)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.catalogs.remove(&id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_started_servers(&self) -> Vec<String> {
|
pub fn list_started_servers(&self) -> Vec<String> {
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
use crate::render::RenderOptions;
|
||||||
|
use anyhow::Result;
|
||||||
|
use inquire::ui::{Attributes, Color, RenderConfig, StyleSheet};
|
||||||
|
use syntect::highlighting::{Highlighter, Theme};
|
||||||
|
use syntect::parsing::Scope;
|
||||||
|
|
||||||
|
const DEFAULT_INQUIRE_PROMPT_THEME: Color = Color::DarkYellow;
|
||||||
|
|
||||||
|
pub fn prompt_theme<'a>(render_options: RenderOptions) -> Result<RenderConfig<'a>> {
|
||||||
|
let theme = render_options.theme.as_ref();
|
||||||
|
let mut render_config = RenderConfig::default();
|
||||||
|
|
||||||
|
if let Some(theme_ref) = theme {
|
||||||
|
let prompt_color = resolve_foreground(theme_ref, "markup.heading")?
|
||||||
|
.unwrap_or(DEFAULT_INQUIRE_PROMPT_THEME);
|
||||||
|
|
||||||
|
render_config.prompt = StyleSheet::new()
|
||||||
|
.with_fg(prompt_color)
|
||||||
|
.with_attr(Attributes::BOLD);
|
||||||
|
render_config.selected_option = Some(
|
||||||
|
render_config
|
||||||
|
.selected_option
|
||||||
|
.unwrap_or(render_config.option)
|
||||||
|
.with_attr(
|
||||||
|
render_config
|
||||||
|
.selected_option
|
||||||
|
.unwrap_or(render_config.option)
|
||||||
|
.att
|
||||||
|
| Attributes::BOLD,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
render_config.selected_checkbox = render_config
|
||||||
|
.selected_checkbox
|
||||||
|
.with_attr(render_config.selected_checkbox.style.att | Attributes::BOLD);
|
||||||
|
render_config.option = render_config
|
||||||
|
.option
|
||||||
|
.with_attr(render_config.option.att | Attributes::BOLD);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(render_config)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_foreground(theme: &Theme, scope_str: &str) -> Result<Option<Color>> {
|
||||||
|
let scope = Scope::new(scope_str)?;
|
||||||
|
let style_mod = Highlighter::new(theme).style_mod_for_stack(&[scope]);
|
||||||
|
let fg = style_mod.foreground.or(theme.settings.foreground);
|
||||||
|
|
||||||
|
Ok(fg.map(|c| Color::Rgb {
|
||||||
|
r: c.r,
|
||||||
|
g: c.g,
|
||||||
|
b: c.b,
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
|
mod inquire;
|
||||||
mod markdown;
|
mod markdown;
|
||||||
mod stream;
|
mod stream;
|
||||||
|
|
||||||
|
pub use inquire::prompt_theme;
|
||||||
|
|
||||||
pub use self::markdown::{MarkdownRender, RenderOptions};
|
pub use self::markdown::{MarkdownRender, RenderOptions};
|
||||||
use self::stream::{markdown_stream, raw_stream};
|
use self::stream::{markdown_stream, raw_stream};
|
||||||
|
|
||||||
|
|||||||
+45
-2
@@ -6,7 +6,7 @@ use self::completer::ReplCompleter;
|
|||||||
use self::highlighter::ReplHighlighter;
|
use self::highlighter::ReplHighlighter;
|
||||||
use self::prompt::ReplPrompt;
|
use self::prompt::ReplPrompt;
|
||||||
|
|
||||||
use crate::client::{call_chat_completions, call_chat_completions_streaming};
|
use crate::client::{call_chat_completions, call_chat_completions_streaming, init_client, oauth};
|
||||||
use crate::config::{
|
use crate::config::{
|
||||||
AgentVariables, AssertState, Config, GlobalConfig, Input, LastMessage, StateFlags,
|
AgentVariables, AssertState, Config, GlobalConfig, Input, LastMessage, StateFlags,
|
||||||
macro_execute,
|
macro_execute,
|
||||||
@@ -17,6 +17,7 @@ use crate::utils::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::mcp::McpRegistry;
|
use crate::mcp::McpRegistry;
|
||||||
|
use crate::resolve_oauth_client;
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use crossterm::cursor::SetCursorStyle;
|
use crossterm::cursor::SetCursorStyle;
|
||||||
use fancy_regex::Regex;
|
use fancy_regex::Regex;
|
||||||
@@ -32,10 +33,15 @@ use std::{env, mem, process};
|
|||||||
|
|
||||||
const MENU_NAME: &str = "completion_menu";
|
const MENU_NAME: &str = "completion_menu";
|
||||||
|
|
||||||
static REPL_COMMANDS: LazyLock<[ReplCommand; 37]> = LazyLock::new(|| {
|
static REPL_COMMANDS: LazyLock<[ReplCommand; 39]> = LazyLock::new(|| {
|
||||||
[
|
[
|
||||||
ReplCommand::new(".help", "Show this help guide", AssertState::pass()),
|
ReplCommand::new(".help", "Show this help guide", AssertState::pass()),
|
||||||
ReplCommand::new(".info", "Show system info", AssertState::pass()),
|
ReplCommand::new(".info", "Show system info", AssertState::pass()),
|
||||||
|
ReplCommand::new(
|
||||||
|
".authenticate",
|
||||||
|
"Authenticate the current model client via OAuth (if configured)",
|
||||||
|
AssertState::pass(),
|
||||||
|
),
|
||||||
ReplCommand::new(
|
ReplCommand::new(
|
||||||
".edit config",
|
".edit config",
|
||||||
"Modify configuration file",
|
"Modify configuration file",
|
||||||
@@ -131,6 +137,11 @@ static REPL_COMMANDS: LazyLock<[ReplCommand; 37]> = LazyLock::new(|| {
|
|||||||
"Leave agent",
|
"Leave agent",
|
||||||
AssertState::True(StateFlags::AGENT),
|
AssertState::True(StateFlags::AGENT),
|
||||||
),
|
),
|
||||||
|
ReplCommand::new(
|
||||||
|
".clear todo",
|
||||||
|
"Clear the todo list and stop auto-continuation",
|
||||||
|
AssertState::True(StateFlags::AGENT),
|
||||||
|
),
|
||||||
ReplCommand::new(
|
ReplCommand::new(
|
||||||
".rag",
|
".rag",
|
||||||
"Initialize or access RAG",
|
"Initialize or access RAG",
|
||||||
@@ -421,6 +432,19 @@ pub async fn run_repl_command(
|
|||||||
}
|
}
|
||||||
None => println!("Usage: .model <name>"),
|
None => println!("Usage: .model <name>"),
|
||||||
},
|
},
|
||||||
|
".authenticate" => {
|
||||||
|
let current_model = config.read().current_model().clone();
|
||||||
|
let client = init_client(config, Some(current_model))?;
|
||||||
|
if !client.supports_oauth() {
|
||||||
|
bail!(
|
||||||
|
"Client '{}' doesn't either support OAuth or isn't configured to use it (i.e. uses an API key instead)",
|
||||||
|
client.name()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let clients = config.read().clients.clone();
|
||||||
|
let (client_name, provider) = resolve_oauth_client(Some(client.name()), &clients)?;
|
||||||
|
oauth::run_oauth_flow(&*provider, &client_name).await?;
|
||||||
|
}
|
||||||
".prompt" => match args {
|
".prompt" => match args {
|
||||||
Some(text) => {
|
Some(text) => {
|
||||||
config.write().use_prompt(text)?;
|
config.write().use_prompt(text)?;
|
||||||
@@ -785,6 +809,25 @@ pub async fn run_repl_command(
|
|||||||
Some("messages") => {
|
Some("messages") => {
|
||||||
bail!("Use '.empty session' instead");
|
bail!("Use '.empty session' instead");
|
||||||
}
|
}
|
||||||
|
Some("todo") => {
|
||||||
|
let mut cfg = config.write();
|
||||||
|
match cfg.agent.as_mut() {
|
||||||
|
Some(agent) => {
|
||||||
|
if !agent.auto_continue_enabled() {
|
||||||
|
bail!(
|
||||||
|
"The todo system is not enabled for this agent. Set 'auto_continue: true' in the agent's config.yaml to enable it."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if agent.todo_list().is_empty() {
|
||||||
|
println!("Todo list is already empty.");
|
||||||
|
} else {
|
||||||
|
agent.clear_todo_list();
|
||||||
|
println!("Todo list cleared.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => bail!("No active agent"),
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => unknown_command()?,
|
_ => unknown_command()?,
|
||||||
},
|
},
|
||||||
".vault" => match split_first_arg(args) {
|
".vault" => match split_first_arg(args) {
|
||||||
|
|||||||
+22
-11
@@ -6,7 +6,6 @@ use gman::providers::local::LocalProvider;
|
|||||||
use indoc::formatdoc;
|
use indoc::formatdoc;
|
||||||
use inquire::validator::Validation;
|
use inquire::validator::Validation;
|
||||||
use inquire::{Confirm, Password, PasswordDisplayMode, Text, min_length, required};
|
use inquire::{Confirm, Password, PasswordDisplayMode, Text, min_length, required};
|
||||||
use std::borrow::Cow;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
pub fn ensure_password_file_initialized(local_provider: &mut LocalProvider) -> Result<()> {
|
pub fn ensure_password_file_initialized(local_provider: &mut LocalProvider) -> Result<()> {
|
||||||
@@ -166,18 +165,30 @@ pub fn create_vault_password_file(vault: &mut Vault) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn interpolate_secrets<'a>(content: &'a str, vault: &Vault) -> (Cow<'a, str>, Vec<String>) {
|
pub fn interpolate_secrets(content: &str, vault: &Vault) -> (String, Vec<String>) {
|
||||||
let mut missing_secrets = vec![];
|
let mut missing_secrets = vec![];
|
||||||
let parsed_content = SECRET_RE.replace_all(content, |caps: &fancy_regex::Captures<'_>| {
|
let parsed_content: String = content
|
||||||
let secret = vault.get_secret(caps[1].trim(), false);
|
.lines()
|
||||||
match secret {
|
.map(|line| {
|
||||||
Ok(s) => s,
|
if line.trim_start().starts_with('#') {
|
||||||
Err(_) => {
|
return line.to_string();
|
||||||
missing_secrets.push(caps[1].to_string());
|
|
||||||
"".to_string()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
SECRET_RE
|
||||||
|
.replace_all(line, |caps: &fancy_regex::Captures<'_>| {
|
||||||
|
let secret = vault.get_secret(caps[1].trim(), false);
|
||||||
|
match secret {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => {
|
||||||
|
missing_secrets.push(caps[1].to_string());
|
||||||
|
"".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.to_string()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
(parsed_content, missing_secrets)
|
(parsed_content, missing_secrets)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user