mirror of
https://github.com/radareorg/r2ai.git
synced 2026-11-09 17:46:36 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9077de9162 | ||
|
|
287fad0ecd | ||
|
|
08afde5117 | ||
|
|
a086a12f1d | ||
|
|
4a86d0fc46 | ||
|
|
0509bf6755 | ||
|
|
1d55b7f72a | ||
|
|
222f0b7410 | ||
|
|
065288e29f | ||
|
|
5824aa4482 | ||
|
|
3c7ed851d8 | ||
|
|
1586d27e93 | ||
|
|
898855d7ce | ||
|
|
20a3751624 | ||
|
|
d81957d856 | ||
|
|
b6d7c9424c | ||
|
|
c3efc53c27 | ||
|
|
b6d234c154 | ||
|
|
ef7f49a68e | ||
|
|
d1fcea6859 | ||
|
|
96dcc0dd4f | ||
|
|
659e14a4cd | ||
|
|
926ef545d0 | ||
|
|
1b11c648de | ||
|
|
394e55cf87 | ||
|
|
8db446cf31 | ||
|
|
65d7d87539 | ||
|
|
372b38380f | ||
|
|
72b9ff2f49 | ||
|
|
742d3f2a51 | ||
|
|
1f3f93fa35 | ||
|
|
3f1b91fbce | ||
|
|
acd0513ebb | ||
|
|
c44e838238 | ||
|
|
f4fa4ba432 | ||
|
|
561db876ac | ||
|
|
bcdc182d03 | ||
|
|
19cba835e4 | ||
|
|
87a1e89112 | ||
|
|
012118c25c | ||
|
|
e2030b31a1 | ||
|
|
53f437d55a | ||
|
|
66f202254e | ||
|
|
8e9555d144 | ||
|
|
a8caa51e76 | ||
|
|
021aa205f0 | ||
|
|
92c86f94e6 | ||
|
|
50c1d63a47 | ||
|
|
79f01945b7 | ||
|
|
9043638e01 | ||
|
|
6bd0227382 | ||
|
|
9f9a3b87ae | ||
|
|
b68ddaeaed | ||
|
|
d2663f75dc | ||
|
|
e24dbab677 | ||
|
|
c765269b02 | ||
|
|
55b856e70b | ||
|
|
5e148bc882 | ||
|
|
9f93754483 | ||
|
|
d262fabb2b | ||
|
|
d0a1f292f5 | ||
|
|
dad02ac243 | ||
|
|
02c561c415 | ||
|
|
9414cdf72b | ||
|
|
7cc61c4cc5 | ||
|
|
e4875eeb37 | ||
|
|
888251a2ea | ||
|
|
af6153ad64 | ||
|
|
87e4e7b232 | ||
|
|
cc3a003a64 |
No files matched your search
+80
-32
@@ -1,5 +1,8 @@
|
||||
name: CI and Release
|
||||
|
||||
env:
|
||||
R2V: 6.2.6
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -23,7 +26,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Meson/Ninja
|
||||
if: matrix.build_system == 'meson'
|
||||
@@ -33,7 +36,9 @@ jobs:
|
||||
|
||||
- name: Install radare2
|
||||
id: r2
|
||||
uses: radareorg/github-actions@v2
|
||||
uses: radareorg/github-actions@v3
|
||||
with:
|
||||
version: ${{ env.R2V }}
|
||||
|
||||
- name: Build with Make
|
||||
if: matrix.build_system == 'make'
|
||||
@@ -56,46 +61,87 @@ jobs:
|
||||
if ($LASTEXITCODE -ne 0) { meson configure build }
|
||||
meson compile -C build
|
||||
|
||||
- name: Create release artifacts (tags only)
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p release
|
||||
# Collect common binary artefact names produced by Make/Meson
|
||||
# Include both "r2ai.*" and "libr2ai.*" variants for DLL/dylib/so on all platforms.
|
||||
find src -type f \( -iname "r2ai.dll" -o -iname "libr2ai.dll" -o -iname "r2ai.dylib" -o -iname "libr2ai.dylib" -o -iname "r2ai.so" -o -iname "libr2ai.so" -o -iname "r2ai.exe" \) -exec cp -f {} release/ \; || true
|
||||
# Also check top-level build output directories
|
||||
find . -maxdepth 3 -type f \( -iname "r2ai.dll" -o -iname "libr2ai.dll" -o -iname "r2ai.dylib" -o -iname "libr2ai.dylib" -o -iname "r2ai.so" -o -iname "libr2ai.so" -o -iname "r2ai.exe" \) -exec cp -f {} release/ \; || true
|
||||
cp -f decai/decai.r2.js release/ || true
|
||||
if command -v zip >/dev/null 2>&1; then
|
||||
zip -j "release/r2ai-${{ github.ref_name }}-${{ matrix.os }}-${{ matrix.build_system }}.zip" release/*
|
||||
else
|
||||
powershell.exe -Command "Compress-Archive -Path release/* -DestinationPath 'release/r2ai-${{ github.ref_name }}-${{ matrix.os }}-${{ matrix.build_system }}.zip'"
|
||||
fi
|
||||
bindist:
|
||||
name: Build r2pm binary (${{ matrix.os }})
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Upload artifacts (tags only)
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
- name: Install radare2
|
||||
uses: radareorg/github-actions@v3
|
||||
with:
|
||||
version: ${{ env.R2V }}
|
||||
|
||||
- name: Package r2pm binary
|
||||
run: make -C src bindist
|
||||
|
||||
- name: Upload r2pm binary artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ matrix.os }}-${{ matrix.build_system }}-binary
|
||||
path: release/r2ai-${{ github.ref_name }}-${{ matrix.os }}-${{ matrix.build_system }}.zip
|
||||
name: bindist-${{ matrix.os }}
|
||||
path: r2ai-*.zip
|
||||
if-no-files-found: error
|
||||
|
||||
# - name: Smoke test
|
||||
# run: |
|
||||
# if command -v r2 >/dev/null 2>&1; then
|
||||
# r2 -q -c 'r2ai -h' -- || true
|
||||
# fi
|
||||
windows-bindist:
|
||||
name: Build r2pm binary (Windows)
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Meson/Ninja
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install meson ninja
|
||||
|
||||
- name: Install radare2
|
||||
id: r2
|
||||
uses: radareorg/github-actions@v3
|
||||
with:
|
||||
version: ${{ env.R2V }}
|
||||
|
||||
- name: Build with Meson
|
||||
shell: powershell
|
||||
run: |
|
||||
cd src
|
||||
$prefix = "${{ steps.r2.outputs.prefix }}"
|
||||
meson setup build --backend ninja --prefix="$prefix" -Dr2_prefix="$prefix"
|
||||
if ($LASTEXITCODE -ne 0) { meson configure build }
|
||||
meson compile -C build
|
||||
|
||||
- name: Package r2pm binary
|
||||
shell: pwsh
|
||||
run: |
|
||||
New-Item -ItemType Directory -Force -Path bindist\plugins,bindist\bin,bindist\prompts | Out-Null
|
||||
Get-ChildItem src\build\ -Filter "*r2ai*.dll" -ErrorAction SilentlyContinue | Copy-Item -Destination bindist\plugins
|
||||
Get-ChildItem src\..\build\ -Filter "*r2ai*.dll" -ErrorAction SilentlyContinue | Copy-Item -Destination bindist\plugins
|
||||
Copy-Item src\build\r2ai.exe bindist\bin
|
||||
Copy-Item prompts\*.r2ai.md bindist\prompts
|
||||
$rv = (& radare2 -qv).Trim()
|
||||
Compress-Archive -Path bindist\* -DestinationPath "r2ai-$rv-windows-x86-64.zip" -Force
|
||||
|
||||
- name: Upload r2pm binary artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: windows-bindist
|
||||
path: r2ai-*.zip
|
||||
if-no-files-found: error
|
||||
|
||||
release:
|
||||
name: Create Release
|
||||
needs: build
|
||||
needs: [bindist, windows-bindist]
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Verify tag is on master
|
||||
run: |
|
||||
@@ -105,10 +151,12 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
|
||||
- name: Download artifacts
|
||||
- name: Download r2pm binary artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: '*bindist*'
|
||||
path: release-artifacts
|
||||
merge-multiple: true
|
||||
|
||||
- name: Add decai.r2.js to release artifacts
|
||||
run: cp decai/decai.r2.js release-artifacts/
|
||||
@@ -117,6 +165,6 @@ jobs:
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
files: |
|
||||
release-artifacts/**/*.zip
|
||||
release-artifacts/*.zip
|
||||
release-artifacts/decai.r2.js
|
||||
generate_release_notes: true
|
||||
generate_release_notes: true
|
||||
@@ -12,3 +12,6 @@ venv
|
||||
*.dylib
|
||||
*.a
|
||||
third_party
|
||||
|
||||
# r2pm binary packages
|
||||
r2ai-*.zip
|
||||
@@ -23,12 +23,12 @@ if [ -f ~/.r2ai.openai-key ]; then
|
||||
OPENAI_KEY=$(cat ~/.r2ai.openai-key)
|
||||
fi
|
||||
CLAUDE_KEY=""
|
||||
CLAUDE_MODEL="claude-3-5-sonnet-20241022"
|
||||
CLAUDE_MODEL="claude-sonnet-5-5"
|
||||
if [ -f ~/.r2ai.anthropic-key ]; then
|
||||
CLAUDE_KEY=$(cat ~/.r2ai.anthropic-key)
|
||||
fi
|
||||
DEEPSEEK_KEY=""
|
||||
DEEPSEEK_MODEL="claude-3-5-sonnet-20241022"
|
||||
DEEPSEEK_MODEL="claude-sonnet-5-5"
|
||||
if [ -f ~/.r2ai.deepseek-key ]; then
|
||||
DEEPSEEK_KEY=$(cat ~/.r2ai.anthropic-key)
|
||||
fi
|
||||
@@ -138,7 +138,7 @@ GEMINI_KEY=~/.r2ai-gemini.key
|
||||
OPENAI_KEY=~/.r2ai-openai.key
|
||||
CLAUDE_KEY=~/.r2ai-anthropic.key
|
||||
DEEPSEEK_KEY=~/.r2ai-deepseek.key
|
||||
CLAUDE_MODEL=claude-3-5-sonnet-20241022
|
||||
CLAUDE_MODEL=claude-sonnet-5-5
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ python3 main.py
|
||||
**Example selecting a remote models:**
|
||||
|
||||
```
|
||||
[r2ai:0x00006aa0]> -m anthropic:claude-3-7-sonnet-20250219
|
||||
[r2ai:0x00006aa0]> -m anthropic:claude-sonnet-5-5
|
||||
[r2ai:0x00006aa0]> -m openai:gpt-4
|
||||
```
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ Uncensored:
|
||||
Remote:
|
||||
-m openapi:http://localhost:5001
|
||||
-m openai:gpt-4
|
||||
-m anthropic:claude-3-7-sonnet-20250219
|
||||
-m anthropic:claude-sonnet-5-5
|
||||
-m bedrock:anthropic.claude-3-5-sonnet-20241022-v1
|
||||
-m kobaldcpp:http://localhost:5001
|
||||
"""
|
||||
@@ -80,12 +80,9 @@ OpenAI:
|
||||
-m openai:gpt-4
|
||||
-m openai:gpt-4-1106-preview
|
||||
Anthropic:
|
||||
-m anthropic:claude-2.1
|
||||
-m anthropic:claude-3-haiku-20240307
|
||||
-m anthropic:claude-3-opus-20240229
|
||||
-m anthropic:claude-3-sonnet-20240229
|
||||
-m anthropic:claude-3-5-sonnet-20241022
|
||||
-m anthropic:claude-3-7-sonnet-20250219
|
||||
-m anthropic:claude-opus-4-8
|
||||
-m anthropic:claude-sonnet-5-5
|
||||
-m anthropic:claude-haiku-4-5-20251001
|
||||
groq:
|
||||
-m groq:gemma-7b-it
|
||||
-m groq:llama2-70b-4096
|
||||
|
||||
@@ -170,8 +170,7 @@ def slurp_until(endword):
|
||||
def list_models():
|
||||
print("ollama/llama3:latest")
|
||||
print("ollama/granite-code:8b")
|
||||
print("anthropic/claude-3-5-sonnet-20240620")
|
||||
print("anthropic/claude-3-7-sonnet-20250219")
|
||||
print("anthropic/claude-sonnet-5-5")
|
||||
print("openai/gpt-4o")
|
||||
print("mistral/mistral-tiny")
|
||||
|
||||
|
||||
@@ -3,4 +3,7 @@ TARGETS=all clean mrproper user-install install user-uninstall uninstall fmt ind
|
||||
$(TARGETS):
|
||||
$(MAKE) -C src $@
|
||||
|
||||
.PHONY: $(TARGETS)
|
||||
test: all
|
||||
$(MAKE) -C test
|
||||
|
||||
.PHONY: $(TARGETS) test
|
||||
@@ -97,7 +97,7 @@ then you can type the commands you want to run when the r2ai plugin is loaded:
|
||||
|
||||
```console
|
||||
r2ai -e api=anthropic
|
||||
r2ai -e model=claude-3-7-sonnet-20250219
|
||||
r2ai -e model=claude-sonnet-5-5
|
||||
r2ai -e max_tokens=64000
|
||||
```
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ Configuration options:
|
||||
```console
|
||||
[0x00000000]> decai -e
|
||||
decai -e api=ollama
|
||||
decai -e apitype=chat
|
||||
decai -e host=http://localhost
|
||||
decai -e port=11434
|
||||
decai -e prompt=Rewrite this function and respond ONLY with code, NO explanations, NO markdown, Change 'goto' into if/else/for/while, Simplify as much as possible, use better variable names, take function arguments and strings from comments like 'string:'
|
||||
@@ -102,6 +103,12 @@ Persist user defaults in `~/.config/r2ai/decai.txt` with one `key=value` entry
|
||||
per line, then edit it with `decai -E`. That file is loaded on the first
|
||||
`decai` command in a session and reloaded after `decai -E` exits.
|
||||
|
||||
Use `decai -e http=curl` to force system curl, `http=r2` to require `r2.http()`,
|
||||
or `http=auto` (the default) to use `r2.http()` when available and curl otherwise.
|
||||
Failed `r2.http()` calls return an error without falling back to curl. The
|
||||
`timeout` setting applies only to system curl. Save `http=curl` in
|
||||
`~/.config/r2ai/decai.txt` to persist the override.
|
||||
|
||||
List possible APIs to discuss with AI: `decai -e api=?`:
|
||||
|
||||
```
|
||||
@@ -211,3 +218,21 @@ void daemonize() {
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
## WASM HTTP
|
||||
|
||||
With radare2's WASI HTTP build, decai defaults to `r2.http()` to reach the existing
|
||||
`r_socket_http_get/post` C APIs. Headers and JSON payloads go directly to the
|
||||
host HTTP adapter; no shell curl, temporary payload file or WebSocket proxy is
|
||||
needed. A failed/denied HTTP call is returned as an error, without a shell
|
||||
fallback. In `http=auto` mode, runtimes without `r2.http()` use system curl.
|
||||
|
||||
Enable HTTP in the embedding application (`enabled: true` in JavaScript or
|
||||
`network=True` in Python). The radare2 sandbox must permit both `socket` and
|
||||
`network`; process execution is not needed. Browser requests remain subject
|
||||
to CORS. The host adapter controls the HTTP deadline in WASM; decai's `timeout`
|
||||
setting still controls native curl only. Responses are buffered, not streamed.
|
||||
|
||||
This requires radareorg/radare2#27037 and the rebuilt decai script. For the
|
||||
C r2ai plugin compiled into WASM, the `auto` HTTP backend likewise selects
|
||||
radare2's C HTTP implementation; native backend selection is unchanged.
|
||||
+33
-30
@@ -1,7 +1,7 @@
|
||||
📦
|
||||
26677 /main.js
|
||||
27680 /main.js
|
||||
✄
|
||||
var ee="1.4.0",y="decai",w="~/.config/r2ai",M=w+"/apikeys.txt",I=w+"/decai.txt",N="Rewrite this pseudocode into concise and clean code. Output only the provided function. Do not add wrappers, helper examples, test code, or main-like functions. Replace goto with structured control flow, simplify as much as possible, infer types and use better names for variables and parameters, some strings may be appearing as comments, preserve only what is implied by the input, and remove dead code.",F={decai:`# Using Decai
|
||||
var te="1.5.4",b="decai",w="~/.config/r2ai",N=w+"/apikeys.txt",U=w+"/decai.txt",T="Rewrite this pseudocode into concise and clean code. Output only the provided function. Do not add wrappers, helper examples, test code, or main-like functions. Replace goto with structured control flow, simplify as much as possible, infer types and use better names for variables and parameters, some strings may be appearing as comments, preserve only what is implied by the input, and remove dead code.",L={decai:`# Using Decai
|
||||
|
||||
Decai is the radare2 plugin for decompiling functions with the help of language models.
|
||||
|
||||
@@ -23,8 +23,10 @@ These are the most recommended models for decompiling in local:
|
||||
|
||||
## Common Options
|
||||
* 'decai -e baseurl=<url>' override default host and port for API endpoint (e.g., 'http://localhost:11434')
|
||||
* 'decai -e apitype=chat|generate' choose the Ollama API endpoint (default: chat)
|
||||
* 'decai -e headers=Authorization: Bearer ...\\nUser-Agent: curl/8.7.1' add or override HTTP headers
|
||||
* "export DECAI_HEADERS='Authorization: Bearer ...\\nUser-Agent: curl/8.7.1'" set extra headers from the environment
|
||||
* 'decai -e http=auto|r2|curl' select the HTTP transport (default: auto, prefers r2.http)
|
||||
* 'decai -e timeout=0' disable curl timeouts, or set a positive number of seconds
|
||||
|
||||
* 'decai -e deterministic=true' to remove randomness from decompilation responses
|
||||
@@ -116,40 +118,41 @@ Use radare2 to resolve user requests.
|
||||
### Special cases
|
||||
|
||||
* On Swift binaries run...
|
||||
`};var x={decopipe:{use:!1},host:"http://localhost",port:"11434",baseurl:"",extraHeaders:[],api:"ollama",pipeline:"",commands:"pdc",yolo:!1,tts:!1,language:"C",humanLanguage:"English",deterministic:!0,debug:!1,timeout:180,think:"",useFiles:!1,contextFile:"",model:"",cache:!1,maxInputTokens:-1,prompt:N,lastOutput:""},r={...x,decopipe:{...x.decopipe},extraHeaders:[...x.extraHeaders]};function te(e){return(r2.cmd("-e dir.tmp").trim()??".")+"/"+e}function q(e,t){r2.cmd("'mkdir -p "+e),r2.cmd("'touch "+t)}function _(e){return(r2.cmd2("test -h").logs?.[0]?.message??"").includes("-fdx")?!0:r2.cmd("'test -vf "+e).startsWith("found")}function L(e,t){return e+" ".repeat(Math.max(0,t-e.length))}function ne(e){return e.replace(/\x1b\[[0-9;]*m/g,"")}function oe(e){let t=e.match(/```json\s*([\s\S]*?)```/);return t&&t[1]?t[1].trim():e}function re(e){let t=e,n=t.indexOf("{");n!==-1&&(t=t.slice(n));let o=t.lastIndexOf("}");return o!==-1&&(t=t.slice(0,o+1)),t}function G(e){return btoa(e)}function ie(e,t){let n=G(t);r2.cmd("p6ds "+n+" > "+e)}function v(e){let t=e;return t=t.replace(/<think>[\s\S]*?<\/think>/gi,""),t.split(`
|
||||
`};var E={decopipe:{use:!1},host:"http://localhost",port:"11434",baseurl:"",extraHeaders:[],api:"ollama",pipeline:"",commands:"pdc",yolo:!1,tts:!1,language:"C",humanLanguage:"English",deterministic:!0,debug:!1,timeout:180,http:"auto",think:"",apitype:"chat",useFiles:!1,contextFile:"",model:"",cache:!1,maxInputTokens:-1,prompt:T,lastOutput:""},r={...E,decopipe:{...E.decopipe},extraHeaders:[...E.extraHeaders]};function re(e){return(r2.cmd("-e dir.tmp").trim()??".")+"/"+e}function ne(e){if(!e.startsWith("~/"))return e;let t=r2.cmd("'%HOME").trim()||r2.cmd("'%USERPROFILE").trim();return t?t+e.slice(1):e}function H(e,t){r2.cmd("'mkdir -p "+ne(e)),r2.cmd("'touch "+ne(t))}function _(e){return(r2.cmd2("test -h").logs?.[0]?.message??"").includes("-fdx")?!0:r2.cmd("'test -vf "+e).startsWith("found")}function G(e,t){return e+" ".repeat(Math.max(0,t-e.length))}function h(e,t){return e.replace(/\/+$/,"")+"/"+t.replace(/^\/+/,"")}function oe(e){return e.replace(/\x1b\[[0-9;]*m/g,"")}function ie(e){let t=e.match(/```json\s*([\s\S]*?)```/);return t&&t[1]?t[1].trim():e}function ae(e){let t=e,n=t.indexOf("{");n!==-1&&(t=t.slice(n));let o=t.lastIndexOf("}");return o!==-1&&(t=t.slice(0,o+1)),t}function $(e){return btoa(e)}function se(e,t){let n=$(t);r2.cmd("p6ds "+n+" > "+e)}function k(e){let t=e;return t=t.replace(/<think>[\s\S]*?<\/think>/gi,""),t.split(`
|
||||
`).filter(n=>!n.trim().startsWith("```")).join(`
|
||||
`)}function $(e){r.debug&&console.log(e)}function ae(e){let t={};for(let n of e.split(/\r?\n/)){let o=n.trim();if(!o||o.startsWith("#"))continue;let[i,...a]=o.split("=");if(!i||a.length===0)continue;let s=a.join("=").trim(),l=i.toLowerCase().replace(/_api_key$/i,"");t[l]=s}return t}var Re=["DECAI_HEADERS","R2AI_HEADERS"];function Ie(e){let t=r2.cmd("'%"+e).trim();return t!==""&&!t.includes("=")?t:""}function _e(e){return e.trim().toLowerCase()}function Ue(e){let t=e.indexOf(":"),n=e.indexOf("="),o=t!==-1&&(n===-1||t<n)?t:n;if(o===-1)return null;let i=e.slice(0,o).trim();return i===""?null:{name:i,value:e.slice(o+1).trim()}}function Me(e){return e.value===""?`${e.name}:`:`${e.name}: ${e.value}`}function se(e,t){for(let n of e){let o=Ue(n.trim());o&&t.set(_e(o.name),Me(o))}}function J(e){let t=new Map,n=e.replace(/\\n/g,`
|
||||
`).split(/\r?\n/g).filter(o=>{let i=o.trim();return i&&!i.startsWith("#")});return se(n,t),Array.from(t.values())}function le(e){return e.join("\\n")}function E(...e){let t=new Map;for(let n of e)se(n,t);return Array.from(t.values())}function Ne(){for(let e of Re){let t=Ie(e);if(t)return J(t)}return[]}function ce(){return E(Ne(),r.extraHeaders)}function U(e){return`'${e.replace(/'/g,`'"'"'`)}'`}function qe(e){let{method:t,url:n,headers:o,payload:i}=e,a=["curl","-s"];if(r.timeout>0&&a.push("--max-time",String(r.timeout)),E(["Content-Type: application/json"],o).forEach(l=>a.push("-H",U(l))),t==="POST"){if(!i)throw new Error("Payload required for POST requests");let l=r2.fdump(i);a.push("--data-binary","@"+U(l),U(n));let u=a.join(" ")+"; rm "+U(l);return $(u),r2.syscmds(u)}else{a.push(U(n));let l=a.join(" ");return $(l),r2.syscmds(l)}}function Ke(e){let t=e.split(`
|
||||
`).filter(i=>i.trim()!=="");if(t.length<=1)return null;let n="",o=null;for(let i of t)try{let a=JSON.parse(i);if(a.error)return a;a.message?.content!==void 0&&(n+=a.message.content),o=a}catch{return null}return o?(o.message={role:"assistant",content:n},o):null}function ue(e){try{let t=qe(e).trim();if(t==="")return{error:"empty response"};try{return JSON.parse(t)}catch{let n=Ke(t);return n||{error:t,rawOutput:t}}}catch(t){return{error:t.message}}}function A(e,t){return ue({method:"GET",url:e,headers:t})}function de(e,t,n){return ue({method:"POST",url:e,headers:t,payload:n})}function W(e){return e.split("_")[0].toLowerCase()}function fe(e){if(!e.authKey)return;let t=Array.from(new Set([e.keyName,W(e.authKey)].filter(Boolean))),n;for(let o of t){let i=K(o,e.authKey);if(i[0])return i;!n&&i[2]!=="nope"&&(n=i)}return n||K(W(e.authKey),e.authKey)}function O(e){if(e)return typeof e=="string"?e:e.message||JSON.stringify(e)}function pe(e){return r.baseurl||e.defaultBaseUrl}function me(e,t=ce()){return E(e,t)}function Te(e){return e?["Authorization: Bearer "+e]:[]}function He(e){let t=["anthropic-version: 2023-06-01"];return e?E(t,["x-api-key: "+e]):t}function he(e,t){switch(e.authStyle||"none"){case"bearer":return Te(t);case"anthropic":return He(t);case"none":default:return[]}}function De(e,t,n){let o=A(e,t),i=O(o.error);return i?(console.error(i),"error invalid response"):o.data?.map(n).join(`
|
||||
`)||""}function je(e,t){let n=new Set;return e.filter(o=>{let i=t(o);return n.has(i)?!1:(n.add(i),!0)})}function z(e){let t=fe(e)?.[0]||null;return{baseUrl:pe(e),headers:me(he(e,t))}}function Be(e){let{baseUrl:t,headers:n}=z(e);return De(t+"/v1/models",n,o=>o.id)}function Fe(e){let{baseUrl:t,headers:n}=z(e),o=A(t+"/api/tags",n),i=O(o.error);return i?(console.error(i),"error invalid response"):o.models?.map(a=>a.name).join(`
|
||||
`)||""}function Le(e){let{baseUrl:t,headers:n}=z(e),o=A(t+"/v1/models",n);return o.data?je(o.data,i=>i.name||i.id).map(i=>[L(i.name||i.id,30),L(""+(i.max_context_length||""),10),i.description||""].join(" ")).join(`
|
||||
`):""}var Ge={openai:{buildPayload:(e,t)=>({stream:!1,model:e,messages:[{role:"user",content:t}]}),parseResponse:e=>{if(e.error)throw new Error(O(e.error)||"Unknown error");if(e.choices&&e.choices[0]?.message?.content)return v(e.choices[0].message.content);throw new Error("Invalid response format")},buildUrl:e=>e+"/v1/chat/completions"},anthropic:{buildPayload:(e,t)=>{let n={model:e,max_tokens:5128,messages:[{role:"user",content:t}]};return Y()&&(n.thinking={type:"enabled",budget_tokens:4096},n.max_tokens=16e3),r.deterministic&&!Y()&&Object.assign(n,{temperature:0,top_p:0,top_k:1}),n},parseResponse:e=>{if(e.content&&Array.isArray(e.content)){let t=[];for(let n of e.content)n.text&&t.push(n.text);if(t.length>0)return v(t.join(`
|
||||
`))}throw e.error?new Error(O(e.error)||"Unknown error"):new Error("Invalid response format")},buildUrl:e=>e+"/v1/messages"},ollama:{buildPayload:(e,t)=>{let n={stream:!1,model:e,messages:[{role:"user",content:t}]};return Y()?r.think==="true"||r.think==="1"?n.think=!0:n.think=r.think:n.think=!1,r.deterministic&&(n.options={repeat_last_n:0,top_p:1,top_k:1,temperature:0,repeat_penalty:1,seed:123}),n},parseResponse:e=>{if(e.error)throw new Error(O(e.error));if(e.message?.content)return v(e.message.content);throw new Error(JSON.stringify(e))},buildUrl:e=>e+"/api/chat"},gemini:{buildPayload:(e,t)=>{let n={contents:[{parts:[{text:t}]}]},o={};return r.deterministic&&Object.assign(o,{temperature:0,topP:1,topK:1}),r.think!==""&&(Q()?o.thinkingConfig={thinkingBudget:0}:o.thinkingConfig={thinkingBudget:8192}),Object.keys(o).length>0&&(n.generationConfig=o),n},parseResponse:e=>{if(e.candidates&&e.candidates[0]?.content?.parts){let n=e.candidates[0].content.parts.filter(o=>!o.thought&&o.text).map(o=>o.text);if(n.length>0)return v(n.join(`
|
||||
`))}throw e.error?new Error(O(e.error)):(console.log(JSON.stringify(e)),new Error("Invalid response format"))},buildUrl:(e,t,n)=>`${e}/v1beta/models/${t}:generateContent?key=${n}`,requiresUrlApiKey:!0}},T={anthropic:{defaultModel:"claude-3-7-sonnet-20250219",defaultBaseUrl:"https://api.anthropic.com",authKey:"ANTHROPIC_API_KEY",keyName:"anthropic",authStyle:"anthropic",apiStyle:"anthropic"},claude:{defaultModel:"claude-3-7-sonnet-20250219",defaultBaseUrl:"https://api.anthropic.com",authKey:"ANTHROPIC_API_KEY",keyName:"claude",authStyle:"anthropic",apiStyle:"anthropic"},openai:{defaultModel:"gpt-4o-mini",defaultBaseUrl:"https://api.openai.com",authKey:"OPENAI_API_KEY",authStyle:"bearer",apiStyle:"openai"},ollama:{defaultModel:"qwen2.5-coder:latest",defaultBaseUrl:"http://localhost:11434",authStyle:"none",apiStyle:"ollama"},ollamacloud:{defaultModel:"gpt-oss:120b",defaultBaseUrl:"https://ollama.com",authKey:"OLLAMA_API_KEY",keyName:"ollamacloud",authStyle:"bearer",apiStyle:"ollama"},zen:{defaultModel:"big-pickle",defaultBaseUrl:"https://opencode.ai/zen",authKey:"OPENCODE_API_KEY",keyName:"zen",authStyle:"bearer",apiStyle:"openai",hardcodedModels:["big-pickle","glm-5","kimi-k2.5"]},gemini:{defaultModel:"gemini-2.5-flash",defaultBaseUrl:"https://generativelanguage.googleapis.com",authKey:"GEMINI_API_KEY",authStyle:"none",apiStyle:"gemini",hardcodedModels:["gemini-2.0-flash","gemini-2.0-flash-lite","gemini-2.5-pro","gemini-2.5-flash","gemini-2.5-flash-lite"]},mistral:{defaultModel:"codestral-latest",defaultBaseUrl:"https://api.mistral.ai",authKey:"MISTRAL_API_KEY",authStyle:"bearer",apiStyle:"openai"},xai:{defaultModel:"grok-beta",defaultBaseUrl:"https://api.x.ai",authKey:"XAI_API_KEY",authStyle:"bearer",apiStyle:"openai",hardcodedModels:["grok-2","grok-beta"]},lmstudio:{defaultModel:"local-model",defaultBaseUrl:"http://127.0.0.1:1234",authStyle:"none",apiStyle:"openai"},deepseek:{defaultModel:"deepseek-coder",defaultBaseUrl:"https://api.deepseek.com",authKey:"DEEPSEEK_API_KEY",authStyle:"bearer",apiStyle:"openai",hardcodedModels:["deepseek-coder","deepseek-chat"]},openrouter:{defaultModel:"openai/gpt-4o-mini",defaultBaseUrl:"https://openrouter.ai/api",authKey:"OPENROUTER_API_KEY",authStyle:"bearer",apiStyle:"openai"},groq:{defaultModel:"llama-3.3-70b-versatile",defaultBaseUrl:"https://api.groq.com/openai",authKey:"GROQ_API_KEY",authStyle:"bearer",apiStyle:"openai"}};function H(e){return T[e]}function V(){return Object.keys(T)}function ge(e){let t=H(e);if(!t)throw new Error(`Unknown provider: ${e}`);if(e==="mistral")return Le(t);switch(t.apiStyle){case"openai":case"anthropic":return Be(t);case"ollama":return Fe(t);case"gemini":return"";default:return""}}function Q(){return r.think==="false"||r.think==="0"}function Y(){return r.think!==""&&!Q()}function $e(e,t,n){let o=e;return r.think!==""&&n==="openai"&&(Q()?(o+=' Answers directly and concisely, without showing any thinking steps or internal reasoning. Never include phrases like "Let me think".',o+=" /no_think"):o="Think step by step and explain the reasoning process, When answering, first output your reasoning inside <think> and </think> tags, then give the final answer."+o),t?o:r.prompt+Je()+o}function Je(){return`
|
||||
`)}function J(e){r.debug&&console.log(e)}function le(e){let t={};for(let n of e.split(/\r?\n/)){let o=n.trim();if(!o||o.startsWith("#"))continue;let[i,...a]=o.split("=");if(!i||a.length===0)continue;let s=a.join("=").trim(),l=i.toLowerCase().replace(/_api_key$/i,"");t[l]=s}return t}var Ue=["DECAI_HEADERS","R2AI_HEADERS"];function _e(e){let t=r2.cmd("'%"+e).trim();return t!==""&&!t.includes("=")?t:""}function Me(e){return e.trim().toLowerCase()}function Ne(e){let t=e.indexOf(":"),n=e.indexOf("="),o=t!==-1&&(n===-1||t<n)?t:n;if(o===-1)return null;let i=e.slice(0,o).trim();return i===""?null:{name:i,value:e.slice(o+1).trim()}}function Te(e){return e.value===""?`${e.name}:`:`${e.name}: ${e.value}`}function ce(e,t){for(let n of e){let o=Ne(n.trim());o&&t.set(Me(o.name),Te(o))}}function W(e){let t=new Map,n=e.replace(/\\n/g,`
|
||||
`).split(/\r?\n/g).filter(o=>{let i=o.trim();return i&&!i.startsWith("#")});return ce(n,t),Array.from(t.values())}function ue(e){return e.join("\\n")}function A(...e){let t=new Map;for(let n of e)ce(n,t);return Array.from(t.values())}function He(){for(let e of Ue){let t=_e(e);if(t)return W(t)}return[]}function de(){return A(He(),r.extraHeaders)}function M(e){return`'${e.replace(/'/g,`'"'"'`)}'`}function Ke(e){let{method:t,url:n,headers:o,payload:i}=e,a=A(["Content-Type: application/json"],o);if(r.http!=="curl"){if(r2.http)return r2.http(t,n,a.join(`
|
||||
`),i??"").body;if(r.http==="r2")throw new Error("r2.http is not available in this runtime")}let s=["curl","-s"];if(r.timeout>0&&s.push("--max-time",String(r.timeout)),a.forEach(l=>s.push("-H",M(l))),t==="POST"){if(!i)throw new Error("Payload required for POST requests");let l=r2.fdump(i);s.push("--data-binary","@"+M(l),M(n));let u=s.join(" ")+"; rm "+M(l);return J(u),r2.syscmds(u)}else{s.push(M(n));let l=s.join(" ");return J(l),r2.syscmds(l)}}function qe(e){let t=e.split(`
|
||||
`).filter(i=>i.trim()!=="");if(t.length<=1)return null;let n="",o=null;for(let i of t)try{let a=JSON.parse(i);if(a.error)return a;a.message?.content!==void 0&&(n+=a.message.content),o=a}catch{return null}return o?(o.message={role:"assistant",content:n},o):null}function pe(e){try{let t=Ke(e).trim();if(t==="")return{error:"empty response"};try{return JSON.parse(t)}catch{let n=qe(t);return n||{error:t,rawOutput:t}}}catch(t){return{error:t.message}}}function O(e,t){return pe({method:"GET",url:e,headers:t})}function fe(e,t,n){return pe({method:"POST",url:e,headers:t,payload:n})}function z(e){return e.split("_")[0].toLowerCase()}function me(e){if(!e.authKey)return;let t=Array.from(new Set([e.keyName,z(e.authKey)].filter(Boolean))),n;for(let o of t){let i=K(o,e.authKey);if(i[0])return i;!n&&i[2]!=="nope"&&(n=i)}return n||K(z(e.authKey),e.authKey)}function S(e){if(e)return typeof e=="string"?e:e.message||JSON.stringify(e)}function he(e){return r.baseurl||e.defaultBaseUrl}function ge(e,t=de()){return A(e,t)}function De(e){return e?["Authorization: Bearer "+e]:[]}function je(e){let t=["anthropic-version: 2023-06-01"];return e?A(t,["x-api-key: "+e]):t}function ye(e,t){switch(e.authStyle||"none"){case"bearer":return De(t);case"anthropic":return je(t);case"none":default:return[]}}function Be(e,t,n){let o=O(e,t),i=S(o.error);return i?(console.error(i),"error invalid response"):o.data?.map(n).join(`
|
||||
`)||""}function Fe(e,t){let n=new Set;return e.filter(o=>{let i=t(o);return n.has(i)?!1:(n.add(i),!0)})}function V(e){let t=me(e)?.[0]||null;return{baseUrl:he(e),headers:ge(ye(e,t))}}function Le(e){let{baseUrl:t,headers:n}=V(e),o=h(t,"v1/models");return Be(o,n,i=>i.id)}function Ge(e){let{baseUrl:t,headers:n}=V(e),o=O(h(t,"api/tags"),n),i=S(o.error);return i?(console.error(i),"error invalid response"):o.models?.map(a=>a.name).join(`
|
||||
`)||""}function $e(e){let{baseUrl:t,headers:n}=V(e),o=O(h(t,"v1/models"),n);return o.data?Fe(o.data,i=>i.name||i.id).map(i=>[G(i.name||i.id,30),G(""+(i.max_context_length||""),10),i.description||""].join(" ")).join(`
|
||||
`):""}var Je={openai:{buildPayload:(e,t)=>({stream:!1,model:e,messages:[{role:"user",content:t}]}),parseResponse:e=>{if(e.error)throw new Error(S(e.error)||"Unknown error");if(e.choices&&e.choices[0]?.message?.content)return k(e.choices[0].message.content);throw new Error("Invalid response format")},buildUrl:e=>h(e,"v1/chat/completions")},anthropic:{buildPayload:(e,t)=>{let n={model:e,max_tokens:5128,messages:[{role:"user",content:t}]};return Y()&&(n.thinking={type:"enabled",budget_tokens:4096},n.max_tokens=16e3),r.deterministic&&!Y()&&Object.assign(n,{temperature:0,top_p:0,top_k:1}),n},parseResponse:e=>{if(e.content&&Array.isArray(e.content)){let t=[];for(let n of e.content)n.text&&t.push(n.text);if(t.length>0)return k(t.join(`
|
||||
`))}throw e.error?new Error(S(e.error)||"Unknown error"):new Error("Invalid response format")},buildUrl:e=>h(e,"v1/messages")},ollama:{buildPayload:(e,t)=>{let n={stream:!1,model:e};return r.apitype==="generate"?n.prompt=t:n.messages=[{role:"user",content:t}],Y()?r.think==="true"||r.think==="1"?n.think=!0:n.think=r.think:n.think=!1,r.deterministic&&(n.options={repeat_last_n:0,top_p:1,top_k:1,temperature:0,repeat_penalty:1,seed:123}),n},parseResponse:e=>{if(e.error)throw new Error(S(e.error));if(typeof e.response=="string")return k(e.response);if(e.message?.content)return k(e.message.content);throw new Error(JSON.stringify(e))},buildUrl:e=>h(e,"api/"+r.apitype)},gemini:{buildPayload:(e,t)=>{let n={contents:[{parts:[{text:t}]}]},o={};return r.deterministic&&Object.assign(o,{temperature:0,topP:1,topK:1}),r.think!==""&&(X()?o.thinkingConfig={thinkingBudget:0}:o.thinkingConfig={thinkingBudget:8192}),Object.keys(o).length>0&&(n.generationConfig=o),n},parseResponse:e=>{if(e.candidates&&e.candidates[0]?.content?.parts){let n=e.candidates[0].content.parts.filter(o=>!o.thought&&o.text).map(o=>o.text);if(n.length>0)return k(n.join(`
|
||||
`))}throw e.error?new Error(S(e.error)):(console.log(JSON.stringify(e)),new Error("Invalid response format"))},buildUrl:(e,t,n)=>h(e,`v1beta/models/${t}:generateContent?key=${n}`),requiresUrlApiKey:!0}},q={anthropic:{defaultModel:"claude-sonnet-5-5",defaultBaseUrl:"https://api.anthropic.com",authKey:"ANTHROPIC_API_KEY",keyName:"anthropic",authStyle:"anthropic",apiStyle:"anthropic"},claude:{defaultModel:"claude-sonnet-5-5",defaultBaseUrl:"https://api.anthropic.com",authKey:"ANTHROPIC_API_KEY",keyName:"claude",authStyle:"anthropic",apiStyle:"anthropic"},openai:{defaultModel:"gpt-4o-mini",defaultBaseUrl:"https://api.openai.com",authKey:"OPENAI_API_KEY",authStyle:"bearer",apiStyle:"openai"},ollama:{defaultModel:"qwen2.5-coder:latest",defaultBaseUrl:"http://localhost:11434",authStyle:"none",apiStyle:"ollama"},ollamacloud:{defaultModel:"gpt-oss:120b",defaultBaseUrl:"https://ollama.com",authKey:"OLLAMA_API_KEY",keyName:"ollamacloud",authStyle:"bearer",apiStyle:"ollama"},zen:{defaultModel:"big-pickle",defaultBaseUrl:"https://opencode.ai/zen",authKey:"OPENCODE_API_KEY",keyName:"zen",authStyle:"bearer",apiStyle:"openai",hardcodedModels:["big-pickle","glm-5","kimi-k2.5"]},gemini:{defaultModel:"gemini-2.5-flash",defaultBaseUrl:"https://generativelanguage.googleapis.com",authKey:"GEMINI_API_KEY",authStyle:"none",apiStyle:"gemini",hardcodedModels:["gemini-2.0-flash","gemini-2.0-flash-lite","gemini-2.5-pro","gemini-2.5-flash","gemini-2.5-flash-lite"]},mistral:{defaultModel:"codestral-latest",defaultBaseUrl:"https://api.mistral.ai",authKey:"MISTRAL_API_KEY",authStyle:"bearer",apiStyle:"openai"},xai:{defaultModel:"grok-beta",defaultBaseUrl:"https://api.x.ai",authKey:"XAI_API_KEY",authStyle:"bearer",apiStyle:"openai",hardcodedModels:["grok-2","grok-beta"]},lmstudio:{defaultModel:"local-model",defaultBaseUrl:"http://127.0.0.1:1234",authStyle:"none",apiStyle:"openai"},deepseek:{defaultModel:"deepseek-coder",defaultBaseUrl:"https://api.deepseek.com",authKey:"DEEPSEEK_API_KEY",authStyle:"bearer",apiStyle:"openai",hardcodedModels:["deepseek-coder","deepseek-chat"]},openrouter:{defaultModel:"openai/gpt-4o-mini",defaultBaseUrl:"https://openrouter.ai/api",authKey:"OPENROUTER_API_KEY",authStyle:"bearer",apiStyle:"openai"},groq:{defaultModel:"llama-3.3-70b-versatile",defaultBaseUrl:"https://api.groq.com/openai",authKey:"GROQ_API_KEY",authStyle:"bearer",apiStyle:"openai"}};function D(e){return q[e]}function Q(){return Object.keys(q)}function be(e){let t=D(e);if(!t)throw new Error(`Unknown provider: ${e}`);if(e==="mistral")return $e(t);switch(t.apiStyle){case"openai":case"anthropic":return Le(t);case"ollama":return Ge(t);case"gemini":return"";default:return""}}function X(){return r.think==="false"||r.think==="0"}function Y(){return r.think!==""&&!X()}function We(e,t,n){let o=e;return r.think!==""&&n==="openai"&&(X()?(o+=' Answers directly and concisely, without showing any thinking steps or internal reasoning. Never include phrases like "Let me think".',o+=" /no_think"):o="Think step by step and explain the reasoning process, When answering, first output your reasoning inside <think> and </think> tags, then give the final answer."+o),t?o:r.prompt+Ye()+o}function Ye(){return`
|
||||
.Translate the code into `+r.language+` programming language
|
||||
`}function Ye(e,t,n,o){let i=r.model||e.defaultModel,a=$e(n,o,e.apiStyle),s=fe(e);if(t.requiresUrlApiKey&&s?.[1]&&e.authKey)return`Cannot read ~/.r2ai.${W(e.authKey)}-key`;let l=t.buildPayload(i,a),u=he(e,s?.[0]||null);e.apiStyle==="ollama"&&u.push("Accept: application/x-ndjson");let k=me(u),c=t.buildUrl(pe(e),i,s?.[0]||void 0);try{return t.parseResponse(de(c,k,JSON.stringify(l)))}catch(d){return"ERROR: "+d.message}}function ye(e,t){let n=H(r.api);return n?Ye(n,Ge[n.apiStyle],e,t):`Unknown value for 'decai -e api'. Available: ${V().join(", ")}`}function We(){return Object.fromEntries(Object.entries(T).filter(([,e])=>e.authKey).map(([e,t])=>[e,t.authKey]))}function K(e,t){let n=r2.cmd("'%"+t).trim();if(!n.includes("=")&&n!=="")return[n.trim(),null,"env"];let o=e.toLowerCase(),i=M;if(_(i)){let s=r2.cmd("'cat "+i),l=ae(s);if(o in l)return[l[o],null,"txt"]}let a="~/.r2ai."+o+"-key";if(_(a)){let s=r2.cmd("'cat "+a);return s===""?[null,"Cannot read "+a,"no"]:[s.trim(),null,"file"]}return[null,"Not available","nope"]}function be(){q(w,M),r2.cmd("'ed "+M)}function X(){Object.entries(We()).forEach(([e,t])=>{let n=K(e,t)[2];console.log(n," ",e)})}function ke(e){let t=H(e);if(!t){console.error(`Unknown provider: ${e}`);return}try{let n=ge(e);n&&console.log(n),t.hardcodedModels&&t.hardcodedModels.forEach(o=>console.log(o)),e==="mistral"&&console.log("ministral-8b-latest"),!n&&!t.hardcodedModels&&console.log(t.defaultModel)}catch(n){let o=n;console.error(`Error listing models for ${e}:`,o.message),console.log(t.defaultModel)}}var Z=!1;function ze(e){return e==="true"||e==="1"}function S(e){let t=r;return{get:()=>t[e],set:n=>{t[e]=ze(n)}}}function P(e){let t=r;return{get:()=>t[e],set:n=>{t[e]=n}}}var xe={pipeline:{get:()=>r.pipeline,set:e=>{r.pipeline=e;try{r.decopipe=JSON.parse(r2.cmd("cat "+e))}catch(t){console.error(t)}}},model:{get:()=>r.model,set:e=>{e==="?"?ke(r.api):r.model=e.trim()}},deterministic:S("deterministic"),files:S("useFiles"),think:{get:()=>r.think||"false",set:e=>{r.think=e}},debug:S("debug"),timeout:{get:()=>r.timeout,set:e=>{r.timeout=Math.max(0,parseInt(e,10)||0)}},api:{get:()=>r.api,set:e=>{e==="?"?console.error(V().join(`
|
||||
`)):r.api=e}},lang:P("language"),hlang:P("humanLanguage"),cache:S("cache"),cmds:P("commands"),tts:S("tts"),yolo:S("yolo"),prompt:P("prompt"),ctxfile:P("contextFile"),baseurl:P("baseurl"),headers:{get:()=>le(r.extraHeaders),set:e=>{r.extraHeaders=J(e)}},maxtokens:{get:()=>r.maxInputTokens,set:e=>{r.maxInputTokens=parseInt(e,10)||-1}}};function Ve(){let e={host:r.host,port:r.port,lastOutput:r.lastOutput};Object.assign(r,x,e),r.decopipe={...x.decopipe},r.extraHeaders=[...x.extraHeaders]}function Qe(e){let t=e.trim();return!t||t.startsWith("#")?null:t.startsWith("decai -e ")?t.slice(9).trim():t}function b(e){let t=e.indexOf("="),n=t===-1?e:e.slice(0,t),o=t===-1?void 0:e.slice(t+1),i=xe[n];if(!i){console.error("Unknown config key");return}typeof o<"u"?i.set(o):console.log(i.get())}function ve(){Object.entries(xe).forEach(([e,t])=>{let n=t.get();console.log("decai -e "+e+"="+n)})}function we(){if(Ve(),!_(I))return;let e=r2.call("cat "+I);for(let t of e.split(/\r?\n/)){let n=Qe(t);n&&n[0]!="#"&&b(n)}}function Ee(){Z||(we(),Z=!0)}function Ae(){q(w,I),r2.cmd("'ed "+I),we(),Z=!0}function f(e,t,n=!1){let o=(t||"").replace(/`/g,""),i=e.replace(/'/g,"");if(r.api==="r2"||r.api==="r2ai"){let s=te(".pdc.txt");ie(s,o);let l=i.startsWith("-")?i:["-i",s,i].join(" "),k=(r.baseurl?r.baseurl+"/cmd":r.host+":"+r.port+"/cmd")+"/"+encodeURIComponent(l),c=A(k,[]);return c.error?`Error: ${c.error}`:c.result||JSON.stringify(c)||"Cannot curl, use r2ai-server or r2ai -w"}if(i.startsWith("-"))return"";let a=i+`:
|
||||
`+o;return r.maxInputTokens>0&&a.length>r.maxInputTokens&&(a=a.slice(0,r.maxInputTokens)),ye(a,n)}function Se(e){let t=r2.cmd("e scr.color");r2.cmd("e scr.color=0");try{return e()}finally{r2.cmd("e scr.color="+t)}}function Oe(){let e="",t=n=>e+=" "+y+" "+n+`
|
||||
`;e+="Usage: "+y+` (-h) ...
|
||||
`,e+="Version: "+ee+`
|
||||
`,t("-a [query] - solve query with auto mode"),t("-b [url] - set base URL (alias for decai -e baseurl)"),t("-d [f1 ..] - decompile given functions"),t("-dd [..] - same as above, but ignoring cache"),t("-dD [query] - decompile current function with given extra query"),t("-dr - decompile function and its called ones (recursive)"),t("-e - display and change eval config vars"),t("-E - edit and run decai.txt"),t("-h - show this help"),t("-H - help setting up r2ai"),t("-i [f] [q] - include given file and query"),t("-k - list API key status"),t("-K - edit apikeys.txt"),t("-m [model] - use -m? or -e model=? to list the available models"),t("-n - suggest better function name"),t("-p [provider] - same as decai -e api (will be provider)"),t("-q [text] - query language model with given text"),t("-Q [text] - query on top of the last output"),t("-r [prompt] - change role prompt (same as: decai -e prompt)"),t("-R - reset role prompt to default prompt"),t("-s - function signature"),t("-v - show local variables"),t("-V - find vulnerabilities"),t("-x[*] - eXplain current function (-x* for r2 script)"),r2.log(e.trim())}function D(e,t,n,o){if(n){let s=r2.cmd("anos").trim();if(s.length>0)return s}let i="";if(o){let s=r2.cmd("s");i+=`## Context functions:
|
||||
`}function ze(e,t,n,o){let i=r.model||e.defaultModel,a=We(n,o,e.apiStyle),s=me(e);if(t.requiresUrlApiKey&&s?.[1]&&e.authKey)return`Cannot read ~/.r2ai.${z(e.authKey)}-key`;let l=t.buildPayload(i,a),u=ye(e,s?.[0]||null);e.apiStyle==="ollama"&&u.push("Accept: application/x-ndjson");let v=ge(u),c=t.buildUrl(he(e),i,s?.[0]||void 0);try{return t.parseResponse(fe(c,v,JSON.stringify(l)))}catch(d){return"ERROR: "+d.message}}function ke(e,t){let n=D(r.api);return n?ze(n,Je[n.apiStyle],e,t):`Unknown value for 'decai -e api'. Available: ${Q().join(", ")}`}function Ve(){return Object.fromEntries(Object.entries(q).filter(([,e])=>e.authKey).map(([e,t])=>[e,t.authKey]))}function K(e,t){let n=r2.cmd("'%"+t).trim();if(!n.includes("=")&&n!=="")return[n.trim(),null,"env"];let o=e.toLowerCase(),i=N;if(_(i)){let s=r2.cmd("'cat "+i),l=le(s);if(o in l)return[l[o],null,"txt"]}let a="~/.r2ai."+o+"-key";if(_(a)){let s=r2.cmd("'cat "+a);return s===""?[null,"Cannot read "+a,"no"]:[s.trim(),null,"file"]}return[null,"Not available","nope"]}function xe(){H(w,N),r2.cmd("'ed "+N)}function Z(){Object.entries(Ve()).forEach(([e,t])=>{let n=K(e,t)[2];console.log(n," ",e)})}function ve(e){let t=D(e);if(!t){console.error(`Unknown provider: ${e}`);return}try{let n=be(e);n&&console.log(n),t.hardcodedModels&&t.hardcodedModels.forEach(o=>console.log(o)),e==="mistral"&&console.log("ministral-8b-latest"),!n&&!t.hardcodedModels&&console.log(t.defaultModel)}catch(n){let o=n;console.error(`Error listing models for ${e}:`,o.message),console.log(t.defaultModel)}}var ee=!1;function Qe(e){return e==="true"||e==="1"}function P(e){let t=r;return{get:()=>t[e],set:n=>{t[e]=Qe(n)}}}function R(e){let t=r;return{get:()=>t[e],set:n=>{t[e]=n}}}function Xe(e){let t=e.trim();return t==="chat"?"chat":t==="generate"?"generate":null}var Ee={pipeline:{get:()=>r.pipeline,set:e=>{r.pipeline=e;try{r.decopipe=JSON.parse(r2.cmd("cat "+e))}catch(t){console.error(t)}}},model:{get:()=>r.model,set:e=>{e==="?"?ve(r.api):r.model=e.trim()}},deterministic:P("deterministic"),files:P("useFiles"),think:{get:()=>r.think||"false",set:e=>{r.think=e}},apitype:{get:()=>r.apitype,set:e=>{let t=Xe(e);t?r.apitype=t:console.error("Invalid apitype. Use chat or generate.")}},debug:P("debug"),http:{get:()=>r.http,set:e=>{let t=e.trim();t==="auto"||t==="r2"||t==="curl"?r.http=t:console.error("Invalid http backend. Use auto, r2 or curl.")}},timeout:{get:()=>r.timeout,set:e=>{r.timeout=Math.max(0,parseInt(e,10)||0)}},api:{get:()=>r.api,set:e=>{e==="?"?console.error(Q().join(`
|
||||
`)):r.api=e}},lang:R("language"),hlang:R("humanLanguage"),cache:P("cache"),cmds:R("commands"),tts:P("tts"),yolo:P("yolo"),prompt:R("prompt"),ctxfile:R("contextFile"),baseurl:R("baseurl"),headers:{get:()=>ue(r.extraHeaders),set:e=>{r.extraHeaders=W(e)}},maxtokens:{get:()=>r.maxInputTokens,set:e=>{r.maxInputTokens=parseInt(e,10)||-1}}};function Ze(){let e={host:r.host,port:r.port,lastOutput:r.lastOutput};Object.assign(r,E,e),r.decopipe={...E.decopipe},r.extraHeaders=[...E.extraHeaders]}function et(e){let t=e.trim();return!t||t.startsWith("#")?null:t.startsWith("decai -e ")?t.slice(9).trim():t}function x(e){let t=e.indexOf("="),n=t===-1?e:e.slice(0,t),o=t===-1?void 0:e.slice(t+1),i=Ee[n];if(!i){console.error("Unknown config key");return}typeof o<"u"?i.set(o):console.log(i.get())}function we(){Object.entries(Ee).forEach(([e,t])=>{let n=t.get();console.log("decai -e "+e+"="+n)})}function Ae(){if(Ze(),!_(U))return;let e=r2.call("cat "+U);for(let t of e.split(/\r?\n/)){let n=et(t);n&&n[0]!="#"&&x(n)}}function Oe(){ee||(Ae(),ee=!0)}function Se(){H(w,U),r2.cmd("'ed "+U),Ae(),ee=!0}function p(e,t,n=!1){let o=(t||"").replace(/`/g,""),i=e.replace(/'/g,"");if(r.api==="r2"||r.api==="r2ai"){let s=re(".pdc.txt");se(s,o);let l=i.startsWith("-")?i:["-i",s,i].join(" "),u=r.baseurl||r.host+":"+r.port,v=h(u,"cmd/"+encodeURIComponent(l)),c=O(v,[]);return c.error?`Error: ${c.error}`:c.result||JSON.stringify(c)||"Cannot curl, use r2ai-server or r2ai -w"}if(i.startsWith("-"))return"";let a=i+`:
|
||||
`+o;return r.maxInputTokens>0&&a.length>r.maxInputTokens&&(a=a.slice(0,r.maxInputTokens)),ke(a,n)}function Re(e){let t=r2.cmd("e scr.color");r2.cmd("e scr.color=0");try{return e()}finally{r2.cmd("e scr.color="+t)}}function Pe(){let e="",t=n=>e+=" "+b+" "+n+`
|
||||
`;e+="Usage: "+b+` (-h) ...
|
||||
`,e+="Version: "+te+`
|
||||
`,t("-a [query] - solve query with auto mode"),t("-b [url] - set base URL (alias for decai -e baseurl)"),t("-d [f1 ..] - decompile given functions"),t("-dd [..] - same as above, but ignoring cache"),t("-dD [query] - decompile current function with given extra query"),t("-dr - decompile function and its called ones (recursive)"),t("-e - display and change eval config vars"),t("-E - edit and run decai.txt"),t("-h - show this help"),t("-H - help setting up r2ai"),t("-i [f] [q] - include given file and query"),t("-k - list API key status"),t("-K - edit apikeys.txt"),t("-m [model] - use -m? or -e model=? to list the available models"),t("-n - suggest better function name"),t("-p [provider] - same as decai -e api (will be provider)"),t("-q [text] - query language model with given text"),t("-Q [text] - query on top of the last output"),t("-r [prompt] - change role prompt (same as: decai -e prompt)"),t("-R - reset role prompt to default prompt"),t("-s - function signature"),t("-v - show local variables"),t("-V - find vulnerabilities"),t("-x[*] - eXplain current function (-x* for r2 script)"),r2.log(e.trim())}function j(e,t,n,o){if(n){let s=r2.cmd("anos").trim();if(s.length>0)return s}let i="";if(o){let s=r2.cmd("s");i+=`## Context functions:
|
||||
`;let l=r2.cmdAt("axff~^C[2]~$$",s);for(let u of l.split(/\n/g))i+=r2.cmd("pdc@"+u);r2.cmd("s "+s)}let a=t?" "+e:"";try{let s=e.slice(2).trim(),l="";r.contextFile!==""&&r2.cmd2("test -f "+r.contextFile).value===0&&(l+=`## Context:
|
||||
[START]
|
||||
`+r2.cmd("cat "+r.contextFile)+`
|
||||
[END]
|
||||
`);let{body:u,count:k}=Se(()=>{let d=`## Before:
|
||||
`,g=0;for(let h of r.commands.split(",")){if(h.trim()==="")continue;let C=t||s.trim().length===0?h:h+"@@= "+s,R=r2.cmd(C);R.length>5&&(d+="Output of "+h+`:
|
||||
`);let{body:u,count:v}=Re(()=>{let d=`## Before:
|
||||
`,y=0;for(let g of r.commands.split(",")){if(g.trim()==="")continue;let C=t||s.trim().length===0?g:g+"@@= "+s,I=r2.cmd(C);I.length>5&&(d+="Output of "+g+`:
|
||||
[START]
|
||||
`+R+`
|
||||
`+I+`
|
||||
[END]
|
||||
`,g++)}return d+=`## After:
|
||||
`,{body:d,count:g}});if(k===0){console.error("Nothing to do.");return}let c="";if(r.decopipe.use){let d=r.model;try{let g=r.decopipe[r.decopipe.default],h=l+u;for(let C of g.pipeline){C.model&&(r.model=C.model);let R=C.query+". "+g.globalQuery;c=f(R,h,!0),r.debug&&console.log(`QUERY
|
||||
`,R,`
|
||||
`,y++)}return d+=`## After:
|
||||
`,{body:d,count:y}});if(v===0){console.error("Nothing to do.");return}let c="";if(r.decopipe.use){let d=r.model;try{let y=r.decopipe[r.decopipe.default],g=l+u;for(let C of y.pipeline){C.model&&(r.model=C.model);let I=C.query+". "+y.globalQuery;c=p(I,g,!0),r.debug&&console.log(`QUERY
|
||||
`,I,`
|
||||
INPUT
|
||||
`,h,`
|
||||
`,g,`
|
||||
OUTPUT
|
||||
`,c),h=c}c=h}finally{r.model=d}}else{let d=a;l+=u+i,c=f(d,l,!1),r.lastOutput=c}return n&&c.length>1&&r2.call("ano=base64:"+G(c)),c.startsWith("```")&&(c=c.replace(/```.*\n/,"").replace(/```$/,"")),c.trim()}catch(s){let l=s;console.error(l,l.stack);return}}function Xe(){let e=Se(()=>"[START]"+r.commands.split(",").map(o=>r2.cmd(o)).join(`
|
||||
`)+"[END]"),n=f("Analyze function calls, references, comments and strings, loops and ignore registers and memory accesses. Explain the purpose of this function in a single short sentence. /no_think Do not introduce or argue the response, translation of the explanation in "+r.humanLanguage,e,!0).trim().split(/\n/g);return n[n.length-1].trim()}function Ze(){let e=r.language,t=r2.cmd("afv;pdc");r.language="C";let n="'afs "+f("analyze the uses of the arguments and return value to infer the signature, identify which is the correct type for the return. Do NOT print the function body, ONLY output the function signature, like if it was going to be used in a C header",t,!1),o=n.indexOf("{");return o!==-1&&(n=n.substring(0,o)),r.language=e,n}function et(e,t){let n=[];for(;;){let o=F.auto;if(n.length>0){o+=`## Command Results
|
||||
`,c),g=c}c=g}finally{r.model=d}}else{let d=a;l+=u+i,c=p(d,l,!1),r.lastOutput=c}return n&&c.length>1&&r2.call("ano=base64:"+$(c)),c.startsWith("```")&&(c=c.replace(/```.*\n/,"").replace(/```$/,"")),c.trim()}catch(s){let l=s;console.error(l,l.stack);return}}function tt(){let e=Re(()=>"[START]"+r.commands.split(",").map(o=>r2.cmd(o)).join(`
|
||||
`)+"[END]"),n=p("Analyze function calls, references, comments and strings, loops and ignore registers and memory accesses. Explain the purpose of this function in a single short sentence. /no_think Do not introduce or argue the response, translation of the explanation in "+r.humanLanguage,e,!0).trim().split(/\n/g);return n[n.length-1].trim()}function nt(){let e=r.language,t=r2.cmd("afv;pdc");r.language="C";let n="'afs "+p("analyze the uses of the arguments and return value to infer the signature, identify which is the correct type for the return. Do NOT print the function body, ONLY output the function signature, like if it was going to be used in a C header",t,!1),o=n.indexOf("{");return o!==-1&&(n=n.substring(0,o)),r.language=e,n}function rt(e,t){let n=[];for(;;){let o=L.auto;if(n.length>0){o+=`## Command Results
|
||||
|
||||
`;for(let a of n){let s=JSON.parse(a);o+="### "+s.command+"\n\n```\n"+s.response+"\n```\n"}}o+=`
|
||||
|
||||
@@ -157,12 +160,12 @@ OUTPUT
|
||||
|
||||
`+e,r.debug&&console.log(`#### input
|
||||
`,o,`
|
||||
#### /input`),console.log("Thinking...");let i=f("",o,!0);r.debug&&console.log(`#### output
|
||||
#### /input`),console.log("Thinking...");let i=p("",o,!0);r.debug&&console.log(`#### output
|
||||
`,i,`
|
||||
#### /output`);try{let a=JSON.parse(re(oe(v(i))));if(a.action==="r2cmd"||a.action==="response"||a.action===a.command){let s=a.command||"";a.reason&&(console.log("[r2cmd] Reasoning: "+a.reason),r.tts&&(r2.syscmd("pkill say"),r2.syscmd("say -v Alex -r 250 '"+a.reason.replace(/'/g,"")+"' &"))),console.log("[r2cmd] Action: "+a.description),console.log("[r2cmd] Command: "+s);let l=s;r.yolo||(l=tt(s,t)),console.log("[r2cmd] Running: "+l);let u=r2.cmd2(l),k=u.logs?u.logs.map(g=>g.type+": "+g.message).join(`
|
||||
`):"",c=(u.res+k).trim();console.log(c);let d=ne(c);r.debug&&console.log(`<r2output>
|
||||
#### /output`);try{let a=JSON.parse(ae(ie(k(i))));if(a.action==="r2cmd"||a.action==="response"||a.action===a.command){let s=a.command||"";a.reason&&(console.log("[r2cmd] Reasoning: "+a.reason),r.tts&&(r2.syscmd("pkill say"),r2.syscmd("say -v Alex -r 250 '"+a.reason.replace(/'/g,"")+"' &"))),console.log("[r2cmd] Action: "+a.description),console.log("[r2cmd] Command: "+s);let l=s;r.yolo||(l=ot(s,t)),console.log("[r2cmd] Running: "+l);let u=r2.cmd2(l),v=u.logs?u.logs.map(y=>y.type+": "+y.message).join(`
|
||||
`):"",c=(u.res+v).trim();console.log(c);let d=oe(c);r.debug&&console.log(`<r2output>
|
||||
`,d,`
|
||||
<(r2output>`),n.push(JSON.stringify({action:"response",command:l,description:a.description,response:d}))}else if(a.action==="reply"){console.log(`Done
|
||||
`,a.response);break}else{console.log(`Unknown response
|
||||
`,JSON.stringify(i));break}}catch(a){let s=i.indexOf('response": "');if(s!==-1){let l=i.slice(s+12).replace(/\\n/g,`
|
||||
`).replace(/\\/g,"");console.log(l)}else console.log(i),console.error(a);break}}}function tt(e,t){for(;;){let n=r2.cmd("'?ie Tweak command? ('?' for help)").trim();if(n==="q!"){console.error("Break!");break}if(n==="?"){nt();continue}else if(n.startsWith(":")){console.log(r2.cmd(n.slice(1)));continue}else if(n.startsWith("-e")){t(n);continue}else{if(n==="!")return"?e do NOT execute '"+e+"' again, continue without it";if(n.startsWith("!")){console.log(r2.syscmd(n.slice(1)));continue}else{if(n==="q")return"?e All data collected!. Do not call more commands, reply the solutions";if(n){let o=n.indexOf("#");return o!==-1?n.slice(0,o).trim():n}else return e}}}return e}function nt(){console.log(" '!' do not run this command"),console.log(" '!c' run system command"),console.log(" 'q' to quit auto and try to solve"),console.log(" 'q!' quit auto without solving"),console.log(" 'c # C' use given command with comment"),console.log(" ':c' run r2 command without feeding auto"),console.log(" '-e' set decai configuration variables")}function ot(e){return e.slice(2).trim()}function j(){return r2.cmd("afi.").trim().length>0}var B="Cannot find function at current offset";function Pe(e,t){if(e===""||!e.startsWith("-")){Oe();return}let n="",o=e[1],i=ot(e);switch(o){case"H":console.log(F.decai);break;case"a":et(i,t);break;case"m":{i==="="?b("model="):i?b("model="+i):b("model");break}case"n":case"f":{if(!j()){n=B;break}n=r2.cmd("axff~$[3]");let a=r2.cmd("fd.").trim().split(/\n/).filter(s=>!s.startsWith("secti")).join(",");n=f("give me a better name for this function. the output must be: 'afn NEWNAME'. do not include the function code, only the afn line. consider: "+a,n,!1).trim(),n+=" @ "+r2.cmd("?v $FB").trim();break}case"v":{if(!j()){n=B;break}n=r2.cmd("afv;pdc"),n=f("guess a better name and type for each local variable and function argument taking using. output an r2 script using afvn and afvt commands",n,!1);break}case"i":{let a=i.split(/\s+/).filter(Boolean);if(a.length>=2){let s=a[0],l=a.slice(1).join(" "),u=r2.cmd("cat "+s);console.log(f(l,u,!0))}else console.log("Use: decai -i [file] [query ...]");break}case"p":{i?b("api="+i):X();break}case"r":{i?r.prompt=i:console.log(r.prompt);break}case"R":r.prompt=N;break;case"s":if(!j()){n=B;break}n=Ze();break;case"V":n=f("find vulnerabilities, dont show the code, only show the response, provide a sample exploit",r.lastOutput,!1);break;case"K":be();break;case"E":Ae();break;case"k":X();break;case"b":{i?b("baseurl="+i):console.log(r.baseurl);break}case"e":{i?b(i):ve();break}case"q":try{n=f(i,null,!0)}catch(a){let s=a;console.error(s,s.stack)}break;case"Q":n=f(i,r.lastOutput,!1);break;case"x":if(!j()){n=B;break}n=Xe(),(e[2]==="*"||e[2]==="r")&&(n="'CC "+n);break;case"d":e[2]==="r"?n=D(e.slice(2),!0,r.cache,!0)||"":e[2]==="d"?n=D(e,!1,!1,!1)||"":e[2]==="D"?n=D(e,!0,!1,!1)||"":n=D(e,!1,r.cache,!1)||"";break;default:Oe();break}return n||void 0}function Ce(e){Ee();let t=Pe(e,Ce);return t&&r2.log(t),!0}function rt(){r2.unload("core",y),r2.plugin("core",function(){function e(t){if(t.startsWith(y)){let n=t.slice(y.length).trim();return Ce(n)}return!1}return{name:y,license:"MIT",desc:"r2 decompiler based on r2ai",call:e}})}rt();
|
||||
`).replace(/\\/g,"");console.log(l)}else console.log(i),console.error(a);break}}}function ot(e,t){for(;;){let n=r2.cmd("'?ie Tweak command? ('?' for help)").trim();if(n==="q!"){console.error("Break!");break}if(n==="?"){it();continue}else if(n.startsWith(":")){console.log(r2.cmd(n.slice(1)));continue}else if(n.startsWith("-e")){t(n);continue}else{if(n==="!")return"?e do NOT execute '"+e+"' again, continue without it";if(n.startsWith("!")){console.log(r2.syscmd(n.slice(1)));continue}else{if(n==="q")return"?e All data collected!. Do not call more commands, reply the solutions";if(n){let o=n.indexOf("#");return o!==-1?n.slice(0,o).trim():n}else return e}}}return e}function it(){console.log(" '!' do not run this command"),console.log(" '!c' run system command"),console.log(" 'q' to quit auto and try to solve"),console.log(" 'q!' quit auto without solving"),console.log(" 'c # C' use given command with comment"),console.log(" ':c' run r2 command without feeding auto"),console.log(" '-e' set decai configuration variables")}function at(e){return e.slice(2).trim()}function B(){return r2.cmd("afi.").trim().length>0}var F="Cannot find function at current offset";function Ce(e,t){if(e===""||!e.startsWith("-")){Pe();return}let n="",o=e[1],i=at(e);switch(o){case"H":console.log(L.decai);break;case"a":rt(i,t);break;case"m":{i==="="?x("model="):i?x("model="+i):x("model");break}case"n":case"f":{if(!B()){n=F;break}n=r2.cmd("axff~$[3]");let a=r2.cmd("fd.").trim().split(/\n/).filter(s=>!s.startsWith("secti")).join(",");n=p("give me a better name for this function. the output must be: 'afn NEWNAME'. do not include the function code, only the afn line. consider: "+a,n,!1).trim(),n+=" @ "+r2.cmd("?v $FB").trim();break}case"v":{if(!B()){n=F;break}n=r2.cmd("afv;pdc"),n=p("guess a better name and type for each local variable and function argument taking using. output an r2 script using afvn and afvt commands",n,!1);break}case"i":{let a=i.split(/\s+/).filter(Boolean);if(a.length>=2){let s=a[0],l=a.slice(1).join(" "),u=r2.cmd("cat "+s);console.log(p(l,u,!0))}else console.log("Use: decai -i [file] [query ...]");break}case"p":{i?x("api="+i):Z();break}case"r":{i?r.prompt=i:console.log(r.prompt);break}case"R":r.prompt=T;break;case"s":if(!B()){n=F;break}n=nt();break;case"V":n=p("find vulnerabilities, dont show the code, only show the response, provide a sample exploit",r.lastOutput,!1);break;case"K":xe();break;case"E":Se();break;case"k":Z();break;case"b":{i?x("baseurl="+i):console.log(r.baseurl);break}case"e":{i?x(i):we();break}case"q":try{n=p(i,null,!0)}catch(a){let s=a;console.error(s,s.stack)}break;case"Q":n=p(i,r.lastOutput,!1);break;case"x":if(!B()){n=F;break}n=tt(),(e[2]==="*"||e[2]==="r")&&(n="'CC "+n);break;case"d":e[2]==="r"?n=j(e.slice(2),!0,r.cache,!0)||"":e[2]==="d"?n=j(e,!1,!1,!1)||"":e[2]==="D"?n=j(e,!0,!1,!1)||"":n=j(e,!1,r.cache,!1)||"";break;default:Pe();break}return n||void 0}function Ie(e){Oe();let t=Ce(e,Ie);return t&&r2.log(t),!0}function st(){r2.unload("core",b),r2.plugin("core",function(){function e(t){if(t.startsWith(b)){let n=t.slice(b.length).trim();return Ie(n)}return!1}return{name:b,license:"MIT",desc:"r2 decompiler based on r2ai",call:e}})}st();
|
||||
@@ -0,0 +1,36 @@
|
||||
# decai r2plugin
|
||||
|
||||
Boilerplate to embed `decai.r2.js` inside the radare2 core library using the
|
||||
external plugin system (XPS). Unlike `r2ai/r2plugin`, this does not link any
|
||||
native code: the r2.js script is converted into a C buffer and registered as
|
||||
an embedded startup script, so r2 runs it at startup exactly like a file in
|
||||
the `plugins/*.r2.js` directory (honoring `cfg.plugins`).
|
||||
|
||||
The `decai.r2.js` bundle is built from the typescript sources with
|
||||
`r2frida-compile` (see `make -C src` in the decai directory). The C buffer is
|
||||
generated at build time with `rax2 -C` (make builds) or `js2c.py` (meson).
|
||||
|
||||
## Usage
|
||||
|
||||
decai lives inside the r2ai repository, so expose it as `libr/xps/p/decai`:
|
||||
|
||||
```console
|
||||
cd radare2/libr/xps/p
|
||||
git clone https://github.com/radareorg/r2ai
|
||||
ln -s r2ai/decai decai
|
||||
```
|
||||
|
||||
For acr/make builds:
|
||||
|
||||
```console
|
||||
cd radare2
|
||||
make -C libr/xps EXTERNAL_PLUGINS=decai
|
||||
./configure-plugins
|
||||
make
|
||||
```
|
||||
|
||||
For meson builds, make `libr/xps/p/meson.build` contain:
|
||||
|
||||
```meson
|
||||
subdir('decai/r2plugin')
|
||||
```
|
||||
@@ -0,0 +1,2 @@
|
||||
/Users/pancake/prg/radare2/libr/xps/p/decai/r2plugin/core/decai.r2.js.o: \
|
||||
/Users/pancake/prg/radare2/libr/xps/p/decai/r2plugin/core/decai.r2.js.c
|
||||
@@ -0,0 +1,3 @@
|
||||
#ifdef R2_CORE_H
|
||||
extern RCorePlugin r_core_plugin_decai;
|
||||
#endif
|
||||
@@ -0,0 +1,10 @@
|
||||
DECAI_CORE_WD=$(LIBR)/xps/p/decai
|
||||
DECAI_CORE_OBJ= \
|
||||
$(DECAI_CORE_WD)/r2plugin/core/plugin.o \
|
||||
$(DECAI_CORE_WD)/r2plugin/core/decai.r2.js.o
|
||||
EXTERNAL_STATIC_OBJS+=$(DECAI_CORE_OBJ)
|
||||
|
||||
# decai.r2.js can be rebuilt from typescript with r2frida-compile (make -C src)
|
||||
$(DECAI_CORE_WD)/r2plugin/core/decai.r2.js.c: $(DECAI_CORE_WD)/decai.r2.js
|
||||
rax2 -C < $(DECAI_CORE_WD)/decai.r2.js | \
|
||||
sed -e 's,^unsigned,const unsigned,' -e 's,buf,decai_qjs,g' > $@
|
||||
@@ -0,0 +1,35 @@
|
||||
/* decai - LGPL - Copyright 2026 - pancake */
|
||||
|
||||
#include <r_core.h>
|
||||
|
||||
// generated from ../decai.r2.js, see ../deps.mk
|
||||
extern const unsigned char decai_qjs[];
|
||||
extern const unsigned int decai_qjs_len;
|
||||
|
||||
static bool decai_init(RCorePluginSession *cps) {
|
||||
r_core_script_embed (cps->core, "decai", "qjs", (const char *)decai_qjs, (int)decai_qjs_len);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool decai_call(RCorePluginSession *cps, const char *input) {
|
||||
return false;
|
||||
}
|
||||
|
||||
RCorePlugin r_core_plugin_decai = {
|
||||
.meta = {
|
||||
.name = "decai",
|
||||
.desc = "AI decompiler (embedded decai.r2.js)",
|
||||
.author = "pancake",
|
||||
.license = "MIT",
|
||||
},
|
||||
.init = decai_init,
|
||||
.call = decai_call,
|
||||
};
|
||||
|
||||
#ifndef R2_PLUGIN_INCORE
|
||||
R_API RLibStruct radare_plugin = {
|
||||
.type = R_LIB_TYPE_CORE,
|
||||
.data = &r_core_plugin_decai,
|
||||
.version = R2_VERSION,
|
||||
};
|
||||
#endif
|
||||
@@ -0,0 +1,372 @@
|
||||
/Users/pancake/prg/radare2/libr/xps/p/decai/r2plugin/core/plugin.o: \
|
||||
/Users/pancake/prg/radare2/libr/xps/p/decai/r2plugin/core/plugin.c \
|
||||
/Users/pancake/prg/radare2/libr/include/r_core.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_main.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_types.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_types_base.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/ctype.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_ctype.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/cdefs.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_symbol_aliasing.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_posix_availability.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/ptrcheck.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_bounds.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/runetype.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_types.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/machine/_types.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/arm/_types.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_pthread/_pthread_types.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_size_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_ct_rune_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_rune_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_wchar_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_wint_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/types.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/appleapiopts.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/machine/types.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/arm/types.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_int8_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_int16_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_int32_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_int64_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_u_int8_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_u_int16_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_u_int32_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_u_int64_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_intptr_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_uintptr_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/machine/endian.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/arm/endian.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_endian.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/machine/_endian.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/arm/_endian.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/__endian.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/libkern/_OSByteOrder.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/libkern/arm/_OSByteOrder.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_u_char.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_u_short.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_u_int.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_caddr_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_dev_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_blkcnt_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_blksize_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_gid_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_in_addr_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_in_port_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_ino_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_ino64_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_key_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_mode_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_nlink_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_id_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_pid_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_off_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_uid_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_clock_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_ssize_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_time_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_useconds_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_suseconds_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_rsize_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_errno_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_fd_def.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/Availability.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/AvailabilityVersions.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/AvailabilityInternal.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/AvailabilityInternalLegacy.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_fd_setsize.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_fd_set.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_fd_clr.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_fd_zero.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_fd_isset.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_fd_copy.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_pthread/_pthread_attr_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_pthread/_pthread_cond_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_pthread/_pthread_condattr_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_pthread/_pthread_mutex_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_pthread/_pthread_mutexattr_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_pthread/_pthread_once_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_pthread/_pthread_rwlock_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_pthread/_pthread_rwlockattr_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_pthread/_pthread_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_pthread/_pthread_key_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_fsblkcnt_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_fsfilcnt_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/limits.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/limits.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/machine/limits.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/arm/limits.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/arm/_limits.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/syslimits.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/stdint.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/stdint.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_types/_uint8_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_types/_uint16_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_types/_uint32_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_types/_uint64_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_types/_intmax_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_types/_uintmax_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/stdbool.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/math.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_types_overflow.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_str_util.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_userconf.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_version.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/stddef.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/stddef.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/__stddef_header_macro.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/__stddef_ptrdiff_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/__stddef_size_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/__stddef_rsize_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/__stddef_wchar_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/__stddef_null.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/__stddef_max_align_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/__stddef_offsetof.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/stdlib.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_stdlib.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/wait.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/signal.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/machine/signal.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/arm/signal.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/machine/_mcontext.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/arm/_mcontext.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/mach/machine/_structs.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/mach/arm/_structs.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_sigaltstack.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_ucontext.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_sigset_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/resource.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_timeval.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/alloca.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_null.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/malloc/_malloc.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/malloc/_malloc_type.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/malloc/_ptrcheck.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_abort.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/assert.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_assert.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_static_assert.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/inttypes.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/inttypes.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_inttypes.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_types_null.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/stdio.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_stdio.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_va_list.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/stdio.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_printf.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_seek_set.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_ctermid.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/secure/_stdio.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/secure/_common.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/string.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_string.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_strings.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/secure/_strings.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/secure/_string.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/stdarg.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/__stdarg_header_macro.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/__stdarg___gnuc_va_list.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/__stdarg_va_list.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/__stdarg_va_arg.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/__stdarg___va_copy.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/__stdarg_va_copy.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/fcntl.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/fcntl.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_o_sync.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_o_dsync.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_s_ifmt.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_timespec.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_filesec_t.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_endian.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/stat.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/dirent.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/dirent.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/unistd.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/unistd.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_posix_vdisable.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/select.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_select.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_uuid_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/gethostuuid.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/time.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_timeval64.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/time.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_time.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_getopt.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_regex.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_list.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/sdb.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/asserts.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/types.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/config.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/ht.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/ht_pp.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/ht_inc.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/ls.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/heap.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/dict.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/cdb.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/cdb_make.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/buffer.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/version.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/rangstr.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_skiplist.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_th.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_w32.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_sys.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/errno.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/errno.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/semaphore.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/semaphore.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/pthread.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/pthread/sched.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/pthread/pthread_impl.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/pthread/qos.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/qos.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_mach_port_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/signal.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_event.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/ht_up.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_vec.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_assert.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_log.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_arena.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_itv.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_signal.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_alloc.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_rbtree.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_trie.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_strs.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_xml.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_new_rbtree.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_intervaltree.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/../r_types.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_big.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_base32.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_base36.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_base64.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_base91.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_buf.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_mem.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_ref.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_bits.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_bitmap.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_bitset.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_time.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_debruijn.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_type.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_file.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_hex.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_name.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_num.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_table.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_graph.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_panels.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_pool.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_diff.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_cons.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/pj.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_strbuf.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_json.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_sandbox.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_stack.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_str.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/wchar.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_wchar.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_mbstate_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/__wctype.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/___wctype.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_types/_wctype_t.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_str_constpool.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/termios.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/termios.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/ttycom.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/ioccom.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/ttydefaults.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/ioctl.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/filio.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/sockio.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/net/if.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/net/if_var.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_timeval32.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/net/net_kev.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/socket.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/constrained_ctypes.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/machine/_param.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/arm/_param.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_sa_family_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_socklen_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_iovec_t.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_punycode.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_queue.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_range.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_spaces.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_strs_store.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_bloom.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_strpool.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_tree.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_uleb128.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_utf8.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_utf16.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_utf32.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_idpool.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_asn1.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_graph_drawable.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_config.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/bplist.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_x509.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_pkcs7.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_protobuf.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_token.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_axml.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_cfloat.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/libc.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_socket.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_bind.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/netinet/in.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/netinet6/in6.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/un.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/poll.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/poll.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/arpa/inet.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/netdb.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/netinet/tcp.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_magic.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_agraph.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_io.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_lib.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/dlfcn.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_w32dw.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/ptrace.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_fs.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_egg.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_asm.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_arch.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_bin.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_muta.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_hash.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_muta/r_sm4.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_bin_dwarf.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_reg.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_anal/op.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_anal/hint.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_esil.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/ht_uu.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_anal.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_print.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_search.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_syscall.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_flag.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_codemeta.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/set.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_lang.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_cmd.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_sign.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_debug.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_bp.h \
|
||||
/Users/pancake/prg/radare2/libr/include/rvc.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_core_priv.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_core_task.h
|
||||
@@ -0,0 +1 @@
|
||||
include p/decai/r2plugin/core/deps.mk
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Convert a script file into a C buffer named decai_qjs (same output as `rax2 -C` + sed)."""
|
||||
|
||||
import sys
|
||||
|
||||
data = open(sys.argv[1], "rb").read()
|
||||
with open(sys.argv[2], "w") as f:
|
||||
f.write("const unsigned char decai_qjs[] = {\n")
|
||||
for i in range(0, len(data), 16):
|
||||
f.write(" %s,\n" % ", ".join("0x%02x" % b for b in data[i:i + 16]))
|
||||
f.write("};\nconst unsigned int decai_qjs_len = %d;\n" % len(data))
|
||||
@@ -0,0 +1,16 @@
|
||||
# decai XPS integration for radare2.
|
||||
# Included from libr/xps/meson.build when decai lives under libr/xps/p/decai/.
|
||||
# Unlike native plugins, decai ships no native code: decai.r2.js is embedded
|
||||
# as a buffer in libr_core and the tiny core plugin registers it with
|
||||
# r_core_script_embed() so r2 runs it at startup like any plugins/*.r2.js file.
|
||||
|
||||
core_plugins += ['decai']
|
||||
|
||||
decai_py3 = import('python').find_installation()
|
||||
|
||||
r_core_additional_sources += files('core/plugin.c')
|
||||
r_core_additional_sources += custom_target('decai_r2_js_c',
|
||||
input: '../decai.r2.js',
|
||||
output: 'decai.r2.js.c',
|
||||
command: [decai_py3, files('js2c.py'), '@INPUT@', '@OUTPUT@'],
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
core.decai
|
||||
+2
-1
@@ -1,10 +1,11 @@
|
||||
PLUGIN_NAME=decai
|
||||
R2F_C_FLAGS=-c -S
|
||||
R2FRIDA_COMPILE?=r2pm -r r2frida-compile
|
||||
|
||||
all: build
|
||||
|
||||
build:
|
||||
r2pm -r r2frida-compile $(R2F_C_FLAGS) -o ../$(PLUGIN_NAME).r2.js main.ts
|
||||
$(R2FRIDA_COMPILE) $(R2F_C_FLAGS) -o ../$(PLUGIN_NAME).r2.js main.ts
|
||||
|
||||
test: build
|
||||
r2 -i ../$(PLUGIN_NAME).r2.js -q -c 'af;decai -d' /bin/ls
|
||||
|
||||
@@ -32,6 +32,17 @@ function stringHandler(key: StateKey): ConfigHandler {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeApiType(value: string): "chat" | "generate" | null {
|
||||
const apiType = value.trim();
|
||||
if (apiType === "chat") {
|
||||
return "chat";
|
||||
}
|
||||
if (apiType === "generate") {
|
||||
return "generate";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export const configHandlers: ConfigHandlers = {
|
||||
pipeline: {
|
||||
get: () => state.pipeline,
|
||||
@@ -60,7 +71,29 @@ export const configHandlers: ConfigHandlers = {
|
||||
get: () => state.think || "false",
|
||||
set: (v: string) => { state.think = v; },
|
||||
},
|
||||
apitype: {
|
||||
get: () => state.apitype,
|
||||
set: (v: string) => {
|
||||
const apiType = normalizeApiType(v);
|
||||
if (apiType) {
|
||||
state.apitype = apiType;
|
||||
} else {
|
||||
console.error("Invalid apitype. Use chat or generate.");
|
||||
}
|
||||
},
|
||||
},
|
||||
debug: boolHandler("debug"),
|
||||
http: {
|
||||
get: () => state.http,
|
||||
set: (v: string) => {
|
||||
const backend = v.trim();
|
||||
if (backend === "auto" || backend === "r2" || backend === "curl") {
|
||||
state.http = backend;
|
||||
} else {
|
||||
console.error("Invalid http backend. Use auto, r2 or curl.");
|
||||
}
|
||||
},
|
||||
},
|
||||
timeout: {
|
||||
get: () => state.timeout,
|
||||
set: (v: string) => { state.timeout = Math.max(0, parseInt(v, 10) || 0); },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const VERSION = "1.4.0";
|
||||
export const VERSION = "1.5.4";
|
||||
export const COMMAND = "decai";
|
||||
export const DECAI_CONFIG_DIR = "~/.config/r2ai";
|
||||
export const API_KEYS_PATH = DECAI_CONFIG_DIR + "/apikeys.txt";
|
||||
@@ -30,8 +30,10 @@ These are the most recommended models for decompiling in local:
|
||||
|
||||
## Common Options
|
||||
* 'decai -e baseurl=<url>' override default host and port for API endpoint (e.g., 'http://localhost:11434')
|
||||
* 'decai -e apitype=chat|generate' choose the Ollama API endpoint (default: chat)
|
||||
* 'decai -e headers=Authorization: Bearer ...\\nUser-Agent: curl/8.7.1' add or override HTTP headers
|
||||
* "export DECAI_HEADERS='Authorization: Bearer ...\\nUser-Agent: curl/8.7.1'" set extra headers from the environment
|
||||
* 'decai -e http=auto|r2|curl' select the HTTP transport (default: auto, prefers r2.http)
|
||||
* 'decai -e timeout=0' disable curl timeouts, or set a positive number of seconds
|
||||
|
||||
* 'decai -e deterministic=true' to remove randomness from decompilation responses
|
||||
|
||||
+14
-6
@@ -14,16 +14,24 @@ function shellEscape(arg: string): string {
|
||||
return `'${arg.replace(/'/g, `'"'"'`)}'`;
|
||||
}
|
||||
|
||||
function executeCurl(options: HttpRequestOptions): string {
|
||||
function executeRequest(options: HttpRequestOptions): string {
|
||||
const { method, url, headers, payload } = options;
|
||||
const cmdParts = ["curl", "-s"];
|
||||
if (state.timeout > 0) {
|
||||
cmdParts.push("--max-time", String(state.timeout));
|
||||
}
|
||||
const requestHeaders = mergeHeaders(
|
||||
["Content-Type: application/json"],
|
||||
headers,
|
||||
);
|
||||
if (state.http !== "curl") {
|
||||
if (r2.http) {
|
||||
return r2.http(method, url, requestHeaders.join("\n"), payload ?? "").body;
|
||||
}
|
||||
if (state.http === "r2") {
|
||||
throw new Error("r2.http is not available in this runtime");
|
||||
}
|
||||
}
|
||||
const cmdParts = ["curl", "-s"];
|
||||
if (state.timeout > 0) {
|
||||
cmdParts.push("--max-time", String(state.timeout));
|
||||
}
|
||||
requestHeaders.forEach((h) => cmdParts.push("-H", shellEscape(h)));
|
||||
|
||||
if (method === "POST") {
|
||||
@@ -70,7 +78,7 @@ function parseNdjson(output: string): HttpResponse | null {
|
||||
|
||||
export function httpRequest(options: HttpRequestOptions): HttpResponse {
|
||||
try {
|
||||
const output = executeCurl(options).trim();
|
||||
const output = executeRequest(options).trim();
|
||||
if (output === "") {
|
||||
return { error: "empty response" };
|
||||
}
|
||||
|
||||
+22
-11
@@ -12,7 +12,7 @@ import {
|
||||
ProviderRegistry,
|
||||
} from "./types";
|
||||
import { state } from "./state";
|
||||
import { filterResponse, padRight } from "./utils";
|
||||
import { filterResponse, joinUrl, padRight } from "./utils";
|
||||
import { getApiKey } from "./apiKeys";
|
||||
import { getConfiguredHeaders, mergeHeaders } from "./headers";
|
||||
import { httpGet, httpPost } from "./http";
|
||||
@@ -138,12 +138,13 @@ function getProviderRequest(provider: ProviderConfig): {
|
||||
|
||||
function listV1Models(provider: ProviderConfig): string {
|
||||
const { baseUrl, headers } = getProviderRequest(provider);
|
||||
return listDataModels(baseUrl + "/v1/models", headers, (model) => model.id);
|
||||
const url = joinUrl(baseUrl, "v1/models");
|
||||
return listDataModels(url, headers, (model) => model.id);
|
||||
}
|
||||
|
||||
function listOllamaModels(provider: ProviderConfig): string {
|
||||
const { baseUrl, headers } = getProviderRequest(provider);
|
||||
const response = httpGet(baseUrl + "/api/tags", headers);
|
||||
const response = httpGet(joinUrl(baseUrl, "api/tags"), headers);
|
||||
const error = getErrorMessage(response.error);
|
||||
if (error) {
|
||||
console.error(error);
|
||||
@@ -156,7 +157,7 @@ function listOllamaModels(provider: ProviderConfig): string {
|
||||
|
||||
function listMistralModels(provider: ProviderConfig): string {
|
||||
const { baseUrl, headers } = getProviderRequest(provider);
|
||||
const response = httpGet(baseUrl + "/v1/models", headers);
|
||||
const response = httpGet(joinUrl(baseUrl, "v1/models"), headers);
|
||||
if (response.data) {
|
||||
return uniqueBy(response.data, (model) => model.name || model.id)
|
||||
.map((model) =>
|
||||
@@ -187,7 +188,7 @@ const providerRuntimes: Record<ApiStyle, ProviderRuntime> = {
|
||||
}
|
||||
throw new Error("Invalid response format");
|
||||
},
|
||||
buildUrl: (baseUrl) => baseUrl + "/v1/chat/completions",
|
||||
buildUrl: (baseUrl) => joinUrl(baseUrl, "v1/chat/completions"),
|
||||
},
|
||||
anthropic: {
|
||||
buildPayload: (model, query) => {
|
||||
@@ -222,15 +223,19 @@ const providerRuntimes: Record<ApiStyle, ProviderRuntime> = {
|
||||
}
|
||||
throw new Error("Invalid response format");
|
||||
},
|
||||
buildUrl: (baseUrl) => baseUrl + "/v1/messages",
|
||||
buildUrl: (baseUrl) => joinUrl(baseUrl, "v1/messages"),
|
||||
},
|
||||
ollama: {
|
||||
buildPayload: (model, query) => {
|
||||
const payload: OllamaPayload = {
|
||||
stream: false,
|
||||
model,
|
||||
messages: [{ role: "user", content: query }],
|
||||
};
|
||||
if (state.apitype === "generate") {
|
||||
payload.prompt = query;
|
||||
} else {
|
||||
payload.messages = [{ role: "user", content: query }];
|
||||
}
|
||||
if (isThinkEnabled()) {
|
||||
if (state.think === "true" || state.think === "1") {
|
||||
payload.think = true;
|
||||
@@ -259,12 +264,15 @@ const providerRuntimes: Record<ApiStyle, ProviderRuntime> = {
|
||||
if (response.error) {
|
||||
throw new Error(getErrorMessage(response.error));
|
||||
}
|
||||
if (typeof response.response === "string") {
|
||||
return filterResponse(response.response);
|
||||
}
|
||||
if (response.message?.content) {
|
||||
return filterResponse(response.message.content);
|
||||
}
|
||||
throw new Error(JSON.stringify(response));
|
||||
},
|
||||
buildUrl: (baseUrl) => baseUrl + "/api/chat",
|
||||
buildUrl: (baseUrl) => joinUrl(baseUrl, "api/" + state.apitype),
|
||||
},
|
||||
gemini: {
|
||||
buildPayload: (_model, query) => {
|
||||
@@ -304,14 +312,17 @@ const providerRuntimes: Record<ApiStyle, ProviderRuntime> = {
|
||||
throw new Error("Invalid response format");
|
||||
},
|
||||
buildUrl: (baseUrl, model, apiKey) =>
|
||||
`${baseUrl}/v1beta/models/${model}:generateContent?key=${apiKey}`,
|
||||
joinUrl(
|
||||
baseUrl,
|
||||
`v1beta/models/${model}:generateContent?key=${apiKey}`,
|
||||
),
|
||||
requiresUrlApiKey: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const providerRegistry: ProviderRegistry = {
|
||||
anthropic: {
|
||||
defaultModel: "claude-3-7-sonnet-20250219",
|
||||
defaultModel: "claude-sonnet-5-5",
|
||||
defaultBaseUrl: "https://api.anthropic.com",
|
||||
authKey: "ANTHROPIC_API_KEY",
|
||||
keyName: "anthropic",
|
||||
@@ -319,7 +330,7 @@ export const providerRegistry: ProviderRegistry = {
|
||||
apiStyle: "anthropic",
|
||||
},
|
||||
claude: {
|
||||
defaultModel: "claude-3-7-sonnet-20250219",
|
||||
defaultModel: "claude-sonnet-5-5",
|
||||
defaultBaseUrl: "https://api.anthropic.com",
|
||||
authKey: "ANTHROPIC_API_KEY",
|
||||
keyName: "claude",
|
||||
|
||||
+3
-6
@@ -1,5 +1,5 @@
|
||||
import { state } from "./state";
|
||||
import { fileDump, tmpdir } from "./utils";
|
||||
import { fileDump, joinUrl, tmpdir } from "./utils";
|
||||
import { callProvider } from "./providers";
|
||||
import { httpGet } from "./http";
|
||||
|
||||
@@ -20,11 +20,8 @@ export function r2ai(
|
||||
? cleanQuery
|
||||
: ["-i", fileName, cleanQuery].join(" ");
|
||||
|
||||
const host = state.baseurl
|
||||
? state.baseurl + "/cmd"
|
||||
: state.host + ":" + state.port + "/cmd";
|
||||
|
||||
const url = host + "/" + encodeURIComponent(q);
|
||||
const baseUrl = state.baseurl || state.host + ":" + state.port;
|
||||
const url = joinUrl(baseUrl, "cmd/" + encodeURIComponent(q));
|
||||
const response = httpGet(url, []);
|
||||
if (response.error) {
|
||||
return `Error: ${response.error}`;
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
export type R2Address = number | string | unknown;
|
||||
|
||||
export interface R2HttpResponse {
|
||||
status: number;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export type R2HttpRequest = (
|
||||
method: "GET" | "POST",
|
||||
url: string,
|
||||
headers: string,
|
||||
data?: string,
|
||||
) => R2HttpResponse;
|
||||
|
||||
export interface R2LogEntry {
|
||||
type: string;
|
||||
message: string;
|
||||
@@ -23,8 +35,10 @@ export type R2PluginFactory = () => R2Plugin;
|
||||
|
||||
export class R2PipeAdapter {
|
||||
r2p: R2Pipe;
|
||||
http?: R2HttpRequest;
|
||||
constructor(r2p: R2Pipe) {
|
||||
this.r2p = r2p;
|
||||
this.http = r2p.http?.bind(r2p);
|
||||
}
|
||||
/**
|
||||
* Run a command in the associated instance of radare2 and return the output as a string
|
||||
@@ -140,6 +154,7 @@ export interface R2Pipe {
|
||||
syscmd(cmd: string): string;
|
||||
syscmds(cmd: string): string;
|
||||
fdump(data: string): string;
|
||||
http?: R2HttpRequest;
|
||||
|
||||
/**
|
||||
* Same as cmdj but using .call which avoids command injection problems
|
||||
@@ -231,6 +246,7 @@ export interface R2PipeSync {
|
||||
syscmd(cmd: string): string;
|
||||
syscmds(cmd: string): string;
|
||||
fdump(data: string): string;
|
||||
http?: R2HttpRequest;
|
||||
|
||||
/**
|
||||
* Same as cmdj but using .call which avoids command injection problems
|
||||
|
||||
@@ -17,7 +17,9 @@ export const defaultState: AppState = {
|
||||
deterministic: true,
|
||||
debug: false,
|
||||
timeout: 180,
|
||||
http: "auto",
|
||||
think: "",
|
||||
apitype: "chat",
|
||||
useFiles: false,
|
||||
contextFile: "",
|
||||
model: "",
|
||||
|
||||
+5
-1
@@ -13,6 +13,7 @@ declare global {
|
||||
|
||||
export type ApiStyle = "openai" | "anthropic" | "ollama" | "gemini";
|
||||
export type AuthStyle = "none" | "bearer" | "anthropic";
|
||||
export type OllamaApiType = "chat" | "generate";
|
||||
|
||||
export interface ProviderConfig {
|
||||
defaultModel: string;
|
||||
@@ -50,7 +51,9 @@ export interface AppState {
|
||||
deterministic: boolean;
|
||||
debug: boolean;
|
||||
timeout: number;
|
||||
http: "auto" | "r2" | "curl";
|
||||
think: string;
|
||||
apitype: OllamaApiType;
|
||||
useFiles: boolean;
|
||||
contextFile: string;
|
||||
model: string;
|
||||
@@ -139,7 +142,8 @@ export interface AnthropicPayload {
|
||||
export interface OllamaPayload {
|
||||
stream: boolean;
|
||||
model: string;
|
||||
messages: Array<{ role: string; content: string }>;
|
||||
messages?: Array<{ role: string; content: string }>;
|
||||
prompt?: string;
|
||||
think?: boolean | string;
|
||||
options?: {
|
||||
repeat_last_n: number;
|
||||
|
||||
+15
-2
@@ -5,9 +5,18 @@ export function tmpdir(path: string): string {
|
||||
return dir + "/" + path;
|
||||
}
|
||||
|
||||
// r2's mkdir and touch commands do not expand '~'
|
||||
export function expandHome(path: string): string {
|
||||
if (!path.startsWith("~/")) {
|
||||
return path;
|
||||
}
|
||||
const home = r2.cmd("'%HOME").trim() || r2.cmd("'%USERPROFILE").trim();
|
||||
return home ? home + path.slice(1) : path;
|
||||
}
|
||||
|
||||
export function ensurePath(dir: string, file: string): void {
|
||||
r2.cmd("'mkdir -p " + dir);
|
||||
r2.cmd("'touch " + file);
|
||||
r2.cmd("'mkdir -p " + expandHome(dir));
|
||||
r2.cmd("'touch " + expandHome(file));
|
||||
}
|
||||
|
||||
export function fileExists(path: string): boolean {
|
||||
@@ -22,6 +31,10 @@ export function padRight(str: string, length: number): string {
|
||||
return str + " ".repeat(Math.max(0, length - str.length));
|
||||
}
|
||||
|
||||
export function joinUrl(baseUrl: string, path: string): string {
|
||||
return baseUrl.replace(/\/+$/, "") + "/" + path.replace(/^\/+/, "");
|
||||
}
|
||||
|
||||
export function trimAnsi(str: string): string {
|
||||
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
||||
}
|
||||
|
||||
@@ -16,3 +16,25 @@ For meson builds, place this repository at `libr/xps/p/r2ai` and make
|
||||
```meson
|
||||
subdir('r2ai/r2plugin')
|
||||
```
|
||||
|
||||
## WASI HTTP build
|
||||
|
||||
Use a radare2 checkout with the host HTTP bridge and place this repository at
|
||||
`libr/xps/p/r2ai`. From that radare2 checkout:
|
||||
|
||||
```sh
|
||||
echo 'EXTERNAL_PLUGINS+=r2ai' >> libr/xps/config.mk
|
||||
make -C libr/xps
|
||||
WASI_HTTP=1 sys/wasi-api.sh
|
||||
```
|
||||
|
||||
This links the C plugin into the reactor; it does not load a native shared
|
||||
library at runtime. Enable HTTP in the host adapter. The `auto` backend selects
|
||||
`r_socket_http_get/post`, preserving provider authentication headers and JSON
|
||||
bodies, subject to the radare2 sandbox. `--disable-network` also denies these calls.
|
||||
|
||||
WASI excludes POSIX signal/alarm handling; the host controls request timeouts,
|
||||
not `r2ai.http.timeout`. Normal queries and model listing use buffered HTTP.
|
||||
Vertex's automatic `gcloud` authentication and background tasks requiring
|
||||
threads are unavailable. Explicit shell backends (`system`/`curl`) cannot work;
|
||||
use `auto` or `socket`. Browser CORS and restricted-header rules still apply.
|
||||
+36
-2
@@ -8,6 +8,7 @@ R2BINDIR=$(shell r2pm -H R2PM_BINDIR)
|
||||
R2INCDIR=$(shell r2 -NNH R2_INCDIR)/libr
|
||||
R2LIBDIR=$(shell r2 -NNH R2_LIBDIR)
|
||||
R2MANDIR=$(shell r2pm -H R2PM_MANDIR)
|
||||
R2AI_PROMPTSDIR=$(HOME)/.config/r2ai/prompts
|
||||
ifeq ($(R2MANDIR),)
|
||||
R2MANDIR=$(shell r2 -NNH R2_PREFIX)/share/man
|
||||
endif
|
||||
@@ -70,6 +71,37 @@ all: $(OBJS) r2check
|
||||
$(AR) rcs libr2ai.a $(OBJS)
|
||||
$(CC) -pie -o r2ai$(DOTEXE) $(OBJS) $(CFLAGS) $(LDFLAGS) -lm main.c
|
||||
|
||||
# Binary package for r2pm -bi: plugins/, bin/, man/ and prompts/ for this r2 version and platform
|
||||
# r2pm -H R2PM_* needs a recent r2pm; fall back to uname when missing
|
||||
R2PM_OS_SH=$(shell r2pm -H R2PM_OS 2>/dev/null)
|
||||
R2PM_ARCH_SH=$(shell r2pm -H R2PM_ARCH 2>/dev/null)
|
||||
R2PM_BITS_SH=$(shell r2pm -H R2PM_BITS 2>/dev/null)
|
||||
ifeq ($(R2PM_OS_SH),)
|
||||
ifeq ($(shell uname -s),Darwin)
|
||||
R2PM_OS_SH=darwin
|
||||
else
|
||||
R2PM_OS_SH=linux
|
||||
endif
|
||||
endif
|
||||
ifeq ($(R2PM_ARCH_SH),)
|
||||
ifeq ($(shell uname -m),x86_64)
|
||||
R2PM_ARCH_SH=x86
|
||||
else ifneq (,$(filter $(shell uname -m),aarch64 arm64))
|
||||
R2PM_ARCH_SH=arm
|
||||
else
|
||||
R2PM_ARCH_SH=$(shell uname -m)
|
||||
endif
|
||||
endif
|
||||
ifeq ($(R2PM_BITS_SH),)
|
||||
R2PM_BITS_SH=$(shell getconf LONG_BIT 2>/dev/null || echo 64)
|
||||
endif
|
||||
BINDIST_ZIP=r2ai-$(shell r2 -qv)-$(R2PM_OS_SH)-$(R2PM_ARCH_SH)-$(R2PM_BITS_SH).zip
|
||||
|
||||
bindist: all
|
||||
rm -rf build/bindist ../$(BINDIST_ZIP)
|
||||
$(MAKE) user-install R2USRPLUGDIR=$(CURDIR)/build/bindist/plugins R2BINDIR=$(CURDIR)/build/bindist/bin R2MANDIR=$(CURDIR)/build/bindist/man R2AI_PROMPTSDIR=$(CURDIR)/build/bindist/prompts
|
||||
cd build/bindist && zip -q -r $(CURDIR)/../$(BINDIST_ZIP) plugins bin man prompts
|
||||
|
||||
try: all
|
||||
$(MAKE) user-install
|
||||
$(MAKE) doc-install
|
||||
@@ -87,12 +119,12 @@ user-install: user-uninstall
|
||||
mkdir -p $(R2USRPLUGDIR)
|
||||
mkdir -p $(R2BINDIR)
|
||||
mkdir -p $(R2MANDIR)/man1
|
||||
mkdir -p $(HOME)/.config/r2ai/prompts
|
||||
mkdir -p $(R2AI_PROMPTSDIR)
|
||||
$(MAKE) user-uninstall
|
||||
cp -f r2ai$(DOTLIB) $(R2USRPLUGDIR)/r2ai$(DOTLIB)
|
||||
cp -f r2ai$(DOTEXE) $(R2BINDIR)/r2ai$(DOTEXE)
|
||||
cp -f r2ai.1 $(R2MANDIR)/man1/r2ai.1
|
||||
cp -r ../prompts/* $(HOME)/.config/r2ai/prompts/
|
||||
cp -r ../prompts/* $(R2AI_PROMPTSDIR)/
|
||||
|
||||
user-uninstall:
|
||||
rm -f $(R2USRPLUGDIR)/r2ai$(DOTLIB)
|
||||
@@ -120,3 +152,5 @@ r2check:
|
||||
@r2 -NN -qcq --
|
||||
clean:
|
||||
rm -f *.o *.d
|
||||
|
||||
.PHONY: all bindist try doc-install fmt user-install user-uninstall install uninstall r2check clean test
|
||||
+7
-3
@@ -46,7 +46,7 @@ Usage: r2ai [-args] [...]
|
||||
| r2ai -q [name] (inst) run predefined prompt with optional instructions
|
||||
| r2ai -r enter the chat repl
|
||||
| r2ai -w launch the interactive setup wizard
|
||||
| r2ai -L show chat logs (See -Lj for json). Only for auto mode.
|
||||
| r2ai -L show chat logs (See -Lj for json)
|
||||
| r2ai -L-[N] delete the last (or N last messages from the chat history)
|
||||
| r2ai -R reset the chat conversation context
|
||||
| r2ai -Rq ([text]) refresh and query embeddings (see r2ai.data)
|
||||
@@ -94,7 +94,9 @@ These can be set with `r2ai -e <keyname>=<value>`
|
||||
|------------------|------------------------------------------------------------------------------------------------|
|
||||
| r2ai.api | Name of the provider e.g `openai`. List possibilities with `r2ai -e r2ai.api=?` |
|
||||
| r2ai.model | Model name. List possibilities with `r2ai -e r2ai.model=?` |
|
||||
| r2ai.baseurl | Remote LLM base URL. Specify host if necessary e.g http://127.0.0.1:11434. |
|
||||
| r2ai.baseurl | Provider API root. Use `/v1` for OpenAI-compatible chat or `/api` for native Ollama. |
|
||||
| r2ai.apitype | Use chat completions (default) or the native Ollama `generate` operation. |
|
||||
| r2ai.cacheck | Warn when chat requests are not append-only and may miss provider prompt-cache hits. |
|
||||
| r2ai.max_tokens | Maximum output tokens or maximum total tokens. Check the appropriate limits for your model |
|
||||
| r2ai.temperature | How creative the model should be. 0=not creative, 1=very creative |
|
||||
| r2ai.cmds | R2 command to issue and send output in context to model |
|
||||
@@ -102,7 +104,9 @@ These can be set with `r2ai -e <keyname>=<value>`
|
||||
| r2ai.hlang | Tells LLM in which language to speak |
|
||||
| r2ai.prompt | User prompt to send to LLM with `r2ai -d` |
|
||||
| r2ai.clippy | Display chat replies using `r2clippy` instead of plain text |
|
||||
| r2ai.markdown | Render LLM responses with the radare2 markdown colorizer (default: true) |
|
||||
| r2ai.wizard | Auto-start the setup wizard on the first interactive chat session |
|
||||
| r2ai.history | Keep direct queries (`r2ai [query]`, `r2ai -d`, `r2ai -i`) in the conversation context instead of sending each one alone. Inspect it with `r2ai -L`, reset it with `r2ai -R` (default: false) |
|
||||
| r2ai.auto.max_runs | Maximum loops when using auto mode `r2ai -a` |
|
||||
| r2ai.auto.verbose | Only for auto mode `r2ai -a`. Will show the output of the tool which ran locally |
|
||||
| r2ai.auto.yolo | Set this to true if you don't want r2ai to ask you for approval before running commands sent by the LLM. This is **dangerous**. Recommendation: **leave this to false** unless you fully trust your LLM not to create havoc! |
|
||||
@@ -117,7 +121,7 @@ These can be set with `r2ai -e <keyname>=<value>`
|
||||
```
|
||||
$ r2 ./substitute
|
||||
[0x000010a0]> r2ai -e api=anthropic
|
||||
[0x000010a0]> r2ai -e model=claude-3-7-sonnet-20250219
|
||||
[0x000010a0]> r2ai -e model=claude-sonnet-5-5
|
||||
[0x000010a0]> aa
|
||||
```
|
||||
|
||||
|
||||
+111
-298
@@ -1,319 +1,132 @@
|
||||
/* Copyright r2ai - 2023-2026 - pancake */
|
||||
|
||||
#include "r2ai.h"
|
||||
#include "r2ai_priv.h"
|
||||
|
||||
static const char *modelname(const char *model_name) {
|
||||
return model_name? model_name: "claude-3-7-sonnet-20250219";
|
||||
// request body, vertex takes the api version in the body and the model in the url
|
||||
R_IPI char *r2ai_anthropic_request(const R2AIArgs *args, bool vertex) {
|
||||
char *messages = r_list_empty (args->messages)? NULL: r2ai_msgs_to_anthropic_json (args->messages);
|
||||
if (!messages) {
|
||||
return NULL;
|
||||
}
|
||||
PJ *pj = pj_new ();
|
||||
pj_o (pj);
|
||||
if (vertex) {
|
||||
pj_ks (pj, "anthropic_version", "vertex-2023-10-16");
|
||||
pj_kb (pj, "stream", false);
|
||||
} else {
|
||||
pj_ks (pj, "model", R_STR_ISNOTEMPTY (args->model)? args->model: "claude-sonnet-5-5");
|
||||
}
|
||||
pj_kn (pj, "max_tokens", args->max_tokens? args->max_tokens: 4096);
|
||||
if (args->thinking_tokens >= 1024) {
|
||||
pj_ko (pj, "thinking");
|
||||
pj_ks (pj, "type", "enabled");
|
||||
pj_kn (pj, "budget_tokens", args->thinking_tokens);
|
||||
pj_end (pj);
|
||||
} else if (args->deterministic) {
|
||||
// top_p is not sent because newer models reject it along with temperature
|
||||
pj_kn (pj, "temperature", 0);
|
||||
pj_kn (pj, "top_k", 1);
|
||||
}
|
||||
if (R_STR_ISNOTEMPTY (args->system_prompt)) {
|
||||
pj_ks (pj, "system", args->system_prompt);
|
||||
}
|
||||
pj_k (pj, "messages");
|
||||
pj_raw (pj, messages);
|
||||
free (messages);
|
||||
char *tools = r_list_empty (args->tools)? NULL: r2ai_tools_to_anthropic_json (args->tools);
|
||||
if (tools) {
|
||||
pj_k (pj, "tools");
|
||||
pj_raw (pj, tools);
|
||||
free (tools);
|
||||
}
|
||||
pj_end (pj);
|
||||
return pj_drain (pj);
|
||||
}
|
||||
|
||||
R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args) {
|
||||
RCore *core = cps->core;
|
||||
const char *model = args.model;
|
||||
char **error = args.error;
|
||||
RList *tools = args.tools;
|
||||
RList *messages_input = args.messages;
|
||||
|
||||
if (error) {
|
||||
*error = NULL;
|
||||
}
|
||||
|
||||
// Setup HTTP headers
|
||||
char *auth_header = r_str_newf ("x-api-key: %s", args.api_key);
|
||||
char *anthropic_version = "anthropic-version: 2023-06-01";
|
||||
const char *headers[] = {
|
||||
"Content-Type: application/json",
|
||||
auth_header,
|
||||
anthropic_version,
|
||||
NULL
|
||||
};
|
||||
|
||||
const char *anthropic_url = "https://api.anthropic.com/v1/messages";
|
||||
|
||||
// Get system message if available
|
||||
const char *system_message = NULL;
|
||||
|
||||
// First check if it's provided in args.system_prompt
|
||||
if (R_STR_ISNOTEMPTY (args.system_prompt)) {
|
||||
system_message = args.system_prompt;
|
||||
} else {
|
||||
// If no system_prompt in args, check config as fallback
|
||||
system_message = r_config_get (core->config, "r2ai.system");
|
||||
}
|
||||
|
||||
// Create messages JSON
|
||||
char *messages_json = NULL;
|
||||
|
||||
if (messages_input && !r_list_empty (messages_input)) {
|
||||
// Convert directly to JSON without filtering
|
||||
messages_json = r2ai_msgs_to_anthropic_json (messages_input);
|
||||
if (!messages_json) {
|
||||
if (error) {
|
||||
*error = strdup ("Failed to convert messages to JSON");
|
||||
}
|
||||
free (auth_header);
|
||||
return NULL;
|
||||
}
|
||||
} else {
|
||||
if (error) {
|
||||
*error = strdup ("No input or messages provided");
|
||||
}
|
||||
free (auth_header);
|
||||
char *data = r2ai_anthropic_request (&args, false);
|
||||
if (!data) {
|
||||
*args.error = strdup ("No input or messages provided");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Convert tools to Anthropic format if available
|
||||
char *anthropic_tools_json = NULL;
|
||||
if (tools && !r_list_empty (tools)) {
|
||||
anthropic_tools_json = r2ai_tools_to_anthropic_json (tools);
|
||||
}
|
||||
|
||||
// Create the request JSON
|
||||
PJ *pj = pj_new ();
|
||||
pj_o (pj);
|
||||
pj_ks (pj, "model", modelname (model));
|
||||
pj_kn (pj, "max_tokens", args.max_tokens);
|
||||
if (args.thinking_tokens >= 1024) {
|
||||
pj_ko (pj, "thinking");
|
||||
pj_ks (pj, "type", "enabled");
|
||||
pj_kn (pj, "budget_tokens", args.thinking_tokens);
|
||||
pj_end (pj);
|
||||
}
|
||||
// Add system message if available
|
||||
if (system_message) {
|
||||
pj_ks (pj, "system", system_message);
|
||||
}
|
||||
|
||||
// Add messages
|
||||
pj_k (pj, "messages");
|
||||
// messages_json already contains the array itself, so we use raw
|
||||
pj_raw (pj, messages_json);
|
||||
|
||||
// Add tools if available
|
||||
if (anthropic_tools_json) {
|
||||
pj_k (pj, "tools");
|
||||
pj_raw (pj, anthropic_tools_json);
|
||||
free (anthropic_tools_json);
|
||||
}
|
||||
|
||||
pj_end (pj);
|
||||
|
||||
char *data = pj_drain (pj);
|
||||
free (messages_json);
|
||||
|
||||
// Save the full JSON for debugging
|
||||
char *tmpdir = r_file_tmpdir ();
|
||||
char *req_path = r_str_newf ("%s" R_SYS_DIR "r2ai_anthropic_request.json", tmpdir);
|
||||
r_file_dump (req_path, (const ut8 *)data, strlen (data), 0);
|
||||
R_LOG_DEBUG ("Full request saved to %s", req_path);
|
||||
free (req_path);
|
||||
free (tmpdir);
|
||||
|
||||
R_LOG_DEBUG ("Anthropic API request data: %s", data);
|
||||
|
||||
if (r_config_get_b (core->config, "r2ai.debug")) {
|
||||
// Generate curl command for debugging
|
||||
RStrBuf *curl_cmd = r_strbuf_new ("curl -X POST");
|
||||
for (int i = 0; headers[i]; i++) {
|
||||
r_strbuf_appendf (curl_cmd, " -H \"%s\"", headers[i]);
|
||||
}
|
||||
r_strbuf_appendf (curl_cmd, " -d '%s' \"%s\"", data, anthropic_url);
|
||||
eprintf ("Curl command: %s\n", r_strbuf_get (curl_cmd));
|
||||
r_strbuf_free (curl_cmd);
|
||||
}
|
||||
|
||||
// Make the API call
|
||||
char *auth = r_str_newf ("x-api-key: %s", args.api_key);
|
||||
const char *headers[] = { "Content-Type: application/json", auth, "anthropic-version: 2023-06-01", NULL };
|
||||
int code = 0;
|
||||
char *res = r2ai_http_post (core, anthropic_url, headers, data, &code, NULL);
|
||||
char *res = r2ai_post (cps->core, "anthropic", "https://api.anthropic.com/v1/messages", headers, data, &code, args.error);
|
||||
free (data);
|
||||
free (auth_header);
|
||||
|
||||
if (!res || code != 200) {
|
||||
R_LOG_ERROR ("Anthropic API error %d", code);
|
||||
if (error && res) {
|
||||
*error = strdup (res);
|
||||
} else if (error) {
|
||||
*error = strdup ("Failed to get response from Anthropic API");
|
||||
}
|
||||
free (res);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Save the response for inspection
|
||||
tmpdir = r_file_tmpdir ();
|
||||
char *res_path = r_str_newf ("%s" R_SYS_DIR "r2ai_anthropic_response.json", tmpdir);
|
||||
r_file_dump (res_path, (const ut8 *)res, strlen (res), 0);
|
||||
R_LOG_DEBUG ("Anthropic API response saved to %s", res_path);
|
||||
free (res_path);
|
||||
free (tmpdir);
|
||||
|
||||
if (r_config_get_b (core->config, "r2ai.debug")) {
|
||||
R_LOG_DEBUG ("Anthropic API response: %s", res);
|
||||
}
|
||||
|
||||
R2AI_ChatResponse *result = r2ai_anthropic_parse_response (res, error);
|
||||
free (auth);
|
||||
R2AI_ChatResponse *result = res? r2ai_anthropic_parse_response (res, args.error): NULL;
|
||||
free (res);
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Shared Anthropic response parser -- used by both anthropic.c and vertex.c */
|
||||
R_IPI R2AI_ChatResponse *r2ai_anthropic_parse_response(const char *json, char **error) {
|
||||
R2AI_ChatResponse *result = R_NEW0 (R2AI_ChatResponse);
|
||||
R2AI_Message *message = NULL;
|
||||
R2AI_Usage *usage = NULL;
|
||||
static char *jstr(const RJson *j, const char *key) {
|
||||
return r_str_new (r_json_get_str (j, key));
|
||||
}
|
||||
|
||||
char *response_copy = strdup (json);
|
||||
if (response_copy) {
|
||||
RJson *jres = r_json_parse (response_copy);
|
||||
if (jres) {
|
||||
message = R_NEW0 (R2AI_Message);
|
||||
|
||||
const RJson *usage_json = r_json_get (jres, "usage");
|
||||
if (usage_json && usage_json->type == R_JSON_OBJECT) {
|
||||
usage = R_NEW0 (R2AI_Usage);
|
||||
if (usage) {
|
||||
const RJson *prompt_tokens = r_json_get (usage_json, "input_tokens");
|
||||
const RJson *completion_tokens = r_json_get (usage_json, "output_tokens");
|
||||
if (prompt_tokens && prompt_tokens->type == R_JSON_INTEGER) {
|
||||
usage->prompt_tokens = prompt_tokens->num.u_value;
|
||||
}
|
||||
if (completion_tokens && completion_tokens->type == R_JSON_INTEGER) {
|
||||
usage->completion_tokens = completion_tokens->num.u_value;
|
||||
}
|
||||
usage->total_tokens = usage->prompt_tokens + usage->completion_tokens;
|
||||
}
|
||||
}
|
||||
|
||||
if (message) {
|
||||
message->role = strdup ("assistant");
|
||||
|
||||
RStrBuf *content_buf = r_strbuf_new ("");
|
||||
|
||||
int has_tool_use = 0;
|
||||
int n_tool_calls = 0;
|
||||
|
||||
const RJson *content_array = r_json_get (jres, "content");
|
||||
if (content_array && content_array->type == R_JSON_ARRAY) {
|
||||
const RJson *content_item = content_array->children.first;
|
||||
while (content_item) {
|
||||
const RJson *type = r_json_get (content_item, "type");
|
||||
if (type && type->type == R_JSON_STRING && !strcmp (type->str_value, "tool_use")) {
|
||||
has_tool_use = 1;
|
||||
n_tool_calls++;
|
||||
}
|
||||
content_item = content_item->next;
|
||||
}
|
||||
}
|
||||
|
||||
if (has_tool_use && n_tool_calls > 0) {
|
||||
message->tool_calls = r_list_new ();
|
||||
if (!message->tool_calls) {
|
||||
if (error) {
|
||||
*error = strdup ("Failed to allocate memory for tool calls");
|
||||
}
|
||||
r_json_free (jres);
|
||||
free (response_copy);
|
||||
r2ai_message_free (message);
|
||||
return NULL;
|
||||
}
|
||||
message->tool_calls->free = (RListFree)r2ai_tool_call_free;
|
||||
}
|
||||
|
||||
int tool_idx = 0;
|
||||
if (content_array && content_array->type == R_JSON_ARRAY) {
|
||||
RList *cb = r2ai_content_blocks_new ();
|
||||
if (!cb) {
|
||||
r_json_free (jres);
|
||||
free (response_copy);
|
||||
r2ai_message_free (message);
|
||||
return NULL;
|
||||
}
|
||||
const RJson *content_item = content_array->children.first;
|
||||
while (content_item) {
|
||||
const RJson *type = r_json_get (content_item, "type");
|
||||
if (type && type->type == R_JSON_STRING) {
|
||||
R2AI_ContentBlock *block = R_NEW0 (R2AI_ContentBlock);
|
||||
if (!strcmp (type->str_value, "text")) {
|
||||
const RJson *text = r_json_get (content_item, "text");
|
||||
if (text && text->type == R_JSON_STRING) {
|
||||
r_strbuf_append (content_buf, text->str_value);
|
||||
block->type = strdup ("text");
|
||||
block->text = strdup (text->str_value);
|
||||
}
|
||||
} else if (!strcmp (type->str_value, "tool_use") && tool_idx < n_tool_calls) {
|
||||
const RJson *name = r_json_get (content_item, "name");
|
||||
const RJson *id = r_json_get (content_item, "id");
|
||||
const RJson *input = r_json_get (content_item, "input");
|
||||
|
||||
block->type = strdup ("tool_use");
|
||||
R2AI_ToolCall *tc = R_NEW0 (R2AI_ToolCall);
|
||||
if (name && name->type == R_JSON_STRING) {
|
||||
block->name = strdup (name->str_value);
|
||||
tc->name = strdup (name->str_value);
|
||||
}
|
||||
|
||||
if (id && id->type == R_JSON_STRING) {
|
||||
block->id = strdup (id->str_value);
|
||||
tc->id = strdup (id->str_value);
|
||||
}
|
||||
|
||||
if (input && input->type == R_JSON_OBJECT) {
|
||||
char *input_str = r_json_to_string (input);
|
||||
if (input_str) {
|
||||
R_LOG_DEBUG ("Input string: %s", input_str);
|
||||
block->input = strdup (input_str);
|
||||
tc->arguments = strdup (input_str);
|
||||
free (input_str);
|
||||
}
|
||||
}
|
||||
|
||||
r_list_append (message->tool_calls, tc);
|
||||
tool_idx++;
|
||||
} else if (!strcmp (type->str_value, "thinking")) {
|
||||
const RJson *tdata = r_json_get (content_item, "data");
|
||||
const RJson *thinking = r_json_get (content_item, "thinking");
|
||||
const RJson *signature = r_json_get (content_item, "signature");
|
||||
if (tdata && tdata->type == R_JSON_STRING) {
|
||||
block->data = strdup (tdata->str_value);
|
||||
}
|
||||
if (thinking && thinking->type == R_JSON_STRING) {
|
||||
block->thinking = strdup (thinking->str_value);
|
||||
}
|
||||
if (signature && signature->type == R_JSON_STRING) {
|
||||
block->signature = strdup (signature->str_value);
|
||||
}
|
||||
block->type = strdup ("thinking");
|
||||
|
||||
r_strbuf_append (content_buf, "\n" Color_GRAY "<thinking>\n");
|
||||
r_strbuf_append (content_buf, block->thinking);
|
||||
r_strbuf_append (content_buf, "\n</thinking>" Color_RESET "\n");
|
||||
}
|
||||
r_list_append (cb, block);
|
||||
}
|
||||
content_item = content_item->next;
|
||||
}
|
||||
message->content_blocks = cb;
|
||||
}
|
||||
|
||||
message->content = r_strbuf_drain (content_buf);
|
||||
|
||||
if (!message->content && (!message->tool_calls || r_list_empty (message->tool_calls))) {
|
||||
r2ai_message_free (message);
|
||||
message = NULL;
|
||||
}
|
||||
}
|
||||
r_json_free (jres);
|
||||
// fill the block from the content item and append its text to the message content
|
||||
static void parse_block(R2AI_ContentBlock *b, const RJson *item, R2AI_Message *msg, RStrBuf *sb) {
|
||||
if (!strcmp (b->type, "text")) {
|
||||
b->text = jstr (item, "text");
|
||||
r_strbuf_append (sb, r_str_get (b->text));
|
||||
} else if (!strcmp (b->type, "tool_use")) {
|
||||
const RJson *input = r_json_get (item, "input");
|
||||
b->id = jstr (item, "id");
|
||||
b->name = jstr (item, "name");
|
||||
b->input = (input && input->type == R_JSON_OBJECT)? r_json_to_string (input): NULL;
|
||||
R2AI_ToolCall *tc = R_NEW0 (R2AI_ToolCall);
|
||||
tc->id = r_str_new (b->id);
|
||||
tc->name = r_str_new (b->name);
|
||||
tc->arguments = r_str_new (b->input);
|
||||
if (!msg->tool_calls) {
|
||||
msg->tool_calls = r_list_newf ((RListFree)r2ai_tool_call_free);
|
||||
}
|
||||
free (response_copy);
|
||||
r_list_append (msg->tool_calls, tc);
|
||||
} else if (!strcmp (b->type, "thinking")) {
|
||||
b->data = jstr (item, "data");
|
||||
b->thinking = jstr (item, "thinking");
|
||||
b->signature = jstr (item, "signature");
|
||||
r_strbuf_appendf (sb, "\n" Color_GRAY "<thinking>\n%s\n</thinking>" Color_RESET "\n", r_str_get (b->thinking));
|
||||
}
|
||||
|
||||
result->message = message;
|
||||
result->usage = usage;
|
||||
return result;
|
||||
}
|
||||
|
||||
R_IPI char *r2ai_anthropic_stream(RCore *core, R2AIArgs args) {
|
||||
(void)core;
|
||||
(void)args;
|
||||
// Not implemented yet
|
||||
return NULL;
|
||||
// parses (and modifies) the json response of the anthropic and vertex-anthropic providers
|
||||
R_IPI R2AI_ChatResponse *r2ai_anthropic_parse_response(char *json, char **error) {
|
||||
RJson *j = r_json_parse (json);
|
||||
if (!j) {
|
||||
*error = strdup ("Failed to parse Anthropic response JSON");
|
||||
return NULL;
|
||||
}
|
||||
R2AI_ChatResponse *res = R_NEW0 (R2AI_ChatResponse);
|
||||
const RJson *u = r_json_get (j, "usage");
|
||||
if (u) {
|
||||
R2AI_Usage *usage = R_NEW0 (R2AI_Usage);
|
||||
usage->prompt_tokens = r_json_get_num (u, "input_tokens");
|
||||
usage->completion_tokens = r_json_get_num (u, "output_tokens");
|
||||
usage->total_tokens = usage->prompt_tokens + usage->completion_tokens;
|
||||
res->usage = usage;
|
||||
}
|
||||
R2AI_Message *msg = R_NEW0 (R2AI_Message);
|
||||
msg->role = strdup ("assistant");
|
||||
RStrBuf *sb = r_strbuf_new ("");
|
||||
const RJson *content = r_json_get (j, "content");
|
||||
if (content && content->type == R_JSON_ARRAY) {
|
||||
msg->content_blocks = r2ai_content_blocks_new ();
|
||||
const RJson *item;
|
||||
for (item = content->children.first; item; item = item->next) {
|
||||
char *type = jstr (item, "type");
|
||||
if (!type) {
|
||||
continue;
|
||||
}
|
||||
R2AI_ContentBlock *b = R_NEW0 (R2AI_ContentBlock);
|
||||
b->type = type;
|
||||
parse_block (b, item, msg, sb);
|
||||
r_list_append (msg->content_blocks, b);
|
||||
}
|
||||
}
|
||||
msg->content = r_strbuf_drain (sb);
|
||||
res->message = msg;
|
||||
r_json_free (j);
|
||||
return res;
|
||||
}
|
||||
+27
-3
@@ -30,16 +30,40 @@ R_API void r2ai_apikeys_edit(RCorePluginSession *cps) {
|
||||
"# MISTRAL=your-mistral-api-key-here\n";
|
||||
r_file_dump (keys_path, (const ut8 *)template, strlen (template), 0);
|
||||
}
|
||||
r_cons_editor (core->cons, keys_path, NULL);
|
||||
r2ai_cons_editor (core->cons, keys_path, NULL);
|
||||
free (keys_path);
|
||||
}
|
||||
|
||||
/* Return a malloc'd API key read from ~/.config/r2ai/apikeys.txt
|
||||
* Provider matching is case-insensitive */
|
||||
static char *r2ai_apikeys_env(const char *provider) {
|
||||
char *upper = strdup (provider);
|
||||
r_str_case (upper, true);
|
||||
char *name = r_str_newf ("%s_API_KEY", upper);
|
||||
char *val = r_sys_getenv (name);
|
||||
if (val) {
|
||||
r_str_trim (val);
|
||||
}
|
||||
free (name);
|
||||
free (upper);
|
||||
if (R_STR_ISNOTEMPTY (val)) {
|
||||
return val;
|
||||
}
|
||||
free (val);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Return a malloc'd API key for a provider.
|
||||
* Resolution order:
|
||||
* 1. environment variable "<PROVIDER>_API_KEY" (provider upper-cased)
|
||||
* 2. the ~/.config/r2ai/apikeys.txt file
|
||||
* Provider matching against the file is case-insensitive. */
|
||||
R_API char *r2ai_apikeys_get(const char *provider) {
|
||||
if (!provider) {
|
||||
return NULL;
|
||||
}
|
||||
char *env_key = r2ai_apikeys_env (provider);
|
||||
if (env_key) {
|
||||
return env_key;
|
||||
}
|
||||
bool exists = false;
|
||||
char *config_file = r2ai_apikeys_path (&exists);
|
||||
if (!exists) {
|
||||
|
||||
+193
-277
@@ -35,6 +35,24 @@ static bool task_is_live_locked(const R2AITask *t) {
|
||||
return s == R2AI_TASK_PENDING || s == R2AI_TASK_RUNNING || s == R2AI_TASK_WAIT_APPROVE || s == R2AI_TASK_WAIT_INPUT;
|
||||
}
|
||||
|
||||
static bool is_done(R2AITaskState st) {
|
||||
return st == R2AI_TASK_COMPLETE || st == R2AI_TASK_ERROR || st == R2AI_TASK_CANCELLED;
|
||||
}
|
||||
|
||||
static R2AITaskQueue *queue(RCorePluginSession *cps) {
|
||||
return ((R2AI_State *)cps->data)->async;
|
||||
}
|
||||
|
||||
static void print_block(RCons *cons, const char *hdr, const char *text) {
|
||||
if (R_STR_ISEMPTY (text)) {
|
||||
return;
|
||||
}
|
||||
r_cons_printf (cons, "%s%s", hdr, text);
|
||||
if (!r_str_endswith (text, "\n")) {
|
||||
r_cons_newline (cons);
|
||||
}
|
||||
}
|
||||
|
||||
/* Append text to task->output. Takes task lock. */
|
||||
static void task_append_output(R2AITask *t, const char *text) {
|
||||
if (R_STR_ISEMPTY (text)) {
|
||||
@@ -54,6 +72,12 @@ static void task_append_outputf(R2AITask *t, const char *fmt, ...) {
|
||||
free (s);
|
||||
}
|
||||
|
||||
static void pending_clear(R2AITask *t) {
|
||||
R_FREE (t->pending_tool_name);
|
||||
R_FREE (t->pending_tool_args);
|
||||
R_FREE (t->pending_tool_call_id);
|
||||
}
|
||||
|
||||
static void task_free(R2AITask *t) {
|
||||
if (!t) {
|
||||
return;
|
||||
@@ -71,6 +95,9 @@ static void task_free(R2AITask *t) {
|
||||
if (t->messages) {
|
||||
r2ai_msgs_free (t->messages);
|
||||
}
|
||||
r_list_free (t->tools);
|
||||
r_config_free (t->llm_core->config);
|
||||
free (t->llm_core);
|
||||
free (t->title);
|
||||
free (t->query);
|
||||
free (t->system_prompt);
|
||||
@@ -78,9 +105,7 @@ static void task_free(R2AITask *t) {
|
||||
free (t->provider);
|
||||
r_strbuf_free (t->output);
|
||||
free (t->error);
|
||||
free (t->pending_tool_name);
|
||||
free (t->pending_tool_args);
|
||||
free (t->pending_tool_call_id);
|
||||
pending_clear (t);
|
||||
free (t->tool_result);
|
||||
free (t);
|
||||
}
|
||||
@@ -92,31 +117,50 @@ static void queue_unlock(R2AITaskQueue *q) {
|
||||
r_th_lock_leave (q->lock);
|
||||
}
|
||||
|
||||
/* Build the LLM args for the worker, without any live RCore work. */
|
||||
static void fill_args_from_task(R2AITask *t, R2AIArgs *args, char **error) {
|
||||
memset (args, 0, sizeof (*args));
|
||||
args->messages = t->messages;
|
||||
args->system_prompt = t->system_prompt;
|
||||
args->model = t->model;
|
||||
args->provider = t->provider;
|
||||
args->error = error;
|
||||
args->dorag = false;
|
||||
static void task_start(R2AITask *t) {
|
||||
task_lock (t);
|
||||
t->state = R2AI_TASK_RUNNING;
|
||||
t->started = time (NULL);
|
||||
task_unlock (t);
|
||||
}
|
||||
|
||||
/* Worker: execute one LLM turn, appending any text output.
|
||||
* Returns the R2AI_ChatResponse (caller owns). */
|
||||
static bool cancelled(R2AITask *t) {
|
||||
task_lock (t);
|
||||
bool res = t->cancel_req;
|
||||
task_unlock (t);
|
||||
return res;
|
||||
}
|
||||
|
||||
// terminate the worker, an earlier error reported by the llm call takes precedence
|
||||
static RThreadFunctionRet finish(R2AITask *t, R2AITaskState st, char *err) {
|
||||
task_lock (t);
|
||||
if (err && !t->error) {
|
||||
t->error = err;
|
||||
} else {
|
||||
free (err);
|
||||
}
|
||||
t->state = st;
|
||||
t->finished = time (NULL);
|
||||
task_unlock (t);
|
||||
return R_TH_STOP;
|
||||
}
|
||||
|
||||
/* Providers read core->config throughout a request. The HTTP server swaps
|
||||
* that pointer between its web and shell contexts, so use the task snapshot.
|
||||
* Tool execution still uses the original session on the main thread. */
|
||||
static R2AI_ChatResponse *run_llm_once(R2AITask *t) {
|
||||
char *error = NULL;
|
||||
R2AIArgs args;
|
||||
fill_args_from_task (t, &args, &error);
|
||||
|
||||
/* For AUTO tasks we must send tools. */
|
||||
if (t->kind == R2AI_TASK_AUTO) {
|
||||
args.tools = r2ai_get_tools (t->cps->core, t->cps->data);
|
||||
}
|
||||
|
||||
R2AI_ChatResponse *res = r2ai_llmcall (t->cps, args);
|
||||
|
||||
R2AIArgs args = {
|
||||
.messages = t->messages,
|
||||
.system_prompt = t->system_prompt,
|
||||
.model = t->model,
|
||||
.provider = t->provider,
|
||||
.tools = t->tools,
|
||||
.error = &error,
|
||||
};
|
||||
RCorePluginSession worker_session = *t->cps;
|
||||
worker_session.core = t->llm_core;
|
||||
R2AI_ChatResponse *res = r2ai_llmcall (&worker_session, args);
|
||||
if (error) {
|
||||
task_lock (t);
|
||||
free (t->error);
|
||||
@@ -126,11 +170,7 @@ static R2AI_ChatResponse *run_llm_once(R2AITask *t) {
|
||||
return res;
|
||||
}
|
||||
|
||||
/* Append human-readable chunks of the assistant message to task output. */
|
||||
static void dump_message_to_output(R2AITask *t, const R2AI_Message *m) {
|
||||
if (!m) {
|
||||
return;
|
||||
}
|
||||
static void dump_message(R2AITask *t, const R2AI_Message *m) {
|
||||
if (m->reasoning_content) {
|
||||
task_append_outputf (t, "<thinking>\n%s\n</thinking>\n", m->reasoning_content);
|
||||
}
|
||||
@@ -139,168 +179,100 @@ static void dump_message_to_output(R2AITask *t, const R2AI_Message *m) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Thread entry-point for QUERY tasks. */
|
||||
static RThreadFunctionRet worker_query(RThread *th) {
|
||||
R2AITask *t = (R2AITask *)th->user;
|
||||
task_lock (t);
|
||||
t->state = R2AI_TASK_RUNNING;
|
||||
t->started = time (NULL);
|
||||
task_unlock (t);
|
||||
|
||||
task_start (t);
|
||||
R2AI_ChatResponse *res = run_llm_once (t);
|
||||
task_lock (t);
|
||||
if (t->cancel_req) {
|
||||
t->state = R2AI_TASK_CANCELLED;
|
||||
} else if (!res || !res->message) {
|
||||
if (!t->error) {
|
||||
t->error = strdup ("llm call returned no response");
|
||||
}
|
||||
t->state = R2AI_TASK_ERROR;
|
||||
} else {
|
||||
const R2AI_Message *m = res->message;
|
||||
task_unlock (t);
|
||||
dump_message_to_output (t, m);
|
||||
task_lock (t);
|
||||
t->state = R2AI_TASK_COMPLETE;
|
||||
bool ok = res && res->message;
|
||||
bool cancel = cancelled (t);
|
||||
if (ok && !cancel) {
|
||||
dump_message (t, res->message);
|
||||
}
|
||||
t->finished = time (NULL);
|
||||
task_unlock (t);
|
||||
|
||||
if (res) {
|
||||
r2ai_message_free ((R2AI_Message *)res->message);
|
||||
free (res);
|
||||
r2ai_chat_response_free (res);
|
||||
if (cancel) {
|
||||
return finish (t, R2AI_TASK_CANCELLED, NULL);
|
||||
}
|
||||
return R_TH_STOP;
|
||||
return ok
|
||||
? finish (t, R2AI_TASK_COMPLETE, NULL)
|
||||
: finish (t, R2AI_TASK_ERROR, strdup ("llm call returned no response"));
|
||||
}
|
||||
|
||||
/* Thread entry-point for AUTO tasks. */
|
||||
static RThreadFunctionRet worker_auto(RThread *th) {
|
||||
R2AITask *t = (R2AITask *)th->user;
|
||||
static R2AI_ToolCall *first_tool_call(const R2AI_Message *m) {
|
||||
RListIter *iter;
|
||||
R2AI_ToolCall *tc;
|
||||
r_list_foreach (m->tool_calls, iter, tc) {
|
||||
if (tc->name && tc->arguments && tc->id) {
|
||||
return tc;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// hand the tool call to the main thread and block until it answers, returns false if cancelled
|
||||
static bool wait_tool(R2AITask *t, const R2AI_ToolCall *tc) {
|
||||
task_lock (t);
|
||||
t->state = R2AI_TASK_RUNNING;
|
||||
t->started = time (NULL);
|
||||
int max_runs = r_config_get_i (t->cps->core->config, "r2ai.auto.max_runs");
|
||||
pending_clear (t);
|
||||
R_FREE (t->tool_result);
|
||||
t->pending_tool_name = strdup (tc->name);
|
||||
t->pending_tool_args = strdup (tc->arguments);
|
||||
t->pending_tool_call_id = strdup (tc->id);
|
||||
t->state = R2AI_TASK_WAIT_APPROVE;
|
||||
task_unlock (t);
|
||||
|
||||
while (true) {
|
||||
r_th_sem_wait (t->gate);
|
||||
|
||||
task_lock (t);
|
||||
if (t->cancel_req) {
|
||||
task_unlock (t);
|
||||
return false;
|
||||
}
|
||||
R2AI_Message msg = {
|
||||
.role = "tool",
|
||||
.tool_call_id = t->pending_tool_call_id,
|
||||
.content = t->tool_result? t->tool_result: "<no output>",
|
||||
};
|
||||
r2ai_msgs_add (t->messages, &msg);
|
||||
R_FREE (t->tool_result);
|
||||
pending_clear (t);
|
||||
task_unlock (t);
|
||||
return true;
|
||||
}
|
||||
|
||||
static RThreadFunctionRet worker_auto(RThread *th) {
|
||||
R2AITask *t = (R2AITask *)th->user;
|
||||
task_start (t);
|
||||
for (;;) {
|
||||
if (cancelled (t)) {
|
||||
return finish (t, R2AI_TASK_CANCELLED, NULL);
|
||||
}
|
||||
if (t->steps >= t->max_runs) {
|
||||
return finish (t, R2AI_TASK_ERROR, r_str_newf ("max runs (%d) reached", t->max_runs));
|
||||
}
|
||||
task_lock (t);
|
||||
if (t->cancel_req) {
|
||||
t->state = R2AI_TASK_CANCELLED;
|
||||
t->finished = time (NULL);
|
||||
task_unlock (t);
|
||||
return R_TH_STOP;
|
||||
}
|
||||
if (t->steps >= max_runs) {
|
||||
t->state = R2AI_TASK_ERROR;
|
||||
free (t->error);
|
||||
t->error = r_str_newf ("max runs (%d) reached", max_runs);
|
||||
t->finished = time (NULL);
|
||||
task_unlock (t);
|
||||
return R_TH_STOP;
|
||||
}
|
||||
t->steps++;
|
||||
t->state = R2AI_TASK_RUNNING;
|
||||
task_unlock (t);
|
||||
|
||||
R2AI_ChatResponse *res = run_llm_once (t);
|
||||
if (!res || !res->message) {
|
||||
task_lock (t);
|
||||
if (!t->error) {
|
||||
t->error = strdup ("llm call returned no response");
|
||||
}
|
||||
t->state = R2AI_TASK_ERROR;
|
||||
t->finished = time (NULL);
|
||||
task_unlock (t);
|
||||
if (res) {
|
||||
free (res);
|
||||
}
|
||||
return R_TH_STOP;
|
||||
r2ai_chat_response_free (res);
|
||||
return finish (t, R2AI_TASK_ERROR, strdup ("llm call returned no response"));
|
||||
}
|
||||
const R2AI_Message *m = res->message;
|
||||
dump_message_to_output (t, m);
|
||||
dump_message (t, m);
|
||||
r2ai_msgs_add (t->messages, m);
|
||||
|
||||
bool has_tool = m->tool_calls && r_list_length (m->tool_calls) > 0;
|
||||
if (!has_tool) {
|
||||
task_lock (t);
|
||||
t->state = R2AI_TASK_COMPLETE;
|
||||
t->finished = time (NULL);
|
||||
task_unlock (t);
|
||||
r2ai_message_free ((R2AI_Message *)m);
|
||||
free (res);
|
||||
return R_TH_STOP;
|
||||
if (r_list_empty (m->tool_calls)) {
|
||||
r2ai_chat_response_free (res);
|
||||
return finish (t, R2AI_TASK_COMPLETE, NULL);
|
||||
}
|
||||
|
||||
/* Take the first valid tool call. */
|
||||
RListIter *iter;
|
||||
R2AI_ToolCall *tc, *valid_tc = NULL;
|
||||
r_list_foreach (m->tool_calls, iter, tc) {
|
||||
if (tc->name && tc->arguments && tc->id) {
|
||||
valid_tc = tc;
|
||||
break;
|
||||
}
|
||||
R2AI_ToolCall *tc = first_tool_call (m);
|
||||
bool ok = tc && wait_tool (t, tc);
|
||||
r2ai_chat_response_free (res);
|
||||
if (!ok) {
|
||||
return tc
|
||||
? finish (t, R2AI_TASK_CANCELLED, NULL)
|
||||
: finish (t, R2AI_TASK_ERROR, strdup ("llm returned an invalid tool call"));
|
||||
}
|
||||
if (!valid_tc) {
|
||||
task_lock (t);
|
||||
t->state = R2AI_TASK_ERROR;
|
||||
free (t->error);
|
||||
t->error = strdup ("llm returned an invalid tool call");
|
||||
t->finished = time (NULL);
|
||||
task_unlock (t);
|
||||
r2ai_message_free ((R2AI_Message *)m);
|
||||
free (res);
|
||||
return R_TH_STOP;
|
||||
}
|
||||
tc = valid_tc;
|
||||
char *name = tc->name? strdup (tc->name): NULL;
|
||||
char *argsjson = tc->arguments? strdup (tc->arguments): NULL;
|
||||
char *callid = tc->id? strdup (tc->id): NULL;
|
||||
r2ai_message_free ((R2AI_Message *)m);
|
||||
free (res);
|
||||
|
||||
task_lock (t);
|
||||
free (t->pending_tool_name);
|
||||
free (t->pending_tool_args);
|
||||
free (t->pending_tool_call_id);
|
||||
free (t->tool_result);
|
||||
t->pending_tool_name = name;
|
||||
t->pending_tool_args = argsjson;
|
||||
t->pending_tool_call_id = callid;
|
||||
t->tool_result = NULL;
|
||||
t->state = R2AI_TASK_WAIT_APPROVE;
|
||||
task_unlock (t);
|
||||
|
||||
/* Wait for main thread to provide tool_result via gate. */
|
||||
r_th_sem_wait (t->gate);
|
||||
|
||||
task_lock (t);
|
||||
if (t->cancel_req) {
|
||||
t->state = R2AI_TASK_CANCELLED;
|
||||
t->finished = time (NULL);
|
||||
task_unlock (t);
|
||||
return R_TH_STOP;
|
||||
}
|
||||
char *tool_out = t->tool_result? t->tool_result: strdup ("<no output>");
|
||||
t->tool_result = NULL;
|
||||
char *tool_id = t->pending_tool_call_id? strdup (t->pending_tool_call_id): NULL;
|
||||
free (t->pending_tool_name);
|
||||
free (t->pending_tool_args);
|
||||
free (t->pending_tool_call_id);
|
||||
t->pending_tool_name = NULL;
|
||||
t->pending_tool_args = NULL;
|
||||
t->pending_tool_call_id = NULL;
|
||||
task_unlock (t);
|
||||
|
||||
R2AI_Message tool_msg = {
|
||||
.role = "tool",
|
||||
.tool_call_id = tool_id,
|
||||
.content = tool_out,
|
||||
};
|
||||
r2ai_msgs_add (t->messages, &tool_msg);
|
||||
free (tool_id);
|
||||
free (tool_out);
|
||||
}
|
||||
return R_TH_STOP;
|
||||
}
|
||||
|
||||
R_IPI void r2ai_async_init(R2AI_State *state) {
|
||||
@@ -328,13 +300,18 @@ R_IPI void r2ai_async_fini(R2AI_State *state) {
|
||||
task_lock (t);
|
||||
bool live = task_is_live_locked (t);
|
||||
t->cancel_req = true;
|
||||
/* take ownership of the thread while holding the task lock */
|
||||
RThread *th = live? t->thread: NULL;
|
||||
if (th) {
|
||||
t->thread = NULL;
|
||||
}
|
||||
task_unlock (t);
|
||||
if (t->gate) {
|
||||
r_th_sem_post (t->gate);
|
||||
}
|
||||
if (live && t->thread) {
|
||||
r_th_kill_free (t->thread);
|
||||
t->thread = NULL;
|
||||
if (th) {
|
||||
/* kill_free joins the thread: never do it under the task lock */
|
||||
r_th_kill_free (th);
|
||||
killed++;
|
||||
}
|
||||
}
|
||||
@@ -352,19 +329,30 @@ static R2AITask *task_new(RCorePluginSession *cps, R2AITaskKind kind, const char
|
||||
RCore *core = cps->core;
|
||||
R2AITask *t = R_NEW0 (R2AITask);
|
||||
t->cps = cps;
|
||||
/* Snapshot on the main thread, before it can mutate the shared core.
|
||||
* Only config is owned; providers borrow the remaining session fields. */
|
||||
t->llm_core = R_NEW (RCore);
|
||||
*t->llm_core = *core;
|
||||
t->llm_core->config = r_config_clone (core->config);
|
||||
t->kind = kind;
|
||||
t->state = R2AI_TASK_PENDING;
|
||||
t->title = strdup (title? title: "");
|
||||
t->query = strdup (query? query: "");
|
||||
// resolve the system prompt here, workers must not read the config
|
||||
if (R_STR_ISEMPTY (system_prompt) && kind == R2AI_TASK_AUTO) {
|
||||
t->system_prompt = r2ai_auto_system_prompt (cps);
|
||||
} else {
|
||||
t->system_prompt = system_prompt? strdup (system_prompt): NULL;
|
||||
t->system_prompt = r_str_new (r2ai_system_prompt (core, system_prompt));
|
||||
}
|
||||
const char *m = r_config_get (core->config, "r2ai.model");
|
||||
const char *p = r_config_get (core->config, "r2ai.api");
|
||||
t->model = m? strdup (m): NULL;
|
||||
t->provider = p? strdup (p): NULL;
|
||||
if (kind == R2AI_TASK_AUTO) {
|
||||
RList *tools = r2ai_get_tools (core, cps->data);
|
||||
t->tools = tools? r_list_clone (tools, NULL): NULL;
|
||||
t->max_runs = r_config_get_i (core->config, "r2ai.auto.max_runs");
|
||||
}
|
||||
t->messages = r2ai_msgs_new ();
|
||||
R2AI_Message um = { .role = "user", .content = (char *)query };
|
||||
r2ai_msgs_add (t->messages, &um);
|
||||
@@ -384,22 +372,15 @@ static int queue_register(R2AITaskQueue *q, R2AITask *t) {
|
||||
}
|
||||
|
||||
static int submit(RCorePluginSession *cps, R2AITaskKind kind, const char *title, const char *query, const char *sysp, RThreadFunction fn) {
|
||||
R2AI_State *state = cps->data;
|
||||
if (!state || !state->async) {
|
||||
return -1;
|
||||
}
|
||||
R2AITask *t = task_new (cps, kind, title, query, sysp);
|
||||
int id = queue_register (state->async, t);
|
||||
int id = queue_register (queue (cps), t);
|
||||
t->thread = r_th_new (fn, t, 0);
|
||||
if (t->thread) {
|
||||
r_th_start (t->thread);
|
||||
} else {
|
||||
task_lock (t);
|
||||
t->state = R2AI_TASK_ERROR;
|
||||
free (t->error);
|
||||
t->error = strdup ("failed to spawn worker");
|
||||
task_unlock (t);
|
||||
finish (t, R2AI_TASK_ERROR, strdup ("failed to spawn worker"));
|
||||
}
|
||||
r_cons_printf (cps->core->cons, "[async] task %d queued (%s)\n", id, t->title);
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -420,13 +401,8 @@ R_IPI int r2ai_async_auto(RCorePluginSession *cps,
|
||||
static void purge_finished(RCorePluginSession *cps);
|
||||
|
||||
static void show_task_list(RCorePluginSession *cps, bool json) {
|
||||
R2AI_State *state = cps->data;
|
||||
RCore *core = cps->core;
|
||||
if (!state || !state->async) {
|
||||
r_cons_printf (core->cons, "async queue not initialised\n");
|
||||
return;
|
||||
}
|
||||
R2AITaskQueue *q = state->async;
|
||||
R2AITaskQueue *q = queue (cps);
|
||||
queue_lock (q);
|
||||
if (json) {
|
||||
PJ *pj = r_core_pj_new (cps->core);
|
||||
@@ -486,13 +462,7 @@ static void show_task_list(RCorePluginSession *cps, bool json) {
|
||||
extra = extrabuf;
|
||||
}
|
||||
r_cons_printf (core->cons, "%-4d %-6s %-13s %-4d %s%s\n", t->id, kind_name (t->kind), state_name (t->state), age, t->title? t->title: "", extra);
|
||||
const char *out = r_strbuf_get (t->output);
|
||||
if (R_STR_ISNOTEMPTY (out)) {
|
||||
r_cons_printf (core->cons, "output:\n%s", out);
|
||||
if (!r_str_endswith (out, "\n")) {
|
||||
r_cons_newline (core->cons);
|
||||
}
|
||||
}
|
||||
print_block (core->cons, "output:\n", r_strbuf_get (t->output));
|
||||
free (extrabuf);
|
||||
task_unlock (t);
|
||||
}
|
||||
@@ -601,11 +571,7 @@ static bool answer_wait_approve(RCorePluginSession *cps, R2AITask *t, bool appro
|
||||
/* Interactive handler: act on first actionable task (or the given id). */
|
||||
static void interact_once(RCorePluginSession *cps, int id) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
if (!state || !state->async) {
|
||||
return;
|
||||
}
|
||||
R2AITaskQueue *q = state->async;
|
||||
R2AITaskQueue *q = queue (cps);
|
||||
R2AITask *t = find_actionable (q, id);
|
||||
if (!t) {
|
||||
/* Nothing to do - silent by default so cmd.prompt stays clean. */
|
||||
@@ -625,19 +591,14 @@ static void interact_once(RCorePluginSession *cps, int id) {
|
||||
queue_unlock (q);
|
||||
|
||||
r_cons_printf (core->cons, "\n" Color_BLUE "[async task %d | %s | %s]" Color_RESET "\n", tid, kind_name (kind), state_name (st));
|
||||
if (!R_STR_ISEMPTY (output)) {
|
||||
r_cons_printf (core->cons, "%s", output);
|
||||
if (!r_str_endswith (output, "\n")) {
|
||||
r_cons_newline (core->cons);
|
||||
}
|
||||
}
|
||||
print_block (core->cons, "", output);
|
||||
free (output);
|
||||
if (err) {
|
||||
r_cons_printf (core->cons, Color_RED "error: %s" Color_RESET "\n", err);
|
||||
free (err);
|
||||
}
|
||||
|
||||
if (st == R2AI_TASK_COMPLETE || st == R2AI_TASK_ERROR || st == R2AI_TASK_CANCELLED) {
|
||||
if (is_done (st)) {
|
||||
/* Remove the task from the queue (join the thread on free). */
|
||||
queue_lock (q);
|
||||
drop_task_locked (q, t);
|
||||
@@ -655,16 +616,17 @@ static void interact_once(RCorePluginSession *cps, int id) {
|
||||
r_cons_flush (core->cons);
|
||||
|
||||
bool yolo = r_config_get_b (core->config, "r2ai.auto.yolo");
|
||||
char *question = r_str_newf ("Run tool %s? (Y/n)", tool_name? tool_name: "?");
|
||||
bool approve = yolo? true: r_cons_yesno (core->cons, 'y', "%s", question? question: "Run tool? (Y/n)");
|
||||
free (question);
|
||||
bool approve = yolo || r_cons_yesno (core->cons, 'y', "Run tool %s? (Y/n)", tool_name? tool_name: "?");
|
||||
answer_wait_approve (cps, t, approve, true);
|
||||
}
|
||||
free (tool_name);
|
||||
free (tool_args);
|
||||
}
|
||||
|
||||
static R2AITask *find_by_id(R2AITaskQueue *q, int id) {
|
||||
// lock the queue and find the task, reports and unlocks when it does not exist
|
||||
static R2AITask *lookup(RCorePluginSession *cps, int id) {
|
||||
R2AITaskQueue *q = queue (cps);
|
||||
queue_lock (q);
|
||||
RListIter *it;
|
||||
R2AITask *t;
|
||||
r_list_foreach (q->tasks, it, t) {
|
||||
@@ -672,25 +634,20 @@ static R2AITask *find_by_id(R2AITaskQueue *q, int id) {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
queue_unlock (q);
|
||||
r_cons_printf (cps->core->cons, "No task with id %d\n", id);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void answer_by_id(RCorePluginSession *cps, int id, bool approve) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
if (!state || !state->async) {
|
||||
return;
|
||||
}
|
||||
if (id <= 0) {
|
||||
r_cons_printf (core->cons, "Missing task id\n");
|
||||
return;
|
||||
}
|
||||
R2AITaskQueue *q = state->async;
|
||||
queue_lock (q);
|
||||
R2AITask *t = find_by_id (q, id);
|
||||
R2AITaskQueue *q = queue (cps);
|
||||
R2AITask *t = lookup (cps, id);
|
||||
if (!t) {
|
||||
queue_unlock (q);
|
||||
r_cons_printf (core->cons, "No task with id %d\n", id);
|
||||
return;
|
||||
}
|
||||
task_lock (t);
|
||||
@@ -724,16 +681,9 @@ static void kill_task_locked(R2AITaskQueue *q, R2AITask *t) {
|
||||
|
||||
static void kill_by_id(RCorePluginSession *cps, int id) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
if (!state || !state->async) {
|
||||
return;
|
||||
}
|
||||
R2AITaskQueue *q = state->async;
|
||||
queue_lock (q);
|
||||
R2AITask *t = find_by_id (q, id);
|
||||
R2AITaskQueue *q = queue (cps);
|
||||
R2AITask *t = lookup (cps, id);
|
||||
if (!t) {
|
||||
queue_unlock (q);
|
||||
r_cons_printf (core->cons, "No task with id %d\n", id);
|
||||
return;
|
||||
}
|
||||
kill_task_locked (q, t);
|
||||
@@ -743,11 +693,7 @@ static void kill_by_id(RCorePluginSession *cps, int id) {
|
||||
|
||||
static void kill_all(RCorePluginSession *cps) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
if (!state || !state->async) {
|
||||
return;
|
||||
}
|
||||
R2AITaskQueue *q = state->async;
|
||||
R2AITaskQueue *q = queue (cps);
|
||||
queue_lock (q);
|
||||
int n = 0;
|
||||
while (!r_list_empty (q->tasks)) {
|
||||
@@ -761,51 +707,29 @@ static void kill_all(RCorePluginSession *cps) {
|
||||
|
||||
static void show_task_by_id(RCorePluginSession *cps, int id) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
if (!state || !state->async) {
|
||||
return;
|
||||
}
|
||||
R2AITaskQueue *q = state->async;
|
||||
queue_lock (q);
|
||||
R2AITask *t = find_by_id (q, id);
|
||||
R2AITaskQueue *q = queue (cps);
|
||||
R2AITask *t = lookup (cps, id);
|
||||
if (!t) {
|
||||
queue_unlock (q);
|
||||
r_cons_printf (core->cons, "No task with id %d\n", id);
|
||||
return;
|
||||
}
|
||||
task_lock (t);
|
||||
r_cons_printf (core->cons, "id: %d\n", t->id);
|
||||
r_cons_printf (core->cons, "kind: %s\n", kind_name (t->kind));
|
||||
r_cons_printf (core->cons, "state: %s\n", state_name (t->state));
|
||||
r_cons_printf (core->cons, "title: %s\n", t->title? t->title: "");
|
||||
r_cons_printf (core->cons, "model: %s\n", t->model? t->model: "");
|
||||
r_cons_printf (core->cons, "prov: %s\n", t->provider? t->provider: "");
|
||||
r_cons_printf (core->cons, "steps: %d\n", t->steps);
|
||||
r_cons_printf (core->cons, "age: %ds\n", (int) (time (NULL) - t->created));
|
||||
r_cons_printf (core->cons, "id: %d\nkind: %s\nstate: %s\ntitle: %s\nmodel: %s\nprov: %s\nsteps: %d\nage: %ds\n",
|
||||
t->id, kind_name (t->kind), state_name (t->state), t->title, r_str_get (t->model),
|
||||
r_str_get (t->provider), t->steps, (int) (time (NULL) - t->created));
|
||||
if (t->pending_tool_name) {
|
||||
r_cons_printf (core->cons, "tool: %s %s\n", t->pending_tool_name, t->pending_tool_args? t->pending_tool_args: "");
|
||||
}
|
||||
if (t->error) {
|
||||
r_cons_printf (core->cons, "error: %s\n", t->error);
|
||||
}
|
||||
const char *out = r_strbuf_get (t->output);
|
||||
if (!R_STR_ISEMPTY (out)) {
|
||||
r_cons_printf (core->cons, "output:\n%s", out);
|
||||
if (!r_str_endswith (out, "\n")) {
|
||||
r_cons_newline (core->cons);
|
||||
}
|
||||
}
|
||||
print_block (core->cons, "output:\n", r_strbuf_get (t->output));
|
||||
task_unlock (t);
|
||||
queue_unlock (q);
|
||||
}
|
||||
|
||||
static void show_last_task(RCorePluginSession *cps) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
if (!state || !state->async) {
|
||||
return;
|
||||
}
|
||||
R2AITaskQueue *q = state->async;
|
||||
R2AITaskQueue *q = queue (cps);
|
||||
queue_lock (q);
|
||||
R2AITask *t = r_list_last (q->tasks);
|
||||
int id = t? t->id: 0;
|
||||
@@ -845,21 +769,17 @@ static RCoreHelpMessage help_msg_r2ai_s_kill = {
|
||||
|
||||
static void show_help(RCorePluginSession *cps) {
|
||||
RCore *core = cps->core;
|
||||
r_core_cmd_help (core, help_msg_r2ai_s);
|
||||
r2ai_cmd_help (core, help_msg_r2ai_s);
|
||||
}
|
||||
|
||||
static void purge_finished(RCorePluginSession *cps) {
|
||||
R2AI_State *state = cps->data;
|
||||
if (!state || !state->async) {
|
||||
return;
|
||||
}
|
||||
R2AITaskQueue *q = state->async;
|
||||
R2AITaskQueue *q = queue (cps);
|
||||
queue_lock (q);
|
||||
RListIter *it, *tmp;
|
||||
R2AITask *t;
|
||||
r_list_foreach_safe (q->tasks, it, tmp, t) {
|
||||
task_lock (t);
|
||||
bool drop = t->state == R2AI_TASK_COMPLETE || t->state == R2AI_TASK_ERROR || t->state == R2AI_TASK_CANCELLED;
|
||||
bool drop = is_done (t->state);
|
||||
task_unlock (t);
|
||||
if (drop) {
|
||||
drop_task_locked (q, t);
|
||||
@@ -869,11 +789,7 @@ static void purge_finished(RCorePluginSession *cps) {
|
||||
}
|
||||
|
||||
static void wait_all(RCorePluginSession *cps) {
|
||||
R2AI_State *state = cps->data;
|
||||
if (!state || !state->async) {
|
||||
return;
|
||||
}
|
||||
R2AITaskQueue *q = state->async;
|
||||
R2AITaskQueue *q = queue (cps);
|
||||
for (;;) {
|
||||
bool any = false;
|
||||
queue_lock (q);
|
||||
@@ -933,7 +849,7 @@ R_IPI void r2ai_async_cmd(RCorePluginSession *cps, const char *input) {
|
||||
if (id > 0) {
|
||||
kill_by_id (cps, id);
|
||||
} else {
|
||||
r_core_cmd_help (core, help_msg_r2ai_s_kill);
|
||||
r2ai_cmd_help (core, help_msg_r2ai_s_kill);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
+63
-128
@@ -56,7 +56,7 @@ static void r2ai_print_run_end(RCorePluginSession *cps, const R2AI_Usage *usage,
|
||||
char *total_time_str = format_time_duration (total_time);
|
||||
|
||||
// Print detailed stats
|
||||
r_cons_printf (core->cons, "\x1b[1" Color_BLUE "%s | total: %d in: %d out: %d | run: %d in: %d out: %d | %s / %s" Color_RESET "\n", r_config_get (core->config, "r2ai.model"), state->stats.total_tokens, state->stats.total_prompt_tokens, state->stats.total_completion_tokens, state->stats.run_tokens, state->stats.run_prompt_tokens, state->stats.run_completion_tokens, run_time_str, total_time_str);
|
||||
r_cons_printf (core->cons, Color_BOLD Color_BLUE "%s | total: %d in: %d out: %d | run: %d in: %d out: %d | %s / %s" Color_RESET "\n", r_config_get (core->config, "r2ai.model"), state->stats.total_tokens, state->stats.total_prompt_tokens, state->stats.total_completion_tokens, state->stats.run_tokens, state->stats.run_prompt_tokens, state->stats.run_completion_tokens, run_time_str, total_time_str);
|
||||
r_cons_newline (core->cons);
|
||||
r_cons_flush (core->cons);
|
||||
|
||||
@@ -87,14 +87,13 @@ static void handle_final_response_attempt(RCorePluginSession *cps, RList *messag
|
||||
r_cons_flush (core->cons);
|
||||
}
|
||||
if (final_msg->content) {
|
||||
r_cons_printf (core->cons, "%s", final_msg->content);
|
||||
r_cons_newline (core->cons);
|
||||
r2ai_print_response (core, final_msg->content);
|
||||
r_cons_flush (core->cons);
|
||||
}
|
||||
// Add final response to messages for completeness
|
||||
r2ai_msgs_add (messages, final_msg);
|
||||
free (final_response);
|
||||
}
|
||||
r2ai_chat_response_free (final_response);
|
||||
free (final_system_prompt);
|
||||
} else {
|
||||
r_cons_printf (core->cons, "Auto mode interrupted without final response.\n");
|
||||
@@ -174,7 +173,11 @@ R_API void process_messages(RCorePluginSession *cps, RList *messages, const char
|
||||
if (!system_prompt) {
|
||||
const char *init_commands = r_config_get (core->config, "r2ai.auto.init_commands");
|
||||
if (R_STR_ISNOTEMPTY (init_commands)) {
|
||||
char *tool_args = r_str_newf ("{\"command\":\"%s\"}", init_commands);
|
||||
PJ *pj = pj_new ();
|
||||
pj_o (pj);
|
||||
pj_ks (pj, "command", init_commands);
|
||||
pj_end (pj);
|
||||
char *tool_args = pj_drain (pj);
|
||||
R2AI_ToolResult tool_result = execute_tool (cps, "r2cmd", tool_args);
|
||||
free (tool_args);
|
||||
if (R_STR_ISNOTEMPTY (tool_result.output)) {
|
||||
@@ -198,7 +201,7 @@ R_API void process_messages(RCorePluginSession *cps, RList *messages, const char
|
||||
|
||||
r2ai_stats_init_run (state, n_run);
|
||||
|
||||
r_cons_printf (core->cons, Color_BLUE "About to call r2ai_llmcall with n_run=%d%s\n", n_run, Color_RESET);
|
||||
R_LOG_DEBUG ("About to call r2ai_llmcall with n_run=%d", n_run);
|
||||
r_cons_flush (core->cons);
|
||||
|
||||
// Set up args for r2ai_llmcall call with tools directly
|
||||
@@ -222,7 +225,7 @@ R_API void process_messages(RCorePluginSession *cps, RList *messages, const char
|
||||
|
||||
if (!message) {
|
||||
R_LOG_ERROR ("No message in response");
|
||||
free (response);
|
||||
r2ai_chat_response_free (response);
|
||||
handle_final_response_attempt (cps, messages, effective_prompt, &error);
|
||||
return;
|
||||
}
|
||||
@@ -255,8 +258,7 @@ R_API void process_messages(RCorePluginSession *cps, RList *messages, const char
|
||||
r_cons_flush (core->cons);
|
||||
}
|
||||
if (message->content) {
|
||||
r_cons_printf (core->cons, "%s", message->content);
|
||||
r_cons_newline (core->cons);
|
||||
r2ai_print_response (core, message->content);
|
||||
r_cons_flush (core->cons);
|
||||
}
|
||||
}
|
||||
@@ -321,13 +323,11 @@ R_API void process_messages(RCorePluginSession *cps, RList *messages, const char
|
||||
cmd_output = strdup ("<no output>");
|
||||
}
|
||||
if (strcmp (cmd_output, "R2AI_SIGINT") == 0) {
|
||||
r_cons_printf (core->cons, "\n\n\x1b[1" Color_RED "[r2ai] Processing interrupted after tool execution" Color_RESET "\n\n");
|
||||
r_cons_printf (core->cons, "\n\n" Color_BOLD Color_RED "[r2ai] Processing interrupted after tool execution" Color_RESET "\n\n");
|
||||
r_cons_flush (core->cons);
|
||||
free (cmd_output);
|
||||
cmd_output = strdup ("<user interrupted>");
|
||||
interrupted = true;
|
||||
|
||||
handle_final_response_attempt (cps, messages, effective_prompt, &error);
|
||||
}
|
||||
if (comment) {
|
||||
char *msg = r_str_newf ("HINT: %s\n%s", comment, cmd_output);
|
||||
@@ -339,7 +339,7 @@ R_API void process_messages(RCorePluginSession *cps, RList *messages, const char
|
||||
// Create a tool call response message
|
||||
R2AI_Message tool_response = {
|
||||
.role = "tool",
|
||||
.tool_call_id = strdup (tool_call->id),
|
||||
.tool_call_id = (char *)tool_call->id,
|
||||
.content = cmd_output
|
||||
};
|
||||
|
||||
@@ -363,20 +363,23 @@ R_API void process_messages(RCorePluginSession *cps, RList *messages, const char
|
||||
process_messages (cps, messages, effective_prompt, n_run + 1);
|
||||
} else {
|
||||
R_LOG_DEBUG ("Auto mode loop ending - no more tool calls or interrupted");
|
||||
if (interrupted) {
|
||||
// all tool responses are in place, so the history stays well ordered
|
||||
handle_final_response_attempt (cps, messages, effective_prompt, &error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
r2ai_print_run_end (cps, usage, n_run, max_runs);
|
||||
}
|
||||
|
||||
// Free the response struct itself since r2ai_message_free doesn't do it anymore
|
||||
free (response);
|
||||
// Free the response, including its message and usage
|
||||
r2ai_chat_response_free (response);
|
||||
}
|
||||
|
||||
R_IPI void cmd_r2ai_a(RCorePluginSession *cps, const char *user_query) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
r_cons_printf (core->cons, Color_CYAN "cmd_r2ai_a called with query: %s" Color_RESET "\n", user_query);
|
||||
r_cons_flush (core->cons);
|
||||
R_LOG_DEBUG ("cmd_r2ai_a called with query: %s", user_query);
|
||||
// Get conversation
|
||||
RList *messages = r2ai_conversation_get (state);
|
||||
if (!messages) {
|
||||
@@ -388,6 +391,7 @@ R_IPI void cmd_r2ai_a(RCorePluginSession *cps, const char *user_query) {
|
||||
// If this is the first message in a new conversation, clear previous history
|
||||
if (r_list_empty (messages) || r_config_get_b (core->config, "r2ai.auto.reset_on_query")) {
|
||||
r2ai_msgs_clear (messages);
|
||||
R_FREE (state->cache_prefix);
|
||||
}
|
||||
|
||||
// Add user query
|
||||
@@ -402,47 +406,52 @@ R_IPI void cmd_r2ai_a(RCorePluginSession *cps, const char *user_query) {
|
||||
free (system_prompt);
|
||||
}
|
||||
|
||||
// Helper function to display content with length indication for long content
|
||||
static void print_content_with_length(RCore *core, const char *content, const char *empty_msg) {
|
||||
if (R_STR_ISEMPTY (content)) {
|
||||
r_cons_println (core->cons, empty_msg? empty_msg: "<no content>");
|
||||
} else {
|
||||
r_cons_println (core->cons, content);
|
||||
static char *log_tostring(const RList *messages, bool color) {
|
||||
const char *bold = color? Color_BOLD: "";
|
||||
const char *reset = color? Color_RESET: "";
|
||||
RStrBuf *sb = r_strbuf_new ("");
|
||||
RListIter *iter, *iter2;
|
||||
const R2AI_Message *msg;
|
||||
R2AI_ToolCall *tc;
|
||||
r_list_foreach (messages, iter, msg) {
|
||||
const char *role = msg->role;
|
||||
bool tool = !strcmp (role, "tool");
|
||||
const char *clr = !color? "": !strcmp (role, "user")? Color_GREEN: !strcmp (role, "assistant")? Color_CYAN: tool? Color_MAGENTA: Color_WHITE;
|
||||
const char *text = R_STR_ISNOTEMPTY (msg->content)? msg->content: tool? "<no result>": "<no content>";
|
||||
r_strbuf_appendf (sb, "%s%s[%s]:%s %s\n", bold, clr, role, reset, text);
|
||||
r_list_foreach (msg->tool_calls, iter2, tc) {
|
||||
r_strbuf_appendf (sb, " %s%s[tool call]:%s %s\n", bold, color? Color_MAGENTA: "", reset, tc->name? tc->name: "<unnamed>");
|
||||
if (tc->arguments) {
|
||||
r_strbuf_appendf (sb, " %s\n", tc->arguments);
|
||||
}
|
||||
}
|
||||
if (color) {
|
||||
r_strbuf_append (sb, "\n");
|
||||
}
|
||||
}
|
||||
return r_strbuf_drain (sb);
|
||||
}
|
||||
|
||||
// Add this function right after cmd_r2ai_a
|
||||
R_IPI void cmd_r2ai_logs(RCorePluginSession *cps, const char *flags) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
// Get conversation
|
||||
RList *messages = r2ai_conversation_get (state);
|
||||
if (!messages || r_list_empty (messages)) {
|
||||
RList *messages = r2ai_conversation_get (cps->data);
|
||||
if (r_list_empty (messages)) {
|
||||
r_cons_printf (core->cons, "No conversation history available\n");
|
||||
return;
|
||||
}
|
||||
|
||||
bool json_mode = flags && strchr (flags, 'j');
|
||||
|
||||
if (json_mode) {
|
||||
if (flags && strchr (flags, 'j')) {
|
||||
PJ *pj = pj_new ();
|
||||
if (!pj) {
|
||||
return;
|
||||
}
|
||||
|
||||
pj_a (pj);
|
||||
|
||||
RListIter *iter;
|
||||
RListIter *iter, *iter2;
|
||||
const R2AI_Message *msg;
|
||||
R2AI_ToolCall *tc;
|
||||
r_list_foreach (messages, iter, msg) {
|
||||
pj_o (pj);
|
||||
pj_ks (pj, "role", msg->role? msg->role: "unknown");
|
||||
pj_ks (pj, "content", msg->content? msg->content: "");
|
||||
if (msg->tool_calls && r_list_length (msg->tool_calls) > 0) {
|
||||
pj_ks (pj, "role", r_str_get_fail (msg->role, "unknown"));
|
||||
pj_ks (pj, "content", r_str_get (msg->content));
|
||||
if (!r_list_empty (msg->tool_calls)) {
|
||||
pj_ka (pj, "tool_calls");
|
||||
RListIter *iter;
|
||||
R2AI_ToolCall *tc;
|
||||
r_list_foreach (msg->tool_calls, iter, tc) {
|
||||
r_list_foreach (msg->tool_calls, iter2, tc) {
|
||||
pj_o (pj);
|
||||
if (tc->name) {
|
||||
pj_ks (pj, "name", tc->name);
|
||||
@@ -456,94 +465,17 @@ R_IPI void cmd_r2ai_logs(RCorePluginSession *cps, const char *flags) {
|
||||
}
|
||||
pj_end (pj);
|
||||
}
|
||||
|
||||
pj_end (pj);
|
||||
|
||||
char *json_str = pj_drain (pj);
|
||||
r_cons_printf (core->cons, "%s\n", json_str);
|
||||
r_cons_println (core->cons, json_str);
|
||||
free (json_str);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
r_cons_printf (core->cons, "\x1b[1" Color_BLUE "[r2ai] Chat Logs (%d messages)" Color_RESET "\n", r_list_length (messages));
|
||||
|
||||
r_cons_printf (core->cons, "\x1b[1" Color_YELLOW "Note: System prompt is applied automatically but not stored in history" Color_RESET "\n\n");
|
||||
|
||||
// Display each message in the conversation
|
||||
RListIter *iter;
|
||||
const R2AI_Message *msg;
|
||||
r_list_foreach (messages, iter, msg) {
|
||||
const char *role = msg->role;
|
||||
|
||||
// Format based on role
|
||||
if (!strcmp (role, "user")) {
|
||||
r_cons_printf (core->cons, "\x1b[1" Color_GREEN "[user]:" Color_RESET " ");
|
||||
print_content_with_length (core, msg->content, "<no content>");
|
||||
} else if (!strcmp (role, "assistant")) {
|
||||
r_cons_printf (core->cons, "\x1b[1" Color_CYAN "[assistant]:" Color_RESET " ");
|
||||
print_content_with_length (core, msg->content, "<no content>");
|
||||
// Show tool calls if present
|
||||
if (msg->tool_calls && r_list_length (msg->tool_calls) > 0) {
|
||||
RListIter *iter;
|
||||
R2AI_ToolCall *tc;
|
||||
r_list_foreach (msg->tool_calls, iter, tc) {
|
||||
r_cons_printf (core->cons, " \x1b[1" Color_MAGENTA "[tool call]:" Color_RESET " %s\n", tc->name? tc->name: "<unnamed>");
|
||||
|
||||
if (tc->arguments) {
|
||||
r_cons_printf (core->cons, " %s\n", tc->arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!strcmp (role, "tool")) {
|
||||
r_cons_printf (core->cons, "\x1b[1" Color_MAGENTA "[tool]:" Color_RESET " ");
|
||||
print_content_with_length (core, msg->content, "<no result>");
|
||||
|
||||
// Don't show the tool call ID as requested
|
||||
} else {
|
||||
// Other roles (system, etc.)
|
||||
r_cons_printf (core->cons, "\x1b[1" Color_WHITE "[%s]:" Color_RESET " ", role);
|
||||
print_content_with_length (core, msg->content, "<no content>");
|
||||
}
|
||||
|
||||
r_cons_newline (core->cons);
|
||||
r_cons_flush (core->cons);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to format conversation log as string
|
||||
static char *format_conversation_log(RList *messages) {
|
||||
RStrBuf *sb = r_strbuf_new ("");
|
||||
r_strbuf_append (sb, "Conversation Log:\n");
|
||||
|
||||
RListIter *iter;
|
||||
const R2AI_Message *msg;
|
||||
r_list_foreach (messages, iter, msg) {
|
||||
const char *role = msg->role;
|
||||
|
||||
if (!strcmp (role, "user")) {
|
||||
r_strbuf_appendf (sb, "[user]: %s\n", msg->content? msg->content: "<no content>");
|
||||
} else if (!strcmp (role, "assistant")) {
|
||||
r_strbuf_appendf (sb, "[assistant]: %s\n", msg->content? msg->content: "<no content>");
|
||||
// Include tool calls if present
|
||||
if (msg->tool_calls && r_list_length (msg->tool_calls) > 0) {
|
||||
RListIter *iter_tc;
|
||||
R2AI_ToolCall *tc;
|
||||
r_list_foreach (msg->tool_calls, iter_tc, tc) {
|
||||
r_strbuf_appendf (sb, " [tool call]: %s\n", tc->name? tc->name: "<unnamed>");
|
||||
if (tc->arguments) {
|
||||
r_strbuf_appendf (sb, " %s\n", tc->arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!strcmp (role, "tool")) {
|
||||
r_strbuf_appendf (sb, "[tool]: %s\n", msg->content? msg->content: "<no result>");
|
||||
} else {
|
||||
r_strbuf_appendf (sb, "[%s]: %s\n", role, msg->content? msg->content: "<no content>");
|
||||
}
|
||||
}
|
||||
|
||||
return r_strbuf_drain (sb);
|
||||
char *log = log_tostring (messages, true);
|
||||
r_cons_printf (core->cons, Color_BOLD Color_BLUE "[r2ai] Chat Logs (%d messages)" Color_RESET "\n"
|
||||
Color_BOLD Color_YELLOW "Note: System prompt is applied automatically but not stored in history" Color_RESET "\n\n%s",
|
||||
r_list_length (messages), log);
|
||||
free (log);
|
||||
}
|
||||
|
||||
// Helper function to process conversation with LLM and handle result
|
||||
@@ -558,7 +490,9 @@ static void process_conversation_with_llm(RCorePluginSession *cps, bool compact)
|
||||
}
|
||||
|
||||
// Format conversation as string
|
||||
char *log_str = format_conversation_log (messages);
|
||||
char *log = log_tostring (messages, false);
|
||||
char *log_str = r_str_newf ("Conversation Log:\n%s", log);
|
||||
free (log);
|
||||
|
||||
// Load the compact prompt from file
|
||||
char *prompt_text = r2ai_load_prompt_text (core, "compact");
|
||||
@@ -590,6 +524,7 @@ static void process_conversation_with_llm(RCorePluginSession *cps, bool compact)
|
||||
if (compact) {
|
||||
// Clear the conversation and add summary as system message
|
||||
r2ai_msgs_clear (messages);
|
||||
R_FREE (state->cache_prefix);
|
||||
R2AI_Message summary_msg = {
|
||||
.role = "system",
|
||||
.content = res
|
||||
|
||||
+1
-1
@@ -144,7 +144,7 @@ R_API void r2ai_claw_edit(RCorePluginSession *cps) {
|
||||
for (i = 0; i < CLAW_NFILES; i++) {
|
||||
char *path = r_file_home (CLAW_FILES[i].rel);
|
||||
if (path && r_file_exists (path)) {
|
||||
r_cons_editor (cps->core->cons, path, NULL);
|
||||
r2ai_cons_editor (cps->core->cons, path, NULL);
|
||||
}
|
||||
free (path);
|
||||
}
|
||||
|
||||
+77
-194
@@ -1,219 +1,102 @@
|
||||
/* r2ai - Copyright 2023-2026 pancake, dnakov */
|
||||
|
||||
#include "r2ai.h"
|
||||
#include "r2ai_priv.h"
|
||||
|
||||
/* Gemini API implementation */
|
||||
static void text_parts(PJ *pj, const char *text) {
|
||||
pj_ka (pj, "parts");
|
||||
pj_o (pj);
|
||||
pj_ks (pj, "text", text);
|
||||
pj_end (pj);
|
||||
pj_end (pj);
|
||||
}
|
||||
|
||||
R_IPI R2AI_ChatResponse *r2ai_gemini(RCorePluginSession *cps, R2AIArgs args) {
|
||||
RCore *core = cps->core;
|
||||
const char *model = R_STR_ISNOTEMPTY (args.model)
|
||||
? args.model
|
||||
: r_config_get (core->config, "r2ai.model");
|
||||
const char *base_url = r2ai_get_provider_url (core, args.provider);
|
||||
const char *model_name = model && strstr (model, "gemini")? model: "gemini-2.0-flash-exp";
|
||||
char **error = args.error;
|
||||
|
||||
// Create a temp conversation to include the system prompt and the rest of the messages
|
||||
RList *temp_msgs = r2ai_msgs_new ();
|
||||
if (!temp_msgs) {
|
||||
if (error) {
|
||||
*error = strdup ("Failed to create temporary messages array");
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get system prompt
|
||||
const char *system_prompt = NULL;
|
||||
if (R_STR_ISNOTEMPTY (args.system_prompt)) {
|
||||
system_prompt = args.system_prompt;
|
||||
} else {
|
||||
system_prompt = r_config_get (core->config, "r2ai.system");
|
||||
}
|
||||
|
||||
if (args.messages) {
|
||||
RListIter *iter;
|
||||
R2AI_Message *msg;
|
||||
r_list_foreach (args.messages, iter, msg) {
|
||||
r2ai_msgs_add (temp_msgs, msg);
|
||||
}
|
||||
} else {
|
||||
R_LOG_WARN ("No messages");
|
||||
}
|
||||
|
||||
if (error) {
|
||||
*error = NULL;
|
||||
}
|
||||
|
||||
// Setup headers for Gemini API
|
||||
const char **headers = NULL;
|
||||
char *auth_header = NULL;
|
||||
char *api_key_header = NULL;
|
||||
|
||||
if (R_STR_ISNOTEMPTY (args.api_key)) {
|
||||
static const char *static_headers[] = { NULL, NULL, NULL };
|
||||
headers = static_headers;
|
||||
headers[0] = "Content-Type: application/json";
|
||||
|
||||
// Check if it's an OAuth token (starts with "ya29.")
|
||||
if (r_str_startswith (args.api_key, "ya29.")) {
|
||||
auth_header = r_str_newf ("Authorization: Bearer %s", args.api_key);
|
||||
headers[1] = auth_header;
|
||||
} else {
|
||||
api_key_header = r_str_newf ("x-goog-api-key: %s", args.api_key);
|
||||
headers[1] = api_key_header;
|
||||
}
|
||||
}
|
||||
|
||||
// Build Gemini API URL
|
||||
char *gemini_url = r_str_newf ("%s/models/%s:generateContent?key=%s",
|
||||
base_url,
|
||||
model_name,
|
||||
args.api_key);
|
||||
|
||||
// Create Gemini-style request JSON
|
||||
// request body shared by the gemini and vertex-gemini providers
|
||||
R_IPI char *r2ai_gemini_request(const R2AIArgs *args) {
|
||||
PJ *pj = pj_new ();
|
||||
pj_o (pj);
|
||||
|
||||
// Build contents array
|
||||
if (R_STR_ISNOTEMPTY (args->system_prompt)) {
|
||||
pj_ko (pj, "systemInstruction");
|
||||
text_parts (pj, args->system_prompt);
|
||||
pj_end (pj);
|
||||
}
|
||||
pj_ka (pj, "contents");
|
||||
RListIter *iter;
|
||||
R2AI_Message *msg;
|
||||
int msg_index = 0;
|
||||
r_list_foreach (temp_msgs, iter, msg) {
|
||||
r_list_foreach (args->messages, iter, msg) {
|
||||
pj_o (pj);
|
||||
// Gemini uses "model" for assistant, "user" for user
|
||||
const char *role = "user";
|
||||
if (strcmp (msg->role, "assistant") == 0) {
|
||||
role = "model";
|
||||
}
|
||||
pj_ks (pj, "role", role);
|
||||
pj_ka (pj, "parts");
|
||||
pj_o (pj);
|
||||
// Prepend system prompt to first user message
|
||||
char *content = NULL;
|
||||
if (system_prompt && strcmp (msg->role, "user") == 0 && msg_index == 0) {
|
||||
content = r_str_newf ("System: %s\n\n%s", system_prompt, msg->content? msg->content: "");
|
||||
} else {
|
||||
content = msg->content? strdup (msg->content): strdup ("");
|
||||
}
|
||||
pj_ks (pj, "text", content);
|
||||
free (content);
|
||||
pj_end (pj); // end parts object
|
||||
pj_end (pj); // end parts array
|
||||
pj_end (pj); // end content object
|
||||
msg_index++;
|
||||
pj_ks (pj, "role", strcmp (msg->role, "assistant")? "user": "model");
|
||||
text_parts (pj, r_str_get (msg->content));
|
||||
pj_end (pj);
|
||||
}
|
||||
pj_end (pj); // end contents array
|
||||
|
||||
// Add generation config
|
||||
pj_end (pj);
|
||||
pj_ko (pj, "generationConfig");
|
||||
if (args.max_tokens > 0) {
|
||||
pj_kn (pj, "maxOutputTokens", args.max_tokens);
|
||||
if (args->max_tokens > 0) {
|
||||
pj_kn (pj, "maxOutputTokens", args->max_tokens);
|
||||
}
|
||||
if (args.temperature > 0) {
|
||||
pj_kd (pj, "temperature", args.temperature);
|
||||
if (args->deterministic) {
|
||||
pj_kn (pj, "temperature", 0);
|
||||
pj_kn (pj, "topP", 1);
|
||||
pj_kn (pj, "topK", 1);
|
||||
} else if (args->temperature > 0) {
|
||||
pj_kd (pj, "temperature", args->temperature);
|
||||
}
|
||||
pj_end (pj); // end generationConfig
|
||||
pj_end (pj);
|
||||
pj_end (pj);
|
||||
return pj_drain (pj);
|
||||
}
|
||||
|
||||
pj_end (pj); // end root object
|
||||
|
||||
char *request_json = pj_drain (pj);
|
||||
|
||||
// Debug: save request
|
||||
char *tmpdir = r_file_tmpdir ();
|
||||
char *req_path = r_str_newf ("%s" R_SYS_DIR "r2ai_gemini_request.json", tmpdir);
|
||||
r_file_dump (req_path, (const ut8 *)request_json, strlen (request_json), 0);
|
||||
free (req_path);
|
||||
free (tmpdir);
|
||||
|
||||
// Make the API call
|
||||
char *response = NULL;
|
||||
int code = 0;
|
||||
response = r2ai_http_post (core, gemini_url, headers, request_json, &code, NULL);
|
||||
|
||||
free (request_json);
|
||||
free (gemini_url);
|
||||
free (auth_header);
|
||||
free (api_key_header);
|
||||
|
||||
if (code != 200) {
|
||||
R_LOG_ERROR ("Gemini API error %d", code);
|
||||
if (response) {
|
||||
R_LOG_ERROR ("Gemini API error response: %s", response);
|
||||
}
|
||||
free (response);
|
||||
r2ai_msgs_free (temp_msgs);
|
||||
// parses (and modifies) the json response of the gemini and vertex-gemini providers
|
||||
R_IPI R2AI_ChatResponse *r2ai_gemini_parse(char *json, char **error) {
|
||||
RJson *j = r_json_parse (json);
|
||||
if (!j) {
|
||||
*error = strdup ("Failed to parse Gemini response JSON");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Debug: save response
|
||||
tmpdir = r_file_tmpdir ();
|
||||
char *res_path = r_str_newf ("%s" R_SYS_DIR "r2ai_gemini_response.json", tmpdir);
|
||||
r_file_dump (res_path, (const ut8 *)response, strlen (response), 0);
|
||||
free (res_path);
|
||||
free (tmpdir);
|
||||
|
||||
// Parse Gemini response
|
||||
RJson *jres = r_json_parse (response);
|
||||
if (!jres) {
|
||||
if (error) {
|
||||
*error = strdup ("Failed to parse Gemini response JSON");
|
||||
}
|
||||
free (response);
|
||||
r2ai_msgs_free (temp_msgs);
|
||||
const RJson *c = r_json_get (j, "candidates");
|
||||
c = c? r_json_item (c, 0): NULL;
|
||||
c = c? r_json_get (c, "content"): NULL;
|
||||
c = c? r_json_get (c, "parts"): NULL;
|
||||
c = c? r_json_item (c, 0): NULL;
|
||||
const char *text = c? r_json_get_str (c, "text"): NULL;
|
||||
if (!text) {
|
||||
r_json_free (j);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Extract response content
|
||||
const RJson *candidates = r_json_get (jres, "candidates");
|
||||
if (!candidates || candidates->type != R_JSON_ARRAY || candidates->children.count == 0) {
|
||||
r_json_free (jres);
|
||||
free (response);
|
||||
r2ai_msgs_free (temp_msgs);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const RJson *candidate = r_json_item (candidates, 0);
|
||||
const RJson *content = r_json_get (candidate, "content");
|
||||
const RJson *parts = r_json_get (content, "parts");
|
||||
|
||||
if (!parts || parts->type != R_JSON_ARRAY || parts->children.count == 0) {
|
||||
r_json_free (jres);
|
||||
free (response);
|
||||
r2ai_msgs_free (temp_msgs);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const RJson *part = r_json_item (parts, 0);
|
||||
const RJson *text = r_json_get (part, "text");
|
||||
|
||||
char *response_text = NULL;
|
||||
if (text && text->type == R_JSON_STRING) {
|
||||
response_text = strdup (text->str_value);
|
||||
}
|
||||
|
||||
r_json_free (jres);
|
||||
free (response);
|
||||
r2ai_msgs_free (temp_msgs);
|
||||
|
||||
if (!response_text) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Create response
|
||||
R2AI_Message *message = R_NEW0 (R2AI_Message);
|
||||
R2AI_Usage *usage = R_NEW0 (R2AI_Usage);
|
||||
|
||||
if (message) {
|
||||
message->role = strdup ("assistant");
|
||||
message->content = response_text;
|
||||
}
|
||||
|
||||
R2AI_ChatResponse *result = R_NEW0 (R2AI_ChatResponse);
|
||||
if (result) {
|
||||
result->message = message;
|
||||
result->usage = usage;
|
||||
const RJson *u = r_json_get (j, "usageMetadata");
|
||||
if (u) {
|
||||
usage->prompt_tokens = r_json_get_num (u, "promptTokenCount");
|
||||
usage->completion_tokens = r_json_get_num (u, "candidatesTokenCount");
|
||||
usage->total_tokens = r_json_get_num (u, "totalTokenCount");
|
||||
}
|
||||
R2AI_Message *message = R_NEW0 (R2AI_Message);
|
||||
message->role = strdup ("assistant");
|
||||
message->content = strdup (text);
|
||||
r_json_free (j);
|
||||
R2AI_ChatResponse *res = R_NEW0 (R2AI_ChatResponse);
|
||||
res->message = message;
|
||||
res->usage = usage;
|
||||
return res;
|
||||
}
|
||||
|
||||
R_IPI R2AI_ChatResponse *r2ai_gemini(RCorePluginSession *cps, R2AIArgs args) {
|
||||
RCore *core = cps->core;
|
||||
const char *model = strstr (args.model, "gemini")? args.model: "gemini-2.0-flash-exp";
|
||||
char *base_url = r2ai_get_provider_url (core, args.provider);
|
||||
char *url = r_str_newf ("%s/models/%s:generateContent", base_url, model);
|
||||
free (base_url);
|
||||
// OAuth tokens start with "ya29.", the key travels in the headers and never in the url
|
||||
const bool oauth = r_str_startswith (args.api_key, "ya29.");
|
||||
char *auth = r_str_newf (oauth? "Authorization: Bearer %s": "x-goog-api-key: %s", args.api_key);
|
||||
const char *headers[] = { "Content-Type: application/json", auth, NULL };
|
||||
char *data = r2ai_gemini_request (&args);
|
||||
int code = 0;
|
||||
char *res = r2ai_post (core, "gemini", url, headers, data, &code, args.error);
|
||||
free (data);
|
||||
free (url);
|
||||
free (auth);
|
||||
R2AI_ChatResponse *result = res? r2ai_gemini_parse (res, args.error): NULL;
|
||||
free (res);
|
||||
return result;
|
||||
}
|
||||
+42
-4
@@ -4,6 +4,7 @@
|
||||
// #define _POSIX_C_SOURCE 200809L
|
||||
#include <signal.h>
|
||||
#include "r2ai.h"
|
||||
#include "r2ai_priv.h"
|
||||
|
||||
// Global flag for tracking interrupt status
|
||||
static volatile sig_atomic_t r2ai_http_interrupted = 0;
|
||||
@@ -28,7 +29,7 @@ static R2AI_HttpConfig get_http_config(RCore *core) {
|
||||
return config;
|
||||
}
|
||||
|
||||
#if R2__UNIX__
|
||||
#if R2__UNIX__ && !__wasi__
|
||||
// Signal handler for timeout (SIGALRM)
|
||||
static void r2ai_http_sigint_handler(int sig) {
|
||||
if (sig == SIGALRM) {
|
||||
@@ -65,7 +66,7 @@ static HttpResponse r2ai_http_request_with_retry(HttpRequestFunc func, const HTT
|
||||
// Reset interrupt flag
|
||||
r2ai_http_interrupted = 0;
|
||||
|
||||
#if R2__UNIX__
|
||||
#if R2__UNIX__ && !__wasi__
|
||||
// Ignore SIGWINCH to prevent window resize from interrupting child processes
|
||||
struct sigaction old_winch_act;
|
||||
sigaction (SIGWINCH, &(struct sigaction){ .sa_handler = SIG_IGN }, &old_winch_act);
|
||||
@@ -128,7 +129,7 @@ static HttpResponse r2ai_http_request_with_retry(HttpRequestFunc func, const HTT
|
||||
r_cons_break_pop (core->cons);
|
||||
}
|
||||
|
||||
#if R2__UNIX__
|
||||
#if R2__UNIX__ && !__wasi__
|
||||
// Restore SIGWINCH handler
|
||||
sigaction (SIGWINCH, &old_winch_act, NULL);
|
||||
#endif
|
||||
@@ -144,7 +145,9 @@ static HttpResponse r2ai_http_request_with_retry(HttpRequestFunc func, const HTT
|
||||
static HttpRequestFunc select_backend(const char *backend, bool is_post) {
|
||||
HttpRequestFunc func = NULL;
|
||||
if (!strcmp (backend, "auto")) {
|
||||
#if R2__WINDOWS__
|
||||
#if __wasi__
|
||||
backend = "socket";
|
||||
#elif R2__WINDOWS__
|
||||
// TODO: we should use pwsh or r2 by default, but for not it seems the only backend that works on Windows is r2curl+system
|
||||
r_sys_setenv ("R2_CURL", "1");
|
||||
backend = "system";
|
||||
@@ -278,3 +281,38 @@ R_API char *r2ai_http_get(RCore *core, const char *url, const char **headers, in
|
||||
}
|
||||
return response.body;
|
||||
}
|
||||
|
||||
static void dump_payload(const char *name, const char *kind, const char *data) {
|
||||
char *tmpdir = r_file_tmpdir ();
|
||||
char *path = r_str_newf ("%s" R_SYS_DIR "r2ai_%s_%s.json", tmpdir, name, kind);
|
||||
r_file_dump (path, (const ut8 *)data, -1, false);
|
||||
R_LOG_DEBUG ("%s %s saved to %s", name, kind, path);
|
||||
free (path);
|
||||
free (tmpdir);
|
||||
}
|
||||
|
||||
// post a provider request, returns the body on success or NULL with the reason in error
|
||||
R_IPI char *r2ai_post(RCore *core, const char *name, const char *url, const char **headers, const char *data, int *code, char **error) {
|
||||
dump_payload (name, "request", data);
|
||||
if (r_config_get_b (core->config, "r2ai.debug")) {
|
||||
RStrBuf *sb = r_strbuf_new ("curl -X POST");
|
||||
int i;
|
||||
for (i = 0; headers[i]; i++) {
|
||||
r_strbuf_appendf (sb, " -H \"%s\"", headers[i]);
|
||||
}
|
||||
r_strbuf_appendf (sb, " -d '%s' \"%s\"", data, url);
|
||||
eprintf ("Curl command: %s\n", r_strbuf_get (sb));
|
||||
r_strbuf_free (sb);
|
||||
}
|
||||
char *res = r2ai_http_post (core, url, headers, data, code, NULL);
|
||||
if (res && *code == 200) {
|
||||
dump_payload (name, "response", res);
|
||||
return res;
|
||||
}
|
||||
if (error) {
|
||||
free (*error);
|
||||
*error = r_str_newf ("%s API error %d%s%s", name, *code, res? ": ": "", r_str_get (res));
|
||||
}
|
||||
free (res);
|
||||
return NULL;
|
||||
}
|
||||
+15
-4
@@ -30,8 +30,7 @@ static char *escape_cmd_double_quotes(const char *str) {
|
||||
return escaped;
|
||||
}
|
||||
|
||||
static HttpResponse build_and_execute_curl(const char *cmd_start, const HTTPRequest *request, const char *input_data) {
|
||||
HttpResponse error = { .code = -1 };
|
||||
static HttpResponse build_and_execute_curl(const char *cmd_start, const HTTPRequest *request, const char *input_data) { HttpResponse error = { .code = -1 };
|
||||
int timeout = request->config.timeout;
|
||||
if (!request->url) {
|
||||
return error;
|
||||
@@ -39,6 +38,9 @@ static HttpResponse build_and_execute_curl(const char *cmd_start, const HTTPRequ
|
||||
|
||||
// Compose curl command
|
||||
RStrBuf *cmd = r_strbuf_new (cmd_start);
|
||||
if (!cmd) {
|
||||
return error;
|
||||
}
|
||||
|
||||
// Add timeout
|
||||
r_strbuf_appendf (cmd, " --connect-timeout %d --max-time %d", 10, timeout);
|
||||
@@ -47,14 +49,23 @@ static HttpResponse build_and_execute_curl(const char *cmd_start, const HTTPRequ
|
||||
if (request->headers) {
|
||||
for (int i = 0; request->headers[i] != NULL; i++) {
|
||||
char *escaped_header = escape_cmd_double_quotes (request->headers[i]);
|
||||
r_strbuf_appendf (cmd, " -H \"%s\"", escaped_header);
|
||||
free (escaped_header);
|
||||
if (escaped_header) {
|
||||
r_strbuf_appendf (cmd, " -H \"%s\"", escaped_header);
|
||||
free (escaped_header);
|
||||
}
|
||||
}
|
||||
}
|
||||
char *escaped_url = escape_cmd_double_quotes (request->url);
|
||||
if (!escaped_url) {
|
||||
r_strbuf_free (cmd);
|
||||
return error;
|
||||
}
|
||||
r_strbuf_appendf (cmd, " -w \"\\n%%{http_code}\" \"%s\"", escaped_url);
|
||||
free (escaped_url);
|
||||
char *cmd_str = r_strbuf_drain (cmd);
|
||||
if (!cmd_str) {
|
||||
return error;
|
||||
}
|
||||
R_LOG_DEBUG ("Running system curl: %s", cmd_str);
|
||||
char *response = r_sys_cmd_str (cmd_str, input_data, NULL);
|
||||
|
||||
|
||||
+15
-31
@@ -36,8 +36,7 @@ static int progress_callback(void *clientp, curl_off_t dltotal, curl_off_t dlnow
|
||||
return r2ai_http_interrupted? 1: 0;
|
||||
}
|
||||
|
||||
static HttpResponse curl_http_request(const HTTPRequest *request, bool is_post) {
|
||||
HttpResponse error = { .code = -1 };
|
||||
static HttpResponse curl_http_request(const HTTPRequest *request, bool is_post) { HttpResponse error = { .code = -1 };
|
||||
if (!request->url || (is_post && !request->data)) {
|
||||
return error;
|
||||
}
|
||||
@@ -46,14 +45,13 @@ static HttpResponse curl_http_request(const HTTPRequest *request, bool is_post)
|
||||
CURLcode res;
|
||||
struct curl_slist *curl_headers = NULL;
|
||||
CurlResponse response = { 0 };
|
||||
HttpResponse result = { .code = -1 };
|
||||
|
||||
// Initialize response
|
||||
response.data = malloc (1);
|
||||
if (!response.data) {
|
||||
return error;
|
||||
}
|
||||
response.data[0] = '\0';
|
||||
response.size = 0;
|
||||
|
||||
curl = curl_easy_init ();
|
||||
if (!curl) {
|
||||
@@ -61,10 +59,8 @@ static HttpResponse curl_http_request(const HTTPRequest *request, bool is_post)
|
||||
return error;
|
||||
}
|
||||
|
||||
// Set URL
|
||||
curl_easy_setopt (curl, CURLOPT_URL, request->url);
|
||||
|
||||
// Set method-specific options
|
||||
if (is_post) {
|
||||
curl_easy_setopt (curl, CURLOPT_POST, 1L);
|
||||
curl_easy_setopt (curl, CURLOPT_POSTFIELDS, request->data);
|
||||
@@ -73,7 +69,6 @@ static HttpResponse curl_http_request(const HTTPRequest *request, bool is_post)
|
||||
curl_easy_setopt (curl, CURLOPT_HTTPGET, 1L);
|
||||
}
|
||||
|
||||
// Set headers if provided
|
||||
if (request->headers) {
|
||||
for (int i = 0; request->headers[i] != NULL; i++) {
|
||||
curl_headers = curl_slist_append (curl_headers, request->headers[i]);
|
||||
@@ -81,53 +76,42 @@ static HttpResponse curl_http_request(const HTTPRequest *request, bool is_post)
|
||||
curl_easy_setopt (curl, CURLOPT_HTTPHEADER, curl_headers);
|
||||
}
|
||||
|
||||
// Set write callback
|
||||
curl_easy_setopt (curl, CURLOPT_WRITEFUNCTION, write_callback);
|
||||
curl_easy_setopt (curl, CURLOPT_WRITEDATA, (void *)&response);
|
||||
|
||||
// Set progress callback to handle interrupts
|
||||
curl_easy_setopt (curl, CURLOPT_NOPROGRESS, 0L);
|
||||
curl_easy_setopt (curl, CURLOPT_XFERINFOFUNCTION, progress_callback);
|
||||
curl_easy_setopt (curl, CURLOPT_XFERINFODATA, NULL);
|
||||
|
||||
// Set timeout options
|
||||
curl_easy_setopt (curl, CURLOPT_CONNECTTIMEOUT, 10L); // 10 seconds connect timeout
|
||||
curl_easy_setopt (curl, CURLOPT_TIMEOUT, (long)request->config.timeout); // Use configured timeout
|
||||
|
||||
// Follow redirects
|
||||
curl_easy_setopt (curl, CURLOPT_CONNECTTIMEOUT, 10L);
|
||||
curl_easy_setopt (curl, CURLOPT_TIMEOUT, (long)request->config.timeout);
|
||||
curl_easy_setopt (curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
|
||||
// Perform the request
|
||||
res = curl_easy_perform (curl);
|
||||
|
||||
// Check for interruption
|
||||
if (r2ai_http_interrupted) {
|
||||
R_LOG_DEBUG ("HTTP request was interrupted by user");
|
||||
free (response.data);
|
||||
curl_slist_free_all (curl_headers);
|
||||
curl_easy_cleanup (curl);
|
||||
return error;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
// Get response code
|
||||
long http_code;
|
||||
long http_code = 0;
|
||||
curl_easy_getinfo (curl, CURLINFO_RESPONSE_CODE, &http_code);
|
||||
|
||||
// Check for errors
|
||||
if (res != CURLE_OK) {
|
||||
R_LOG_ERROR ("curl_easy_perform () failed: %s", curl_easy_strerror (res));
|
||||
free (response.data);
|
||||
curl_slist_free_all (curl_headers);
|
||||
curl_easy_cleanup (curl);
|
||||
return error;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
// If we get here, the request was successful
|
||||
// Cleanup
|
||||
result.body = response.data;
|
||||
result.code = (int)http_code;
|
||||
result.length = (int)response.size;
|
||||
response.data = NULL;
|
||||
|
||||
cleanup:
|
||||
free (response.data);
|
||||
curl_slist_free_all (curl_headers);
|
||||
curl_easy_cleanup (curl);
|
||||
|
||||
return (HttpResponse){ .body = response.data, .code = (int)http_code, .length = response.size };
|
||||
return result;
|
||||
}
|
||||
|
||||
HttpResponse curl_http_get(const HTTPRequest *request) {
|
||||
|
||||
+80
-38
@@ -33,64 +33,92 @@ static char *escape_single_quotes(const char *str) {
|
||||
}
|
||||
|
||||
// Helper to append headers to PowerShell command
|
||||
static void append_headers_to_cmd(RStrBuf *cmd, const char **headers) {
|
||||
r_strbuf_appendf (cmd, "$headers=@{");
|
||||
if (headers) {
|
||||
for (int i = 0; headers[i]; i++) {
|
||||
char *header = strdup (headers[i]);
|
||||
char *colon = strchr (header, ':');
|
||||
if (colon) {
|
||||
*colon = '\0';
|
||||
char *key = r_str_trim_dup (header);
|
||||
char *value = r_str_trim_dup (colon + 1);
|
||||
char *escaped_key = escape_single_quotes (key);
|
||||
char *escaped_value = escape_single_quotes (value);
|
||||
r_strbuf_appendf (cmd, "'%s'='%s';", escaped_key, escaped_value);
|
||||
free (escaped_key);
|
||||
free (escaped_value);
|
||||
free (key);
|
||||
free (value);
|
||||
}
|
||||
free (header);
|
||||
static void append_headers_to_cmd(RStrBuf *cmd, const char **headers) { r_strbuf_appendf (cmd, "$headers=@{");
|
||||
if (!headers) {
|
||||
r_strbuf_appendf (cmd, "};");
|
||||
return;
|
||||
}
|
||||
for (int i = 0; headers[i]; i++) {
|
||||
char *header = strdup (headers[i]);
|
||||
if (!header) {
|
||||
continue;
|
||||
}
|
||||
char *colon = strchr (header, ':');
|
||||
if (!colon) {
|
||||
free (header);
|
||||
continue;
|
||||
}
|
||||
*colon = '\0';
|
||||
char *key = r_str_trim_dup (header);
|
||||
char *value = r_str_trim_dup (colon + 1);
|
||||
char *escaped_key = key ? escape_single_quotes (key) : NULL;
|
||||
char *escaped_value = value ? escape_single_quotes (value) : NULL;
|
||||
if (escaped_key && escaped_value) {
|
||||
r_strbuf_appendf (cmd, "'%s'='%s';", escaped_key, escaped_value);
|
||||
}
|
||||
free (escaped_key);
|
||||
free (escaped_value);
|
||||
free (key);
|
||||
free (value);
|
||||
free (header);
|
||||
}
|
||||
r_strbuf_appendf (cmd, "};");
|
||||
}
|
||||
|
||||
// Helper to parse PowerShell response
|
||||
static HttpResponse parse_powershell_response(char *full_response) {
|
||||
if (full_response) {
|
||||
char *newline = strchr (full_response, '\n');
|
||||
if (newline) {
|
||||
*newline = '\0';
|
||||
int code = atoi (full_response);
|
||||
char *body = newline + 1;
|
||||
char *body_copy = strdup (body);
|
||||
size_t length = strlen (body);
|
||||
free (full_response);
|
||||
return (HttpResponse){ .body = body_copy, .code = code, .length = length };
|
||||
} else {
|
||||
return (HttpResponse){ .body = full_response, .code = 200, .length = strlen (full_response) };
|
||||
}
|
||||
static HttpResponse parse_powershell_response(char *full_response) { if (!full_response) {
|
||||
return (HttpResponse){ .code = -1 };
|
||||
}
|
||||
return (HttpResponse){ .code = -1 };
|
||||
char *newline = strchr (full_response, '\n');
|
||||
if (newline) {
|
||||
*newline = '\0';
|
||||
int code = atoi (full_response);
|
||||
char *body = newline + 1;
|
||||
char *body_copy = strdup (body);
|
||||
if (!body_copy) {
|
||||
free (full_response);
|
||||
return (HttpResponse){ .code = -1 };
|
||||
}
|
||||
size_t length = strlen (body);
|
||||
free (full_response);
|
||||
return (HttpResponse){ .body = body_copy, .code = code, .length = length };
|
||||
}
|
||||
return (HttpResponse){ .body = full_response, .code = 200, .length = strlen (full_response) };
|
||||
}
|
||||
/**
|
||||
* Windows-specific HTTP POST using PowerShell
|
||||
*/
|
||||
HttpResponse windows_http_post(const HTTPRequest *request) {
|
||||
const char *url = request->url;
|
||||
HttpResponse windows_http_post(const HTTPRequest *request) { const char *url = request->url;
|
||||
const char **headers = request->headers;
|
||||
const char *data = request->data;
|
||||
int timeout = request->config.timeout;
|
||||
if (!url || !data) {
|
||||
return (HttpResponse){ .code = -1 };
|
||||
}
|
||||
char *escaped_url = escape_single_quotes (url);
|
||||
char *escaped_data = escape_single_quotes (data);
|
||||
if (!escaped_url || !escaped_data) {
|
||||
free (escaped_url);
|
||||
free (escaped_data);
|
||||
return (HttpResponse){ .code = -1 };
|
||||
}
|
||||
RStrBuf *cmd = r_strbuf_new ("powershell -Command \"");
|
||||
if (!cmd) {
|
||||
free (escaped_url);
|
||||
free (escaped_data);
|
||||
return (HttpResponse){ .code = -1 };
|
||||
}
|
||||
append_headers_to_cmd (cmd, headers);
|
||||
r_strbuf_appendf (cmd, "$body='%s';", escaped_data);
|
||||
r_strbuf_appendf (cmd, "try{$r=Invoke-WebRequest -Method Post -Uri '%s' -Headers $headers -Body $body -TimeoutSec %d;", escaped_url, timeout);
|
||||
r_strbuf_appendf (cmd, "Write-Host $r.StatusCode;$r.Content}catch{Write-Host 0;$_.Exception.Message}\"");
|
||||
free (escaped_url);
|
||||
free (escaped_data);
|
||||
char *cmd_str = r_strbuf_drain (cmd);
|
||||
if (!cmd_str) {
|
||||
r_strbuf_free (cmd);
|
||||
return (HttpResponse){ .code = -1 };
|
||||
}
|
||||
R_LOG_DEBUG ("Running PowerShell: %s", cmd_str);
|
||||
char *full_response = r_sys_cmd_str (cmd_str, NULL, NULL);
|
||||
free (cmd_str);
|
||||
@@ -100,16 +128,30 @@ HttpResponse windows_http_post(const HTTPRequest *request) {
|
||||
/**
|
||||
* Windows-specific HTTP GET using PowerShell
|
||||
*/
|
||||
HttpResponse windows_http_get(const HTTPRequest *request) {
|
||||
const char *url = request->url;
|
||||
HttpResponse windows_http_get(const HTTPRequest *request) { const char *url = request->url;
|
||||
const char **headers = request->headers;
|
||||
int timeout = request->config.timeout;
|
||||
if (!url) {
|
||||
return (HttpResponse){ .code = -1 };
|
||||
}
|
||||
char *escaped_url = escape_single_quotes (url);
|
||||
if (!escaped_url) {
|
||||
return (HttpResponse){ .code = -1 };
|
||||
}
|
||||
RStrBuf *cmd = r_strbuf_new ("powershell -Command \"");
|
||||
if (!cmd) {
|
||||
free (escaped_url);
|
||||
return (HttpResponse){ .code = -1 };
|
||||
}
|
||||
append_headers_to_cmd (cmd, headers);
|
||||
r_strbuf_appendf (cmd, "try{$r=Invoke-WebRequest -Method Get -Uri '%s' -Headers $headers -TimeoutSec %d;", escaped_url, timeout);
|
||||
r_strbuf_appendf (cmd, "Write-Host $r.StatusCode;$r.Content}catch{Write-Host 0;$_.Exception.Message}\"");
|
||||
free (escaped_url);
|
||||
char *cmd_str = r_strbuf_drain (cmd);
|
||||
if (!cmd_str) {
|
||||
r_strbuf_free (cmd);
|
||||
return (HttpResponse){ .code = -1 };
|
||||
}
|
||||
R_LOG_DEBUG ("Running PowerShell: %s", cmd_str);
|
||||
char *full_response = r_sys_cmd_str (cmd_str, NULL, NULL);
|
||||
free (cmd_str);
|
||||
|
||||
+10
-5
@@ -6,14 +6,14 @@ static HttpResponse socket_http_post_with_interrupt(const HTTPRequest *request)
|
||||
#if R2__WINDOWS__
|
||||
r_sys_setenv ("R2_CURL", "1");
|
||||
#endif
|
||||
#if R2__UNIX__
|
||||
#if R2__UNIX__ && !__wasi__
|
||||
signal (SIGALRM, r2ai_http_sigint_handler);
|
||||
alarm (request->config.timeout); // Use configured timeout
|
||||
#endif
|
||||
int code = 0;
|
||||
int rlen = 0;
|
||||
char *result = r_socket_http_post (request->url, request->headers, request->data, &code, &rlen);
|
||||
#if R2__UNIX__
|
||||
#if R2__UNIX__ && !__wasi__
|
||||
alarm (0);
|
||||
#endif
|
||||
if (r2ai_http_interrupted) {
|
||||
@@ -21,6 +21,9 @@ static HttpResponse socket_http_post_with_interrupt(const HTTPRequest *request)
|
||||
free (result);
|
||||
return (HttpResponse){ .body = NULL, .code = -1, .length = 0 };
|
||||
}
|
||||
if (!result) {
|
||||
return (HttpResponse){ .body = NULL, .code = -1, .length = 0 };
|
||||
}
|
||||
return (HttpResponse){ .body = result, .code = code, .length = rlen };
|
||||
}
|
||||
|
||||
@@ -29,15 +32,14 @@ static HttpResponse socket_http_get_with_interrupt(const HTTPRequest *request) {
|
||||
#if R2__WINDOWS__
|
||||
r_sys_setenv ("R2_CURL", "1");
|
||||
#endif
|
||||
#if R2__UNIX__
|
||||
#if R2__UNIX__ && !__wasi__
|
||||
signal (SIGALRM, r2ai_http_sigint_handler);
|
||||
alarm (request->config.timeout); // Use configured timeout
|
||||
#endif
|
||||
// Make the request - use r_socket_http_get if available
|
||||
int code = 0;
|
||||
int rlen = 0;
|
||||
char *result = r_socket_http_get (request->url, request->headers, &code, &rlen);
|
||||
#if R2__UNIX__
|
||||
#if R2__UNIX__ && !__wasi__
|
||||
alarm (0);
|
||||
#endif
|
||||
if (r2ai_http_interrupted) {
|
||||
@@ -45,5 +47,8 @@ static HttpResponse socket_http_get_with_interrupt(const HTTPRequest *request) {
|
||||
free (result);
|
||||
return (HttpResponse){ .body = NULL, .code = -1, .length = 0 };
|
||||
}
|
||||
if (!result) {
|
||||
return (HttpResponse){ .body = NULL, .code = -1, .length = 0 };
|
||||
}
|
||||
return (HttpResponse){ .body = result, .code = code, .length = rlen };
|
||||
}
|
||||
+29
-127
@@ -2,152 +2,54 @@
|
||||
|
||||
#include "r2ai.h"
|
||||
// TODO: move into r2/libr/util/json.c
|
||||
//
|
||||
// Helper function to convert RJson to PJ without draining
|
||||
R_API PJ *r_json_to_pj(const RJson *json, PJ *existing_pj) {
|
||||
if (!json) {
|
||||
return existing_pj;
|
||||
}
|
||||
|
||||
PJ *pj = existing_pj? existing_pj: pj_new ();
|
||||
if (!pj) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
switch (json->type) {
|
||||
static void emit(PJ *pj, const RJson *j) {
|
||||
const RJson *c;
|
||||
switch (j->type) {
|
||||
case R_JSON_STRING:
|
||||
pj_s (pj, json->str_value);
|
||||
pj_s (pj, j->str_value);
|
||||
break;
|
||||
case R_JSON_INTEGER:
|
||||
pj_n (pj, json->num.u_value);
|
||||
if (j->num.dbl_value < 0) {
|
||||
pj_N (pj, j->num.s_value);
|
||||
} else {
|
||||
pj_n (pj, j->num.u_value);
|
||||
}
|
||||
break;
|
||||
case R_JSON_DOUBLE:
|
||||
pj_d (pj, json->num.dbl_value);
|
||||
pj_d (pj, j->num.dbl_value);
|
||||
break;
|
||||
case R_JSON_BOOLEAN:
|
||||
pj_b (pj, json->num.u_value);
|
||||
pj_b (pj, j->num.u_value);
|
||||
break;
|
||||
case R_JSON_NULL:
|
||||
pj_null (pj);
|
||||
break;
|
||||
case R_JSON_OBJECT:
|
||||
if (!existing_pj) {
|
||||
pj_o (pj);
|
||||
}
|
||||
// Handle object's properties
|
||||
const RJson *prop = json->children.first;
|
||||
while (prop) {
|
||||
if (prop->key) {
|
||||
switch (prop->type) {
|
||||
case R_JSON_STRING:
|
||||
pj_ks (pj, prop->key, prop->str_value);
|
||||
break;
|
||||
case R_JSON_INTEGER:
|
||||
pj_kn (pj, prop->key, prop->num.u_value);
|
||||
break;
|
||||
case R_JSON_DOUBLE:
|
||||
pj_kd (pj, prop->key, prop->num.dbl_value);
|
||||
break;
|
||||
case R_JSON_BOOLEAN:
|
||||
pj_kb (pj, prop->key, prop->num.u_value);
|
||||
break;
|
||||
case R_JSON_NULL:
|
||||
pj_knull (pj, prop->key);
|
||||
break;
|
||||
case R_JSON_OBJECT:
|
||||
pj_ko (pj, prop->key);
|
||||
if (!r_json_to_pj (prop, pj)) {
|
||||
if (!existing_pj) {
|
||||
pj_free (pj);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
pj_end (pj);
|
||||
break;
|
||||
case R_JSON_ARRAY:
|
||||
pj_ka (pj, prop->key);
|
||||
if (!r_json_to_pj (prop, pj)) {
|
||||
if (!existing_pj) {
|
||||
pj_free (pj);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
pj_end (pj);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
pj_o (pj);
|
||||
for (c = j->children.first; c; c = c->next) {
|
||||
if (c->key) {
|
||||
pj_k (pj, c->key);
|
||||
emit (pj, c);
|
||||
}
|
||||
prop = prop->next;
|
||||
}
|
||||
if (!existing_pj) {
|
||||
pj_end (pj);
|
||||
}
|
||||
pj_end (pj);
|
||||
break;
|
||||
case R_JSON_ARRAY:
|
||||
if (!existing_pj) {
|
||||
pj_a (pj);
|
||||
}
|
||||
// Handle array items
|
||||
const RJson *item = json->children.first;
|
||||
while (item) {
|
||||
switch (item->type) {
|
||||
case R_JSON_STRING:
|
||||
pj_s (pj, item->str_value);
|
||||
break;
|
||||
case R_JSON_INTEGER:
|
||||
pj_n (pj, item->num.u_value);
|
||||
break;
|
||||
case R_JSON_DOUBLE:
|
||||
pj_d (pj, item->num.dbl_value);
|
||||
break;
|
||||
case R_JSON_BOOLEAN:
|
||||
pj_b (pj, item->num.u_value);
|
||||
break;
|
||||
case R_JSON_NULL:
|
||||
pj_null (pj);
|
||||
break;
|
||||
case R_JSON_OBJECT:
|
||||
pj_o (pj);
|
||||
if (!r_json_to_pj (item, pj)) {
|
||||
if (!existing_pj) {
|
||||
pj_free (pj);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
pj_end (pj);
|
||||
break;
|
||||
case R_JSON_ARRAY:
|
||||
pj_a (pj);
|
||||
if (!r_json_to_pj (item, pj)) {
|
||||
if (!existing_pj) {
|
||||
pj_free (pj);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
pj_end (pj);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
item = item->next;
|
||||
}
|
||||
if (!existing_pj) {
|
||||
pj_end (pj);
|
||||
pj_a (pj);
|
||||
for (c = j->children.first; c; c = c->next) {
|
||||
emit (pj, c);
|
||||
}
|
||||
pj_end (pj);
|
||||
break;
|
||||
default:
|
||||
if (!existing_pj) {
|
||||
pj_free (pj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
R_API char *r_json_to_string(const RJson *json) {
|
||||
if (!json) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return pj;
|
||||
}
|
||||
|
||||
// Helper function to clone RJson to string
|
||||
R_API char *r_json_to_string(const RJson *json) {
|
||||
PJ *pj = r_json_to_pj (json, NULL);
|
||||
return pj? pj_drain (pj): NULL;
|
||||
PJ *pj = pj_new ();
|
||||
emit (pj, json);
|
||||
return pj_drain (pj);
|
||||
}
|
||||
@@ -11,7 +11,7 @@ static const R2AIProvider r2ai_providers[] = {
|
||||
{ "gemini", "https://generativelanguage.googleapis.com/v1beta", R2AI_API_GEMINI, true, false },
|
||||
{ "anthropic", "https://api.anthropic.com/v1", R2AI_API_ANTHROPIC, true, false },
|
||||
{ "claude", "https://api.anthropic.com/v1", R2AI_API_ANTHROPIC, true, false },
|
||||
{ "ollama", "http://localhost:11434/api", R2AI_API_OLLAMA, false, true },
|
||||
{ "ollama", "http://localhost:11434/v1", R2AI_API_OLLAMA, false, true },
|
||||
{ "ollamacloud", "https://ollama.com/api", R2AI_API_OLLAMA, true, true },
|
||||
{ "openapi", "http://127.0.0.1:11434", R2AI_API_OPENAI_COMPATIBLE, false, false },
|
||||
{ "opencode", "https://opencode.ai/zen/v1", R2AI_API_OPENAI_COMPATIBLE, true, true },
|
||||
@@ -39,7 +39,45 @@ R_IPI const R2AIProvider *r2ai_get_provider(const char *name) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Forward declaration for rawtools
|
||||
static bool is_vertex(const R2AIProvider *p) {
|
||||
return p->api_type == R2AI_API_VERTEX_GEMINI || p->api_type == R2AI_API_VERTEX_ANTHROPIC;
|
||||
}
|
||||
|
||||
R_IPI R2AI_ChatResponse *r2ai_send(RCorePluginSession *cps, const R2AIProvider *p, R2AIArgs args) {
|
||||
switch (p->api_type) {
|
||||
case R2AI_API_ANTHROPIC:
|
||||
return r2ai_anthropic (cps, args);
|
||||
case R2AI_API_GEMINI:
|
||||
return r2ai_gemini (cps, args);
|
||||
case R2AI_API_VERTEX_GEMINI:
|
||||
return r2ai_vertex_gemini (cps, args);
|
||||
case R2AI_API_VERTEX_ANTHROPIC:
|
||||
return r2ai_vertex_anthropic (cps, args);
|
||||
default:
|
||||
return r2ai_openai (cps, args);
|
||||
}
|
||||
}
|
||||
|
||||
static bool is_generate_api(RCore *core) {
|
||||
const char *apitype = r_config_get (core->config, "r2ai.apitype");
|
||||
return R_STR_ISNOTEMPTY (apitype) && !strcmp (apitype, "generate");
|
||||
}
|
||||
|
||||
static bool use_rawtools(RCore *core, const R2AIProvider *provider, const R2AIArgs *args) {
|
||||
if (!args || !args->tools || r_list_empty (args->tools)) {
|
||||
return false;
|
||||
}
|
||||
if (r_config_get_b (core->config, "r2ai.auto.raw")) {
|
||||
return true;
|
||||
}
|
||||
return provider && provider->api_type == R2AI_API_OLLAMA && is_generate_api (core);
|
||||
}
|
||||
|
||||
// the given system prompt, or r2ai.system when it is empty
|
||||
R_IPI const char *r2ai_system_prompt(RCore *core, const char *sysp) {
|
||||
return R_STR_ISNOTEMPTY (sysp)? sysp: r_config_get (core->config, "r2ai.system");
|
||||
}
|
||||
|
||||
R_IPI R2AI_ChatResponse *r2ai_llmcall(RCorePluginSession *cps, R2AIArgs args) {
|
||||
RCore *core = cps->core;
|
||||
char *owned_model = NULL;
|
||||
@@ -48,15 +86,10 @@ R_IPI R2AI_ChatResponse *r2ai_llmcall(RCorePluginSession *cps, R2AIArgs args) {
|
||||
char *api_key = NULL;
|
||||
R2AI_ChatResponse *res = NULL;
|
||||
|
||||
// Check if rawtools mode is enabled
|
||||
bool rawtools_enabled = r_config_get_b (core->config, "r2ai.auto.raw");
|
||||
const char *provider = args.provider? args.provider: r_config_get (core->config, "r2ai.api");
|
||||
if (!provider) {
|
||||
provider = "gemini";
|
||||
}
|
||||
if (rawtools_enabled && args.tools && r_list_length (args.tools) > 0) {
|
||||
return r2ai_rawtools_llmcall (cps, args);
|
||||
}
|
||||
R2AI_State *state = cps->data;
|
||||
if (!args.model) {
|
||||
const char *config_model = r_config_get (core->config, "r2ai.model");
|
||||
@@ -75,6 +108,7 @@ R_IPI R2AI_ChatResponse *r2ai_llmcall(RCorePluginSession *cps, R2AIArgs args) {
|
||||
const char *configtemp = r_config_get (core->config, "r2ai.temperature");
|
||||
args.temperature = configtemp? atof (configtemp): 0;
|
||||
}
|
||||
args.deterministic = r_config_get_b (core->config, "r2ai.deterministic");
|
||||
|
||||
const R2AIProvider *prov = r2ai_get_provider (provider);
|
||||
if (!prov) {
|
||||
@@ -82,8 +116,7 @@ R_IPI R2AI_ChatResponse *r2ai_llmcall(RCorePluginSession *cps, R2AIArgs args) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
bool is_vertex = (prov->api_type == R2AI_API_VERTEX_GEMINI || prov->api_type == R2AI_API_VERTEX_ANTHROPIC);
|
||||
if (is_vertex) {
|
||||
if (is_vertex (prov)) {
|
||||
const char *vtoken = r2ai_vertex_get_token (state);
|
||||
if (!vtoken) {
|
||||
goto cleanup;
|
||||
@@ -96,19 +129,18 @@ R_IPI R2AI_ChatResponse *r2ai_llmcall(RCorePluginSession *cps, R2AIArgs args) {
|
||||
}
|
||||
}
|
||||
// Make sure we have an API key before proceeding
|
||||
if (prov->requires_api_key) {
|
||||
if (R_STR_ISEMPTY (args.api_key)) {
|
||||
char *Provider = strdup (provider);
|
||||
r_str_case (Provider, true);
|
||||
R_LOG_ERROR ("No API key found for the %s provider. Use r2ai -K", provider);
|
||||
free (Provider);
|
||||
goto cleanup;
|
||||
}
|
||||
if (prov->requires_api_key && R_STR_ISEMPTY (args.api_key)) {
|
||||
R_LOG_ERROR ("No API key found for the %s provider. Use r2ai -K", provider);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (!args.system_prompt) {
|
||||
args.system_prompt = r_config_get (core->config, "r2ai.system");
|
||||
args.system_prompt = r2ai_system_prompt (core, args.system_prompt);
|
||||
int context_pullback = -1;
|
||||
if (use_rawtools (core, prov, &args)) {
|
||||
res = r2ai_rawtools_llmcall (cps, prov, args);
|
||||
goto finish;
|
||||
}
|
||||
|
||||
owned_system_prompt = r2ai_claw_system_prompt (args.system_prompt);
|
||||
args.system_prompt = owned_system_prompt;
|
||||
if (!args.messages) {
|
||||
@@ -118,7 +150,6 @@ R_IPI R2AI_ChatResponse *r2ai_llmcall(RCorePluginSession *cps, R2AIArgs args) {
|
||||
R2AI_Message msg = { .role = "user", .content = (char *)args.input };
|
||||
r2ai_msgs_add (args.messages, &msg);
|
||||
}
|
||||
int context_pullback = -1;
|
||||
// context and user message
|
||||
if (args.input && r_config_get_b (core->config, "r2ai.data")) {
|
||||
const int K = r_config_get_i (core->config, "r2ai.data.nth");
|
||||
@@ -155,30 +186,8 @@ R_IPI R2AI_ChatResponse *r2ai_llmcall(RCorePluginSession *cps, R2AIArgs args) {
|
||||
|
||||
args.thinking_tokens = r_config_get_i (core->config, "r2ai.thinking_tokens");
|
||||
|
||||
const R2AIProvider *p = r2ai_get_provider (provider);
|
||||
if (!p) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
switch (p->api_type) {
|
||||
case R2AI_API_ANTHROPIC:
|
||||
res = r2ai_anthropic (cps, args);
|
||||
break;
|
||||
case R2AI_API_GEMINI:
|
||||
res = r2ai_gemini (cps, args);
|
||||
break;
|
||||
case R2AI_API_VERTEX_GEMINI:
|
||||
res = r2ai_vertex_gemini (cps, args);
|
||||
break;
|
||||
case R2AI_API_VERTEX_ANTHROPIC:
|
||||
res = r2ai_vertex_anthropic (cps, args);
|
||||
break;
|
||||
case R2AI_API_OPENAI_COMPATIBLE:
|
||||
case R2AI_API_OLLAMA:
|
||||
default:
|
||||
res = r2ai_openai (cps, args);
|
||||
break;
|
||||
}
|
||||
res = r2ai_send (cps, prov, args);
|
||||
finish:
|
||||
if (context_pullback != -1) {
|
||||
R2AI_Message *msg = r_list_get_n (args.messages, context_pullback);
|
||||
free ((char *)msg->content);
|
||||
@@ -198,111 +207,98 @@ cleanup:
|
||||
return res;
|
||||
}
|
||||
|
||||
R_IPI const char *r2ai_get_provider_url(RCore *core, const char *provider) {
|
||||
static char *provider_api_url(const char *host, const char *api_root, bool preserve_root) {
|
||||
char *url = r_str_startswith (host, "http")? strdup (host): r_str_newf ("http://%s", host);
|
||||
if (!url) {
|
||||
return NULL;
|
||||
}
|
||||
r_str_trim (url);
|
||||
size_t len = strlen (url);
|
||||
while (len > 0 && url[len - 1] == '/') {
|
||||
url[--len] = 0;
|
||||
}
|
||||
const bool has_root = r_str_endswith (url, "/v1") || r_str_endswith (url, "/api");
|
||||
if (has_root && (preserve_root || r_str_endswith (url, api_root))) {
|
||||
return url;
|
||||
}
|
||||
if (has_root) {
|
||||
url[len - 3] = 0;
|
||||
}
|
||||
char *res = r_str_newf ("%s%s", url, api_root);
|
||||
free (url);
|
||||
return res;
|
||||
}
|
||||
|
||||
R_IPI char *r2ai_get_provider_url(RCore *core, const char *provider) {
|
||||
const R2AIProvider *p = r2ai_get_provider (provider);
|
||||
if (!p) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (p->api_type == R2AI_API_VERTEX_GEMINI || p->api_type == R2AI_API_VERTEX_ANTHROPIC) {
|
||||
if (is_vertex (p)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Handle providers that support custom baseurl
|
||||
const bool is_ollama = p->api_type == R2AI_API_OLLAMA;
|
||||
const bool use_generate = is_ollama && is_generate_api (core);
|
||||
|
||||
if (p->supports_custom_baseurl) {
|
||||
const char *host = r_config_get (core->config, "r2ai.baseurl");
|
||||
if (R_STR_ISNOTEMPTY (host)) {
|
||||
if (r_str_startswith (host, "http")) {
|
||||
if (p->api_type == R2AI_API_OPENAI_COMPATIBLE) {
|
||||
if (r_str_endswith (host, "/v1")) {
|
||||
return r_str_newf ("%s", host);
|
||||
}
|
||||
return r_str_newf ("%s/v1", host);
|
||||
}
|
||||
return r_str_newf ("%s/api", host);
|
||||
}
|
||||
if (p->api_type == R2AI_API_OPENAI_COMPATIBLE) {
|
||||
return r_str_newf ("http://%s/v1", host);
|
||||
}
|
||||
return r_str_newf ("http://%s/api", host);
|
||||
const char *api_root = use_generate? "/api": "/v1";
|
||||
return provider_api_url (host, api_root, is_ollama && !use_generate);
|
||||
}
|
||||
}
|
||||
if (use_generate) {
|
||||
return provider_api_url (p->url, "/api", false);
|
||||
}
|
||||
|
||||
return p->url;
|
||||
return p->url? strdup (p->url): NULL;
|
||||
}
|
||||
R_IPI RList *r2ai_fetch_available_models(RCore *core, const char *provider) {
|
||||
if (!provider) {
|
||||
return NULL;
|
||||
}
|
||||
const R2AIProvider *pcheck = r2ai_get_provider (provider);
|
||||
if (pcheck && (pcheck->api_type == R2AI_API_VERTEX_GEMINI || pcheck->api_type == R2AI_API_VERTEX_ANTHROPIC)) {
|
||||
const R2AIProvider *p = r2ai_get_provider (provider);
|
||||
if (!p) {
|
||||
return NULL;
|
||||
}
|
||||
if (is_vertex (p)) {
|
||||
R_LOG_ERROR ("Model listing is not supported for Vertex AI providers");
|
||||
return NULL;
|
||||
}
|
||||
const char *purl = r2ai_get_provider_url (core, provider);
|
||||
char *purl = r2ai_get_provider_url (core, provider);
|
||||
if (!purl) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get API key for authentication (except for providers that don't require it)
|
||||
char *api_key = NULL;
|
||||
const R2AIProvider *p = r2ai_get_provider (provider);
|
||||
if (p && p->requires_api_key) {
|
||||
// Consolidated helper to fetch the API key from env or file
|
||||
api_key = r2ai_apikeys_get (provider);
|
||||
const bool gemini = p->api_type == R2AI_API_GEMINI;
|
||||
const bool usetags = p->api_type == R2AI_API_OLLAMA && r_str_endswith (purl, "/api");
|
||||
char *api_key = p->requires_api_key? r2ai_apikeys_get (provider): NULL;
|
||||
if (gemini && !api_key) {
|
||||
free (purl);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char *models_url = NULL;
|
||||
int code = 0;
|
||||
char *response = NULL;
|
||||
|
||||
// Special handling for Gemini
|
||||
if (!strcmp (provider, "gemini")) {
|
||||
if (!api_key) {
|
||||
return NULL;
|
||||
}
|
||||
models_url = r_str_newf ("%s/models?key=%s", purl, api_key);
|
||||
const char *headers[2] = { "Content-Type: application/json", NULL };
|
||||
R_LOG_DEBUG ("GET %s", models_url);
|
||||
response = r2ai_http_get (core, models_url, headers, &code, NULL);
|
||||
} else {
|
||||
// Create models endpoint URL
|
||||
const R2AIProvider *prov = r2ai_get_provider (provider);
|
||||
const bool usetags = (prov && prov->api_type == R2AI_API_OLLAMA);
|
||||
models_url = r_str_newf ("%s/%s", purl, usetags? "tags": "models");
|
||||
|
||||
if (api_key) {
|
||||
const char *headers[4] = { "Content-Type: application/json", NULL, NULL, NULL };
|
||||
char *auth_header = NULL;
|
||||
char *version_header = NULL;
|
||||
|
||||
const R2AIProvider *prov = r2ai_get_provider (provider);
|
||||
if (prov && prov->api_type == R2AI_API_ANTHROPIC) {
|
||||
// Anthropic uses different header format
|
||||
auth_header = r_str_newf ("x-api-key: %s", api_key);
|
||||
version_header = strdup ("anthropic-version: 2023-06-01");
|
||||
headers[1] = auth_header;
|
||||
headers[2] = version_header;
|
||||
} else {
|
||||
// Standard OpenAI-compatible format
|
||||
auth_header = r_str_newf ("Authorization: Bearer %s", api_key);
|
||||
headers[1] = auth_header;
|
||||
}
|
||||
|
||||
// Make HTTP GET request
|
||||
R_LOG_DEBUG ("GET %s Headers: %s", models_url, headers);
|
||||
response = r2ai_http_get (core, models_url, headers, &code, NULL);
|
||||
free (auth_header);
|
||||
free (version_header);
|
||||
char *models_url = r_str_newf ("%s/%s", purl, usetags? "tags": "models");
|
||||
char *auth_header = NULL;
|
||||
const char *headers[4] = { "Content-Type: application/json", NULL, NULL, NULL };
|
||||
if (api_key) {
|
||||
if (p->api_type == R2AI_API_ANTHROPIC) {
|
||||
auth_header = r_str_newf ("x-api-key: %s", api_key);
|
||||
headers[2] = "anthropic-version: 2023-06-01";
|
||||
} else if (gemini) {
|
||||
auth_header = r_str_newf ("x-goog-api-key: %s", api_key);
|
||||
} else {
|
||||
// We have no headers
|
||||
R_LOG_DEBUG ("GET %s", models_url);
|
||||
response = r2ai_http_get (core, models_url, NULL, &code, NULL);
|
||||
auth_header = r_str_newf ("Authorization: Bearer %s", api_key);
|
||||
}
|
||||
headers[1] = auth_header;
|
||||
}
|
||||
|
||||
R_LOG_DEBUG ("GET %s", models_url);
|
||||
int code = 0;
|
||||
char *response = r2ai_http_get (core, models_url, headers, &code, NULL);
|
||||
free (auth_header);
|
||||
free (models_url);
|
||||
free (api_key);
|
||||
free (purl);
|
||||
|
||||
if (!response || code != 200) {
|
||||
R_LOG_DEBUG ("Failed to fetch models from %s (code: %d)", provider, code);
|
||||
@@ -310,73 +306,27 @@ R_IPI RList *r2ai_fetch_available_models(RCore *core, const char *provider) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Parse JSON response
|
||||
RList *models = r_list_newf (free);
|
||||
if (!models) {
|
||||
free (response);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
RJson *json = r_json_parse (response);
|
||||
if (json) {
|
||||
const RJson *data = NULL;
|
||||
|
||||
if (!strcmp (provider, "gemini")) {
|
||||
// Gemini has "models" array directly
|
||||
data = r_json_get (json, "models");
|
||||
} else {
|
||||
const R2AIProvider *prov = r2ai_get_provider (provider);
|
||||
const bool usetags = (prov && prov->api_type == R2AI_API_OLLAMA);
|
||||
data = r_json_get (json, usetags? "models": "data");
|
||||
const RJson *data = json? r_json_get (json, (gemini || usetags)? "models": "data"): NULL;
|
||||
const char *key = gemini? "name": usetags? "model": "id";
|
||||
const RJson *item;
|
||||
for (item = (data && data->type == R_JSON_ARRAY)? data->children.first: NULL; item; item = item->next) {
|
||||
const char *id = r_json_get_str (item, key);
|
||||
if (R_STR_ISEMPTY (id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (data && data->type == R_JSON_ARRAY) {
|
||||
const RJson *model_item = data->children.first;
|
||||
while (model_item) {
|
||||
const RJson *id = NULL;
|
||||
char *model_id = NULL;
|
||||
|
||||
if (!strcmp (provider, "gemini")) {
|
||||
// Gemini: extract model ID from "name" field (e.g., "models/gemini-1.5-flash" -> "gemini-1.5-flash")
|
||||
const RJson *name = r_json_get (model_item, "name");
|
||||
if (name && name->type == R_JSON_STRING && R_STR_ISNOTEMPTY (name->str_value)) {
|
||||
char *s = strdup (name->str_value);
|
||||
RList *parts = r_str_split_list (s, "/", 0);
|
||||
if (parts && r_list_length (parts) > 1) {
|
||||
model_id = strdup ((char *)r_list_get_n (parts, r_list_length (parts) - 1));
|
||||
} else {
|
||||
model_id = strdup (name->str_value);
|
||||
}
|
||||
r_list_free (parts);
|
||||
// Only include Gemini models
|
||||
if (!strstr (model_id, "gemini")) {
|
||||
free (model_id);
|
||||
model_id = NULL;
|
||||
}
|
||||
free (s);
|
||||
}
|
||||
} else {
|
||||
const R2AIProvider *prov = r2ai_get_provider (provider);
|
||||
if (prov && prov->api_type == R2AI_API_OLLAMA) {
|
||||
id = r_json_get (model_item, "model");
|
||||
} else {
|
||||
id = r_json_get (model_item, "id");
|
||||
}
|
||||
if (id && id->type == R_JSON_STRING && R_STR_ISNOTEMPTY (id->str_value)) {
|
||||
model_id = strdup (id->str_value);
|
||||
}
|
||||
}
|
||||
|
||||
if (model_id) {
|
||||
R_LOG_DEBUG ("Model: %s", model_id);
|
||||
r_list_append (models, model_id);
|
||||
}
|
||||
model_item = model_item->next;
|
||||
if (gemini) {
|
||||
// "models/gemini-1.5-flash" -> "gemini-1.5-flash", skipping non gemini models
|
||||
const char *slash = strrchr (id, '/');
|
||||
id = slash? slash + 1: id;
|
||||
if (!strstr (id, "gemini")) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
r_json_free (json);
|
||||
r_list_append (models, strdup (id));
|
||||
}
|
||||
|
||||
r_json_free (json);
|
||||
free (response);
|
||||
return models;
|
||||
}
|
||||
@@ -395,20 +345,16 @@ R_IPI void r2ai_list_providers(RCore *core, RStrBuf *sb) {
|
||||
}
|
||||
|
||||
R_IPI void r2ai_refresh_embeddings(RCorePluginSession *cps) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
RListIter *iter, *iter2;
|
||||
char *line;
|
||||
char *file;
|
||||
// refresh embeddings database
|
||||
r_vdb_free (state->db);
|
||||
state->db = r_vdb_new (R2AI_DEFAULT_VECTORS);
|
||||
// enumerate .txt files in directory
|
||||
const char *path = r_config_get (core->config, "r2ai.data.path");
|
||||
const char *path = r_config_get (cps->core->config, "r2ai.data.path");
|
||||
RList *files = r_sys_dir (path);
|
||||
if (r_list_empty (files)) {
|
||||
R_LOG_WARN ("Cannot find any file in r2ai.data.path");
|
||||
}
|
||||
RListIter *iter, *iter2;
|
||||
char *file, *line;
|
||||
r_list_foreach (files, iter, file) {
|
||||
if (!r_str_endswith (file, ".txt")) {
|
||||
continue;
|
||||
@@ -416,19 +362,18 @@ R_IPI void r2ai_refresh_embeddings(RCorePluginSession *cps) {
|
||||
R_LOG_DEBUG ("Index %s", file);
|
||||
char *filepath = r_file_new (path, file, NULL);
|
||||
char *text = r_file_slurp (filepath, NULL);
|
||||
if (text) {
|
||||
R_LOG_DEBUG ("Index %s", file);
|
||||
RList *lines = r_str_split_list (text, "\n", -1);
|
||||
r_list_foreach (lines, iter2, line) {
|
||||
if (r_str_trim_head_ro (line)[0] == 0) {
|
||||
continue;
|
||||
}
|
||||
r_vdb_insert (state->db, line);
|
||||
R_LOG_DEBUG ("Insert %s", line);
|
||||
}
|
||||
r_list_free (lines);
|
||||
}
|
||||
free (filepath);
|
||||
if (!text) {
|
||||
continue;
|
||||
}
|
||||
RList *lines = r_str_split_list (text, "\n", -1);
|
||||
r_list_foreach (lines, iter2, line) {
|
||||
if (*r_str_trim_head_ro (line)) {
|
||||
r_vdb_insert (state->db, line);
|
||||
}
|
||||
}
|
||||
r_list_free (lines);
|
||||
free (text);
|
||||
}
|
||||
r_list_free (files);
|
||||
}
|
||||
+14
-7
@@ -25,7 +25,7 @@ static void show_help() {
|
||||
" -f <file> Load file with r2 API\n"
|
||||
" -s <addr> Seek to address after loading file\n"
|
||||
" -i <script> Load and interpret script file before executing commands\n"
|
||||
" -e <var=value> Set configuration variable\n"
|
||||
" -e <var=value> Set configuration variable (same as r2ai.var)\n"
|
||||
" -E Edit the r2ai rc file\n"
|
||||
" -K Edit the API keys file\n"
|
||||
" -w Launch interactive setup wizard\n");
|
||||
@@ -105,8 +105,8 @@ static void r2ai_repl(RCorePluginSession *cps, const char *provider, const char
|
||||
free (cmd);
|
||||
} else {
|
||||
r_cons_println (core->cons, res);
|
||||
r_list_append (conversation, r_str_newf ("Assistant: %s", res));
|
||||
}
|
||||
r_list_append (conversation, r_str_newf ("Assistant: %s", res));
|
||||
free (res);
|
||||
}
|
||||
if (err) {
|
||||
@@ -197,7 +197,11 @@ int main(int argc, const char **argv) {
|
||||
*equals = '\0';
|
||||
const char *var = var_value;
|
||||
const char *value = equals + 1;
|
||||
r_config_set (core->config, var, value);
|
||||
char *key = r_str_startswith (var, "r2ai")
|
||||
? strdup (var)
|
||||
: r_str_newf ("r2ai.%s", var);
|
||||
r_config_set (core->config, key, value);
|
||||
free (key);
|
||||
} else {
|
||||
r_cons_println (core->cons, "Invalid format for -e. Use var=value.");
|
||||
}
|
||||
@@ -207,7 +211,7 @@ int main(int argc, const char **argv) {
|
||||
case 'E':
|
||||
{
|
||||
char *rc_path = r_file_home (".config/r2ai/rc");
|
||||
r_cons_editor (core->cons, rc_path, NULL);
|
||||
r2ai_cons_editor (core->cons, rc_path, NULL);
|
||||
free (rc_path);
|
||||
goto beach;
|
||||
}
|
||||
@@ -310,8 +314,7 @@ int main(int argc, const char **argv) {
|
||||
if (opt.ind >= argc) {
|
||||
r2ai_repl (&cps, provider, model, conversation);
|
||||
} else {
|
||||
const char *prompt = argv[opt.ind];
|
||||
|
||||
char *prompt = join_argv (argc, argv, opt.ind);
|
||||
char *err = NULL;
|
||||
R2AIArgs args = {
|
||||
.input = prompt,
|
||||
@@ -325,7 +328,11 @@ int main(int argc, const char **argv) {
|
||||
r_cons_println (core->cons, res);
|
||||
free (res);
|
||||
}
|
||||
free (err);
|
||||
if (err) {
|
||||
R_LOG_ERROR ("%s", err);
|
||||
free (err);
|
||||
}
|
||||
free (prompt);
|
||||
r_cons_flush (core->cons);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
project('r2ai', 'c',
|
||||
version : '1.4.0',
|
||||
version : '1.5.4',
|
||||
default_options : ['warning_level=2', 'c_std=c11']
|
||||
)
|
||||
|
||||
|
||||
+62
-244
@@ -11,20 +11,41 @@ R_API void r2ai_tool_call_free(R2AI_ToolCall *tc) {
|
||||
}
|
||||
}
|
||||
|
||||
R_API RList *r2ai_content_blocks_new(void) {
|
||||
RList *cb = r_list_new ();
|
||||
if (!cb) {
|
||||
return NULL;
|
||||
}
|
||||
cb->free = (RListFree)free; // ContentBlocks contain pointers to structs, not structs themselves
|
||||
return cb;
|
||||
static void block_free(R2AI_ContentBlock *b) {
|
||||
free (b->type);
|
||||
free (b->id);
|
||||
free (b->name);
|
||||
free (b->input);
|
||||
free (b->data);
|
||||
free (b->thinking);
|
||||
free (b->signature);
|
||||
free (b->text);
|
||||
free (b);
|
||||
}
|
||||
|
||||
R_API void r2ai_content_blocks_free(RList *cb) {
|
||||
if (!cb) {
|
||||
return;
|
||||
}
|
||||
r_list_free (cb);
|
||||
static R2AI_ContentBlock *block_dup(const R2AI_ContentBlock *b) {
|
||||
R2AI_ContentBlock *d = R_NEW0 (R2AI_ContentBlock);
|
||||
d->type = r_str_new (b->type);
|
||||
d->id = r_str_new (b->id);
|
||||
d->name = r_str_new (b->name);
|
||||
d->input = r_str_new (b->input);
|
||||
d->data = r_str_new (b->data);
|
||||
d->thinking = r_str_new (b->thinking);
|
||||
d->signature = r_str_new (b->signature);
|
||||
d->text = r_str_new (b->text);
|
||||
return d;
|
||||
}
|
||||
|
||||
static R2AI_ToolCall *tc_dup(const R2AI_ToolCall *tc) {
|
||||
R2AI_ToolCall *d = R_NEW0 (R2AI_ToolCall);
|
||||
d->id = r_str_new (tc->id);
|
||||
d->name = r_str_new (tc->name);
|
||||
d->arguments = r_str_new (tc->arguments);
|
||||
return d;
|
||||
}
|
||||
|
||||
R_API RList *r2ai_content_blocks_new(void) {
|
||||
return r_list_newf ((RListFree)block_free);
|
||||
}
|
||||
|
||||
R_API void r2ai_message_fini(R2AI_Message *msg) {
|
||||
@@ -35,276 +56,73 @@ R_API void r2ai_message_fini(R2AI_Message *msg) {
|
||||
free ((void *)msg->content);
|
||||
free (msg->reasoning_content);
|
||||
free (msg->tool_call_id);
|
||||
if (msg->tool_calls) {
|
||||
r_list_free (msg->tool_calls);
|
||||
}
|
||||
if (msg->content_blocks) {
|
||||
r2ai_content_blocks_free (msg->content_blocks);
|
||||
}
|
||||
r_list_free (msg->tool_calls);
|
||||
r_list_free (msg->content_blocks);
|
||||
memset (msg, 0, sizeof (*msg));
|
||||
}
|
||||
|
||||
R_API void r2ai_message_free(R2AI_Message *msg) {
|
||||
if (!msg) {
|
||||
return;
|
||||
}
|
||||
r2ai_message_fini (msg);
|
||||
free (msg);
|
||||
}
|
||||
|
||||
// Conversation is now stored in R2AI_State
|
||||
|
||||
R_API void r2ai_conversation_init(R2AI_State *state) {
|
||||
if (!state || state->conversation) {
|
||||
// Already initialized or invalid state
|
||||
R_IPI void r2ai_chat_response_free(R2AI_ChatResponse *res) {
|
||||
if (!res) {
|
||||
return;
|
||||
}
|
||||
r2ai_message_free ((R2AI_Message *)res->message);
|
||||
free ((void *)res->usage);
|
||||
free (res);
|
||||
}
|
||||
|
||||
state->conversation = r2ai_msgs_new ();
|
||||
R_API void r2ai_conversation_init(R2AI_State *state) {
|
||||
if (state && !state->conversation) {
|
||||
state->conversation = r2ai_msgs_new ();
|
||||
}
|
||||
}
|
||||
|
||||
R_API RList *r2ai_conversation_get(R2AI_State *state) {
|
||||
return state? state->conversation: NULL;
|
||||
}
|
||||
|
||||
// Create a new temporary messages container
|
||||
R_API RList *r2ai_msgs_new(void) {
|
||||
RList *msgs = r_list_new ();
|
||||
if (!msgs) {
|
||||
return NULL;
|
||||
}
|
||||
msgs->free = (RListFree)r2ai_message_free;
|
||||
return msgs;
|
||||
return r_list_newf ((RListFree)r2ai_message_free);
|
||||
}
|
||||
|
||||
R_API void r2ai_msgs_free(RList *msgs) {
|
||||
if (msgs) {
|
||||
r_list_free (msgs);
|
||||
}
|
||||
r_list_free (msgs);
|
||||
}
|
||||
|
||||
// Free the conversation when plugin is unloaded
|
||||
R_API void r2ai_conversation_free(R2AI_State *state) {
|
||||
if (state && state->conversation) {
|
||||
r2ai_msgs_free (state->conversation);
|
||||
if (state) {
|
||||
r_list_free (state->conversation);
|
||||
state->conversation = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear messages in a container without freeing the container itself
|
||||
R_API void r2ai_msgs_clear(RList *msgs) {
|
||||
if (!msgs) {
|
||||
return;
|
||||
}
|
||||
r_list_purge (msgs);
|
||||
}
|
||||
|
||||
// append a deep copy of the message
|
||||
R_API bool r2ai_msgs_add(RList *msgs, const R2AI_Message *msg) {
|
||||
if (!msgs || !msg) {
|
||||
return false;
|
||||
}
|
||||
|
||||
R2AI_Message *new_msg = R_NEW0 (R2AI_Message);
|
||||
new_msg->role = msg->role? strdup (msg->role): NULL;
|
||||
new_msg->content = msg->content? strdup (msg->content): NULL;
|
||||
new_msg->reasoning_content = msg->reasoning_content? strdup (msg->reasoning_content): NULL;
|
||||
|
||||
R2AI_Message *m = R_NEW0 (R2AI_Message);
|
||||
m->role = r_str_new (msg->role);
|
||||
m->content = r_str_new (msg->content);
|
||||
m->reasoning_content = r_str_new (msg->reasoning_content);
|
||||
m->tool_call_id = r_str_new (msg->tool_call_id);
|
||||
if (msg->content_blocks) {
|
||||
RList *cb = r2ai_content_blocks_new ();
|
||||
if (!cb) {
|
||||
r2ai_message_free (new_msg);
|
||||
return false;
|
||||
}
|
||||
RListIter *iter;
|
||||
R2AI_ContentBlock *src;
|
||||
r_list_foreach (msg->content_blocks, iter, src) {
|
||||
R2AI_ContentBlock *dst = R_NEW0 (R2AI_ContentBlock);
|
||||
dst->type = src->type? strdup (src->type): NULL;
|
||||
dst->data = src->data? strdup (src->data): NULL;
|
||||
dst->thinking = src->thinking? strdup (src->thinking): NULL;
|
||||
dst->signature = src->signature? strdup (src->signature): NULL;
|
||||
dst->text = src->text? strdup (src->text): NULL;
|
||||
dst->id = src->id? strdup (src->id): NULL;
|
||||
dst->name = src->name? strdup (src->name): NULL;
|
||||
dst->input = src->input? strdup (src->input): NULL;
|
||||
r_list_append (cb, dst);
|
||||
}
|
||||
new_msg->content_blocks = cb;
|
||||
m->content_blocks = r_list_clone (msg->content_blocks, (RListClone)block_dup);
|
||||
m->content_blocks->free = (RListFree)block_free;
|
||||
}
|
||||
|
||||
new_msg->tool_call_id = msg->tool_call_id? strdup (msg->tool_call_id): NULL;
|
||||
new_msg->tool_calls = r_list_new ();
|
||||
if (!new_msg->tool_calls) {
|
||||
r2ai_message_free (new_msg);
|
||||
return false;
|
||||
}
|
||||
new_msg->tool_calls->free = (RListFree)r2ai_tool_call_free;
|
||||
|
||||
// Copy tool calls if any
|
||||
if (msg->tool_calls) {
|
||||
RListIter *iter;
|
||||
R2AI_ToolCall *src_tc;
|
||||
r_list_foreach (msg->tool_calls, iter, src_tc) {
|
||||
R2AI_ToolCall *dst_tc = R_NEW0 (R2AI_ToolCall);
|
||||
dst_tc->name = src_tc->name? strdup (src_tc->name): NULL;
|
||||
dst_tc->arguments = src_tc->arguments? strdup (src_tc->arguments): NULL;
|
||||
dst_tc->id = src_tc->id? strdup (src_tc->id): NULL;
|
||||
r_list_append (new_msg->tool_calls, dst_tc);
|
||||
}
|
||||
}
|
||||
|
||||
r_list_append (msgs, new_msg);
|
||||
return true;
|
||||
}
|
||||
|
||||
R_API bool r2ai_msgs_add_tool_call(RList *msgs, const R2AI_ToolCall *tc) {
|
||||
if (!msgs || !tc || r_list_empty (msgs)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
R2AI_Message *msg = r_list_get_n (msgs, r_list_length (msgs) - 1);
|
||||
|
||||
// Ensure tool_calls list exists
|
||||
if (!msg->tool_calls) {
|
||||
msg->tool_calls = r_list_new ();
|
||||
if (!msg->tool_calls) {
|
||||
return false;
|
||||
}
|
||||
msg->tool_calls->free = (RListFree)r2ai_tool_call_free;
|
||||
}
|
||||
|
||||
// Copy the tool call
|
||||
R2AI_ToolCall *dst_tc = R_NEW0 (R2AI_ToolCall);
|
||||
dst_tc->name = tc->name? strdup (tc->name): NULL;
|
||||
dst_tc->arguments = tc->arguments? strdup (tc->arguments): NULL;
|
||||
dst_tc->id = tc->id? strdup (tc->id): NULL;
|
||||
|
||||
r_list_append (msg->tool_calls, dst_tc);
|
||||
return true;
|
||||
}
|
||||
|
||||
R_API bool r2ai_msgs_from_response(RList *msgs, const char *json_str) {
|
||||
if (!msgs || !json_str) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool result = false;
|
||||
// r_json_parse expects (and modifies) non-const char*, so we need to cast it
|
||||
RJson *json = r_json_parse ((char *)json_str);
|
||||
if (json) {
|
||||
result = r2ai_msgs_from_json (msgs, json);
|
||||
r_json_free (json);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
R_API bool r2ai_msgs_from_json(RList *msgs, const RJson *json) {
|
||||
if (!msgs || !json) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const RJson *choices = r_json_get (json, "choices");
|
||||
if (!choices || choices->type != R_JSON_ARRAY) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const RJson *choice = r_json_item (choices, 0);
|
||||
if (!choice) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const RJson *message = r_json_get (choice, "message");
|
||||
if (!message) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const RJson *role = r_json_get (message, "role");
|
||||
const RJson *content = r_json_get (message, "content");
|
||||
const RJson *content_blocks = r_json_get (message, "content_blocks");
|
||||
|
||||
// Create a new message to add
|
||||
R2AI_Message new_msg = { 0 };
|
||||
new_msg.role = (role && role->type == R_JSON_STRING)? strdup (role->str_value): strdup ("assistant");
|
||||
new_msg.content = (content && content->type == R_JSON_STRING)? strdup (content->str_value): NULL;
|
||||
new_msg.tool_call_id = NULL;
|
||||
new_msg.tool_calls = NULL;
|
||||
|
||||
if (content_blocks && content_blocks->type == R_JSON_ARRAY && content_blocks->children.count > 0) {
|
||||
RList *cb = r2ai_content_blocks_new ();
|
||||
if (!cb) {
|
||||
r2ai_message_fini (&new_msg);
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < content_blocks->children.count; i++) {
|
||||
const RJson *block = r_json_item (content_blocks, i);
|
||||
if (!block) {
|
||||
continue;
|
||||
}
|
||||
R2AI_ContentBlock *dst = R_NEW0 (R2AI_ContentBlock);
|
||||
if (!dst) {
|
||||
r2ai_content_blocks_free (cb);
|
||||
r2ai_message_fini (&new_msg);
|
||||
return false;
|
||||
}
|
||||
const RJson *type = r_json_get (block, "type");
|
||||
const RJson *data = r_json_get (block, "data");
|
||||
const RJson *thinking = r_json_get (block, "thinking");
|
||||
const RJson *signature = r_json_get (block, "signature");
|
||||
const RJson *text = r_json_get (block, "text");
|
||||
const RJson *id = r_json_get (block, "id");
|
||||
const RJson *name = r_json_get (block, "name");
|
||||
const RJson *input = r_json_get (block, "input");
|
||||
|
||||
dst->type = (type && type->type == R_JSON_STRING)? strdup (type->str_value): NULL;
|
||||
dst->data = (data && data->type == R_JSON_STRING)? strdup (data->str_value): NULL;
|
||||
dst->thinking = (thinking && thinking->type == R_JSON_STRING)? strdup (thinking->str_value): NULL;
|
||||
dst->signature = (signature && signature->type == R_JSON_STRING)? strdup (signature->str_value): NULL;
|
||||
dst->text = (text && text->type == R_JSON_STRING)? strdup (text->str_value): NULL;
|
||||
dst->id = (id && id->type == R_JSON_STRING)? strdup (id->str_value): NULL;
|
||||
dst->name = (name && name->type == R_JSON_STRING)? strdup (name->str_value): NULL;
|
||||
dst->input = (input && input->type == R_JSON_STRING)? strdup (input->str_value): NULL;
|
||||
r_list_append (cb, dst);
|
||||
}
|
||||
new_msg.content_blocks = cb;
|
||||
}
|
||||
|
||||
// Add the message without tool calls first
|
||||
if (!r2ai_msgs_add (msgs, &new_msg)) {
|
||||
r2ai_message_fini (&new_msg);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Now add tool calls if present
|
||||
const RJson *tool_calls = r_json_get (message, "tool_calls");
|
||||
if (tool_calls && tool_calls->type == R_JSON_ARRAY) {
|
||||
// Iterate through array elements
|
||||
for (size_t i = 0; i < tool_calls->children.count; i++) {
|
||||
const RJson *tool_call = r_json_item (tool_calls, i);
|
||||
if (!tool_call) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const RJson *id = r_json_get (tool_call, "id");
|
||||
const RJson *function = r_json_get (tool_call, "function");
|
||||
if (!function) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const RJson *name = r_json_get (function, "name");
|
||||
const RJson *arguments = r_json_get (function, "arguments");
|
||||
|
||||
R2AI_ToolCall tc = { 0 };
|
||||
tc.name = (name && name->type == R_JSON_STRING)? name->str_value: NULL;
|
||||
tc.arguments = (arguments && arguments->type == R_JSON_STRING)? arguments->str_value: NULL;
|
||||
tc.id = (id && id->type == R_JSON_STRING)? id->str_value: NULL;
|
||||
|
||||
if (!r2ai_msgs_add_tool_call (msgs, &tc)) {
|
||||
r2ai_message_fini (&new_msg);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m->tool_calls = msg->tool_calls
|
||||
? r_list_clone (msg->tool_calls, (RListClone)tc_dup)
|
||||
: r_list_new ();
|
||||
m->tool_calls->free = (RListFree)r2ai_tool_call_free;
|
||||
r_list_append (msgs, m);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+379
-345
@@ -3,403 +3,437 @@
|
||||
#include "r2ai.h"
|
||||
#include "r2ai_priv.h"
|
||||
|
||||
R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
static const char *json_string(const RJson *json) {
|
||||
return (json && json->type == R_JSON_STRING)? json->str_value: NULL;
|
||||
}
|
||||
|
||||
static uint64_t json_integer(const RJson *json) {
|
||||
return (json && json->type == R_JSON_INTEGER)? json->num.u_value: 0;
|
||||
}
|
||||
|
||||
typedef enum {
|
||||
R2AI_CHAT_OPENAI,
|
||||
R2AI_CHAT_OLLAMA,
|
||||
R2AI_GENERATE_OLLAMA
|
||||
} R2AIChatAPI;
|
||||
|
||||
static bool is_generate_api(RCore *core) {
|
||||
const char *apitype = r_config_get (core->config, "r2ai.apitype");
|
||||
if (R_STR_ISEMPTY (apitype)) {
|
||||
return false;
|
||||
}
|
||||
return !strcmp (apitype, "generate");
|
||||
}
|
||||
|
||||
static char *ollama_generate_prompt_from_messages(const RList *msgs, char **system_prompt) {
|
||||
if (system_prompt) {
|
||||
*system_prompt = NULL;
|
||||
}
|
||||
if (!msgs || r_list_empty (msgs)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
RStrBuf *sb = r_strbuf_new ("");
|
||||
if (!sb) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
RListIter *iter;
|
||||
const R2AI_Message *msg;
|
||||
r_list_foreach (msgs, iter, msg) {
|
||||
const char *role = msg->role? msg->role: "user";
|
||||
const char *content = msg->content;
|
||||
if (R_STR_ISEMPTY (content)) {
|
||||
continue;
|
||||
}
|
||||
if (!strcmp (role, "system") || !strcmp (role, "developer")) {
|
||||
if (system_prompt && !*system_prompt) {
|
||||
*system_prompt = strdup (content);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
r_strbuf_appendf (sb, "%s: %s\n\n", role, content);
|
||||
}
|
||||
|
||||
char *prompt = r_strbuf_drain (sb);
|
||||
if (R_STR_ISEMPTY (prompt) && system_prompt && *system_prompt) {
|
||||
free (prompt);
|
||||
prompt = strdup (*system_prompt);
|
||||
}
|
||||
return prompt;
|
||||
}
|
||||
|
||||
static void cache_part(RStrBuf *sb, const char *s) {
|
||||
const char *v = r_str_get (s);
|
||||
r_strbuf_appendf (sb, "%zu:%s\n", strlen (v), v);
|
||||
}
|
||||
|
||||
static char *cache_prefix(const char *provider, const char *model, const char *tools_json, const char *messages_json) {
|
||||
RStrBuf *sb = r_strbuf_new ("");
|
||||
cache_part (sb, provider);
|
||||
cache_part (sb, model);
|
||||
cache_part (sb, tools_json);
|
||||
const char *messages = r_str_get (messages_json);
|
||||
size_t len = strlen (messages);
|
||||
if (len > 0 && messages[len - 1] == ']') {
|
||||
len--;
|
||||
}
|
||||
r_strbuf_appendf (sb, "%zu:", len);
|
||||
r_strbuf_append_n (sb, messages, len);
|
||||
return r_strbuf_drain (sb);
|
||||
}
|
||||
|
||||
static size_t prefix_len(const char *a, const char *b) {
|
||||
size_t i = 0;
|
||||
while (a[i] && b[i] && a[i] == b[i]) {
|
||||
i++;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
static void check_chat_cache(RCorePluginSession *cps, const char *provider, const char *model, const char *tools_json, const char *messages_json) {
|
||||
RCore *core = cps->core;
|
||||
const char *provider_name = R_STR_ISNOTEMPTY (args.provider)
|
||||
? args.provider
|
||||
: r_config_get (core->config, "r2ai.api");
|
||||
const char *model_name = R_STR_ISNOTEMPTY (args.model)
|
||||
? args.model
|
||||
: r_config_get (core->config, "r2ai.model");
|
||||
|
||||
const R2AIProvider *provider_info = r2ai_get_provider (provider_name);
|
||||
const char *base_url = r2ai_get_provider_url (core, provider_name);
|
||||
// TODO: default model name should depend on api
|
||||
model_name = model_name? model_name: "gpt-4o-mini";
|
||||
char **error = args.error;
|
||||
RList *tools = args.tools;
|
||||
// create a temp conversation to include the system prompt and the rest of the messages
|
||||
RList *temp_msgs = r2ai_msgs_new ();
|
||||
if (!temp_msgs) {
|
||||
if (error) {
|
||||
*error = strdup ("Failed to create temporary messages array");
|
||||
}
|
||||
return NULL;
|
||||
R2AI_State *state = cps->data;
|
||||
if (!state || !r_config_get_b (core->config, "r2ai.cacheck")) {
|
||||
return;
|
||||
}
|
||||
R2AI_Message system_msg = {
|
||||
.role = "system",
|
||||
.content = (char *)args.system_prompt
|
||||
};
|
||||
// Add system message if available from args.system_prompt
|
||||
if (R_STR_ISNOTEMPTY (args.system_prompt)) {
|
||||
R_LOG_DEBUG ("Using system prompt: %s", args.system_prompt);
|
||||
// if the model name contains "o1" or "o3", it's "developer" role
|
||||
if (strstr (model_name, "o1") || strstr (model_name, "o3")) {
|
||||
system_msg.role = "developer";
|
||||
system_msg.content = (char *)args.system_prompt;
|
||||
char *prefix = cache_prefix (provider, model, tools_json, messages_json);
|
||||
if (state->cache_prefix) {
|
||||
const size_t old_len = strlen (state->cache_prefix);
|
||||
if (!r_str_startswith (prefix, state->cache_prefix)) {
|
||||
const size_t kept = prefix_len (state->cache_prefix, prefix);
|
||||
R_LOG_WARN ("Chat cache prefix changed: preserved %zu/%zu bytes from previous request. Keep system prompt, tool catalog, and previous messages append-only to maximize provider cache hits.", kept, old_len);
|
||||
} else {
|
||||
system_msg.role = "system";
|
||||
system_msg.content = (char *)args.system_prompt;
|
||||
}
|
||||
r2ai_msgs_add (temp_msgs, &system_msg);
|
||||
} else {
|
||||
// Fallback to config if args.system_prompt is not set
|
||||
const char *sysprompt = r_config_get (core->config, "r2ai.system");
|
||||
if (R_STR_ISNOTEMPTY (sysprompt)) {
|
||||
R_LOG_DEBUG ("Using system prompt from config: %s", sysprompt);
|
||||
if (strstr (model_name, "o1") || strstr (model_name, "o3")) {
|
||||
system_msg.role = "developer";
|
||||
} else {
|
||||
system_msg.role = "system";
|
||||
}
|
||||
system_msg.content = (char *)sysprompt;
|
||||
r2ai_msgs_add (temp_msgs, &system_msg);
|
||||
R_LOG_DEBUG ("Chat cache prefix preserved: %zu bytes", old_len);
|
||||
}
|
||||
R_FREE (state->cache_prefix);
|
||||
}
|
||||
if (args.messages) {
|
||||
RListIter *iter;
|
||||
R2AI_Message *msg;
|
||||
r_list_foreach (args.messages, iter, msg) {
|
||||
r2ai_msgs_add (temp_msgs, msg);
|
||||
}
|
||||
} else {
|
||||
R_LOG_WARN ("No messages");
|
||||
}
|
||||
// Safely print debug info about first message
|
||||
if (temp_msgs && !r_list_empty (temp_msgs) && ((R2AI_Message *)r_list_get_n (temp_msgs, 0))->role) {
|
||||
R_LOG_DEBUG ("First message role: %s", ((R2AI_Message *)r_list_get_n (temp_msgs, 0))->role);
|
||||
}
|
||||
if (error) {
|
||||
*error = NULL;
|
||||
}
|
||||
state->cache_prefix = prefix;
|
||||
}
|
||||
|
||||
const char **headers = NULL;
|
||||
char *auth_header = NULL;
|
||||
if (R_STR_ISNOTEMPTY (args.api_key)) {
|
||||
auth_header = r_str_newf ("Authorization: Bearer %s", args.api_key);
|
||||
R_LOG_DEBUG ("Auth header: %s", auth_header);
|
||||
static const char *static_headers[] = { NULL, NULL, NULL };
|
||||
headers = static_headers;
|
||||
headers[0] = "Content-Type: application/json";
|
||||
headers[1] = auth_header;
|
||||
}
|
||||
const char *urlfmt = (provider_info && provider_info->api_type == R2AI_API_OLLAMA)
|
||||
? "%s/chat"
|
||||
: "%s/chat/completions";
|
||||
char *openai_url = r_str_newf (urlfmt, base_url);
|
||||
|
||||
// Create a messages JSON object, either from input messages or from content
|
||||
static char *chat_request_json(RCorePluginSession *cps, const R2AIArgs *args, const char *provider, const char *model, const RList *messages, R2AIChatAPI api, bool check_cache) {
|
||||
const bool use_generate = api == R2AI_GENERATE_OLLAMA;
|
||||
const bool use_ollama = api != R2AI_CHAT_OPENAI;
|
||||
const R2AIProvider *provider_info = r2ai_get_provider (provider);
|
||||
const bool is_ollama_provider = provider_info && provider_info->api_type == R2AI_API_OLLAMA;
|
||||
char *messages_json = NULL;
|
||||
|
||||
if (temp_msgs && !r_list_empty (temp_msgs)) {
|
||||
R_LOG_DEBUG ("Using input messages: %d messages", r_list_length (temp_msgs));
|
||||
messages_json = r2ai_msgs_to_json (temp_msgs, provider_info && provider_info->api_type == R2AI_API_OLLAMA);
|
||||
if (!messages_json) {
|
||||
if (error) {
|
||||
*error = strdup ("Failed to convert messages to JSON");
|
||||
}
|
||||
free (auth_header);
|
||||
return NULL;
|
||||
}
|
||||
char *generate_prompt = NULL;
|
||||
char *generate_system = NULL;
|
||||
if (use_generate) {
|
||||
generate_prompt = ollama_generate_prompt_from_messages (messages, &generate_system);
|
||||
} else {
|
||||
if (error) {
|
||||
*error = strdup ("No messages provided");
|
||||
}
|
||||
free (auth_header);
|
||||
messages_json = r2ai_msgs_to_json (messages, use_ollama);
|
||||
}
|
||||
if (!messages_json && !generate_prompt) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Convert tools to OpenAI format if available
|
||||
char *openai_tools_json = NULL;
|
||||
if (tools && !r_list_empty (tools)) {
|
||||
openai_tools_json = r2ai_tools_to_openai_json (tools);
|
||||
}
|
||||
|
||||
// Create the model settings part
|
||||
PJ *pj = pj_new ();
|
||||
pj_o (pj);
|
||||
pj_ks (pj, "model", model_name);
|
||||
pj_kb (pj, "stream", false);
|
||||
|
||||
if (provider_info && provider_info->api_type == R2AI_API_OLLAMA) {
|
||||
pj_kb (pj, "think", args.thinking_tokens > 0);
|
||||
pj_ko (pj, "options");
|
||||
if (args.max_tokens) {
|
||||
pj_kn (pj, "num_predict", args.max_tokens);
|
||||
}
|
||||
if (args.temperature > 0) {
|
||||
pj_kd (pj, "temperature", args.temperature);
|
||||
}
|
||||
pj_end (pj); // end options
|
||||
} else {
|
||||
#if 0
|
||||
// gpt-5-mini-chat is the only gpt-5 model that supports temperature
|
||||
// gpt-5 gpt-5-mini and gpt-5-nano just throw an error
|
||||
// Only add temperature if this provider/model doesn't have the temperature error flag
|
||||
if (!model_has_error (args.provider, model_name, MODEL_ERROR_TEMPERATURE)) {
|
||||
pj_kd (pj, "temperature", args.temperature? args.temperature: 0.01);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!strcmp (provider_name, "mistral")) {
|
||||
pj_kn (pj, "max_tokens", args.max_tokens? args.max_tokens: 5128);
|
||||
char *tools_json = NULL;
|
||||
if (args->tools && !r_list_empty (args->tools)) {
|
||||
if (use_generate) {
|
||||
R_LOG_DEBUG ("Skipping native tool definitions for /api/generate payload");
|
||||
} else {
|
||||
pj_kn (pj, "max_completion_tokens", args.max_tokens? args.max_tokens: 5128);
|
||||
tools_json = r2ai_tools_to_openai_json (args->tools);
|
||||
}
|
||||
}
|
||||
|
||||
PJ *pj = pj_new ();
|
||||
if (!pj) {
|
||||
free (messages_json);
|
||||
free (generate_prompt);
|
||||
free (generate_system);
|
||||
free (tools_json);
|
||||
return NULL;
|
||||
}
|
||||
pj_o (pj);
|
||||
pj_ks (pj, "model", model);
|
||||
pj_kb (pj, "stream", false);
|
||||
if (use_ollama) {
|
||||
pj_kb (pj, "think", args->thinking_tokens > 0);
|
||||
pj_ko (pj, "options");
|
||||
if (args->max_tokens) {
|
||||
pj_kn (pj, "num_predict", args->max_tokens);
|
||||
}
|
||||
if (args->deterministic) {
|
||||
pj_kn (pj, "temperature", 0);
|
||||
pj_kn (pj, "top_p", 1);
|
||||
pj_kn (pj, "top_k", 1);
|
||||
pj_kn (pj, "repeat_last_n", 0);
|
||||
pj_kn (pj, "repeat_penalty", 1);
|
||||
pj_kn (pj, "seed", 123);
|
||||
} else if (args->temperature > 0) {
|
||||
pj_kd (pj, "temperature", args->temperature);
|
||||
}
|
||||
pj_end (pj);
|
||||
} else if (is_ollama_provider || !strcmp (provider, "mistral")) {
|
||||
pj_kn (pj, "max_tokens", args->max_tokens? args->max_tokens: 5128);
|
||||
} else {
|
||||
pj_kn (pj, "max_completion_tokens", args->max_tokens? args->max_tokens: 5128);
|
||||
}
|
||||
if (use_generate) {
|
||||
pj_ks (pj, "prompt", generate_prompt);
|
||||
if (R_STR_ISNOTEMPTY (generate_system)) {
|
||||
pj_ks (pj, "system", generate_system);
|
||||
}
|
||||
} else {
|
||||
pj_k (pj, "messages");
|
||||
pj_raw (pj, messages_json);
|
||||
if (tools_json) {
|
||||
pj_k (pj, "tools");
|
||||
pj_raw (pj, tools_json);
|
||||
}
|
||||
}
|
||||
pj_end (pj);
|
||||
|
||||
// Get the JSON for model settings
|
||||
char *model_json = pj_drain (pj);
|
||||
|
||||
// Manually create the final JSON by combining parts
|
||||
// Remove the closing brace from model_json
|
||||
size_t model_len = strlen (model_json);
|
||||
if (model_len > 0 && model_json[model_len - 1] == '}') {
|
||||
model_json[model_len - 1] = '\0';
|
||||
if (!use_generate && check_cache) {
|
||||
check_chat_cache (cps, provider, model, tools_json, messages_json);
|
||||
}
|
||||
|
||||
// Create the full JSON with proper structure
|
||||
char *complete_json;
|
||||
if (openai_tools_json) {
|
||||
complete_json = r_str_newf ("%s, \"messages\": %s, \"tools\": %s}",
|
||||
model_json,
|
||||
messages_json,
|
||||
openai_tools_json);
|
||||
free (openai_tools_json);
|
||||
} else {
|
||||
complete_json = r_str_newf ("%s, \"messages\": %s}",
|
||||
model_json,
|
||||
messages_json);
|
||||
}
|
||||
|
||||
// Free intermediate strings
|
||||
free (model_json);
|
||||
free (messages_json);
|
||||
free (generate_prompt);
|
||||
free (generate_system);
|
||||
free (tools_json);
|
||||
return pj_drain (pj);
|
||||
}
|
||||
|
||||
if (!complete_json) {
|
||||
if (error) {
|
||||
*error = strdup ("Failed to create complete request JSON");
|
||||
static char *chat_api_url(const char *base_url, R2AIChatAPI api) {
|
||||
const char *path = api == R2AI_CHAT_OPENAI
|
||||
? "/chat/completions"
|
||||
: (api == R2AI_GENERATE_OLLAMA? "/generate": "/chat");
|
||||
return r_str_newf ("%s%s", base_url, path);
|
||||
}
|
||||
|
||||
static char *ollama_native_base(const char *base_url) {
|
||||
if (r_str_endswith (base_url, "/api")) {
|
||||
return strdup (base_url);
|
||||
}
|
||||
if (r_str_endswith (base_url, "/v1")) {
|
||||
char *url = strdup (base_url);
|
||||
if (!url) {
|
||||
return NULL;
|
||||
}
|
||||
free (auth_header);
|
||||
url[strlen (url) - 3] = 0;
|
||||
char *res = r_str_newf ("%s/api", url);
|
||||
free (url);
|
||||
return res;
|
||||
}
|
||||
return r_str_newf ("%s/api", base_url);
|
||||
}
|
||||
|
||||
static bool allow_ollama_fallback(RCore *core, const char *provider) {
|
||||
if (strcmp (provider, "ollama")) {
|
||||
return false;
|
||||
}
|
||||
const char *base_url = r_config_get (core->config, "r2ai.baseurl");
|
||||
if (R_STR_ISEMPTY (base_url)) {
|
||||
return true;
|
||||
}
|
||||
char *url = strdup (base_url);
|
||||
if (!url) {
|
||||
return false;
|
||||
}
|
||||
r_str_trim (url);
|
||||
size_t len = strlen (url);
|
||||
while (len > 0 && url[len - 1] == '/') {
|
||||
url[--len] = 0;
|
||||
}
|
||||
const bool explicit_root = r_str_endswith (url, "/v1") || r_str_endswith (url, "/api");
|
||||
free (url);
|
||||
return !explicit_root;
|
||||
}
|
||||
|
||||
R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
RCore *core = cps->core;
|
||||
const char *provider_name = args.provider;
|
||||
const char *model_name = R_STR_ISNOTEMPTY (args.model)? args.model: "gpt-4o-mini";
|
||||
|
||||
const R2AIProvider *provider_info = r2ai_get_provider (provider_name);
|
||||
const bool is_ollama_provider = provider_info && provider_info->api_type == R2AI_API_OLLAMA;
|
||||
const bool use_generate = is_ollama_provider && is_generate_api (core);
|
||||
char *base_url = r2ai_get_provider_url (core, provider_name);
|
||||
if (!base_url) {
|
||||
*args.error = strdup ("Failed to resolve provider URL");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Save the full JSON to a file for inspection
|
||||
// XXX: only create request/response files when r2ai.debug is set
|
||||
char *tmpdir = r_file_tmpdir ();
|
||||
char *req_path = r_str_newf ("%s" R_SYS_DIR "r2ai_openai_request.json", tmpdir);
|
||||
r_file_dump (req_path, (const ut8 *)complete_json, strlen (complete_json), 0);
|
||||
R_LOG_DEBUG ("Full request saved to %s", req_path);
|
||||
free (req_path);
|
||||
free (tmpdir);
|
||||
|
||||
R_LOG_DEBUG ("OpenAI API request data: %s", complete_json);
|
||||
|
||||
if (r_config_get_b (core->config, "r2ai.debug")) {
|
||||
// Generate curl command for debugging
|
||||
RStrBuf *curl_cmd = r_strbuf_new ("curl -X POST");
|
||||
if (headers) {
|
||||
for (int i = 0; headers[i]; i++) {
|
||||
r_strbuf_appendf (curl_cmd, " -H \"%s\"", headers[i]);
|
||||
}
|
||||
}
|
||||
r_strbuf_appendf (curl_cmd, " -d '%s' \"%s\"", complete_json, openai_url);
|
||||
eprintf ("Curl command: %s\n", r_strbuf_get (curl_cmd));
|
||||
r_strbuf_free (curl_cmd);
|
||||
R2AIChatAPI chat_api = use_generate
|
||||
? R2AI_GENERATE_OLLAMA
|
||||
: (is_ollama_provider && r_str_endswith (base_url, "/api")? R2AI_CHAT_OLLAMA: R2AI_CHAT_OPENAI);
|
||||
char **error = args.error;
|
||||
// the system prompt goes first, o1 and o3 models call it "developer"
|
||||
RList *temp_msgs = r2ai_msgs_new ();
|
||||
if (R_STR_ISNOTEMPTY (args.system_prompt)) {
|
||||
const bool dev = strstr (model_name, "o1") || strstr (model_name, "o3");
|
||||
R2AI_Message system_msg = { .role = dev? "developer": "system", .content = (char *)args.system_prompt };
|
||||
r2ai_msgs_add (temp_msgs, &system_msg);
|
||||
}
|
||||
RListIter *iter;
|
||||
R2AI_Message *msg;
|
||||
r_list_foreach (args.messages, iter, msg) {
|
||||
r2ai_msgs_add (temp_msgs, msg);
|
||||
}
|
||||
if (r_list_empty (args.messages)) {
|
||||
*error = strdup ("No messages provided");
|
||||
free (base_url);
|
||||
r2ai_msgs_free (temp_msgs);
|
||||
return NULL;
|
||||
}
|
||||
char *auth_header = NULL;
|
||||
const char *headers[] = { "Content-Type: application/json", NULL, NULL };
|
||||
if (R_STR_ISNOTEMPTY (args.api_key)) {
|
||||
auth_header = r_str_newf ("Authorization: Bearer %s", args.api_key);
|
||||
headers[1] = auth_header;
|
||||
}
|
||||
|
||||
// Make the API call
|
||||
char *res = NULL;
|
||||
int code = 0;
|
||||
res = r2ai_http_post (core, openai_url, headers, complete_json, &code, NULL);
|
||||
free (complete_json);
|
||||
free (openai_url);
|
||||
|
||||
if (code != 200) {
|
||||
R_LOG_ERROR ("OpenAI API error %d", code);
|
||||
if (res) {
|
||||
R_LOG_ERROR ("OpenAI API error response: %s", res);
|
||||
char *fallback_base = NULL;
|
||||
const char *request_base = base_url;
|
||||
bool first_request = true;
|
||||
for (;;) {
|
||||
char *request_json = chat_request_json (cps, &args, provider_name, model_name, temp_msgs, chat_api, first_request);
|
||||
if (!request_json) {
|
||||
*error = strdup ("Failed to create request JSON");
|
||||
free (fallback_base);
|
||||
free (auth_header);
|
||||
free (base_url);
|
||||
r2ai_msgs_free (temp_msgs);
|
||||
return NULL;
|
||||
}
|
||||
free (auth_header);
|
||||
free (res);
|
||||
char *api_url = chat_api_url (request_base, chat_api);
|
||||
res = r2ai_post (core, "openai", api_url, headers, request_json, &code, error);
|
||||
free (request_json);
|
||||
free (api_url);
|
||||
if (chat_api != R2AI_CHAT_OPENAI || (code != 404 && code != 405) || !allow_ollama_fallback (core, provider_name)) {
|
||||
break;
|
||||
}
|
||||
fallback_base = ollama_native_base (base_url);
|
||||
if (!fallback_base) {
|
||||
break;
|
||||
}
|
||||
R_LOG_INFO ("OpenAI-compatible Ollama endpoint unavailable; falling back to /api/chat");
|
||||
R_FREE (*error);
|
||||
request_base = fallback_base;
|
||||
chat_api = R2AI_CHAT_OLLAMA;
|
||||
first_request = false;
|
||||
}
|
||||
free (fallback_base);
|
||||
free (base_url);
|
||||
|
||||
free (auth_header);
|
||||
if (!res) {
|
||||
r2ai_msgs_free (temp_msgs);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Save the response for inspection
|
||||
tmpdir = r_file_tmpdir ();
|
||||
char *res_path = r_str_newf ("%s" R_SYS_DIR "r2ai_openai_response.json", tmpdir);
|
||||
r_file_dump (res_path, (const ut8 *)res, strlen (res), 0);
|
||||
if (r_config_get_b (core->config, "r2ai.debug")) {
|
||||
eprintf ("OpenAI API response: %s\n", res);
|
||||
}
|
||||
free (res_path);
|
||||
free (tmpdir);
|
||||
|
||||
// Parse the response into our messages structure
|
||||
|
||||
char *res_copy = strdup (res);
|
||||
RJson *jres = r_json_parse (res_copy);
|
||||
RJson *jres = r_json_parse (res);
|
||||
if (jres) {
|
||||
// Create a new message structure
|
||||
const bool is_native_ollama = chat_api != R2AI_CHAT_OPENAI;
|
||||
R2AI_Message *message = R_NEW0 (R2AI_Message);
|
||||
R2AI_Usage *usage = R_NEW0 (R2AI_Usage);
|
||||
if (message) {
|
||||
// Process the response using our r2ai_msgs_from_json logic
|
||||
const RJson *usage_json = NULL;
|
||||
if (provider_info && provider_info->api_type == R2AI_API_OLLAMA) {
|
||||
// Ollama has usage info at top level
|
||||
usage_json = jres;
|
||||
} else {
|
||||
usage_json = r_json_get (jres, "usage");
|
||||
const RJson *usage_json = is_native_ollama? jres: r_json_get (jres, "usage");
|
||||
if (usage_json && usage_json->type == R_JSON_OBJECT) {
|
||||
usage->prompt_tokens = json_integer (r_json_get (usage_json, is_native_ollama? "prompt_eval_count": "prompt_tokens"));
|
||||
usage->completion_tokens = json_integer (r_json_get (usage_json, is_native_ollama? "eval_count": "completion_tokens"));
|
||||
usage->total_tokens = is_native_ollama
|
||||
? usage->prompt_tokens + usage->completion_tokens
|
||||
: json_integer (r_json_get (usage_json, "total_tokens"));
|
||||
}
|
||||
|
||||
const RJson *message_json = NULL;
|
||||
if (use_generate) {
|
||||
const char *content = json_string (r_json_get (jres, "response"));
|
||||
const char *thinking = json_string (r_json_get (jres, "thinking"));
|
||||
message->role = strdup ("assistant");
|
||||
if (content) {
|
||||
message->content = strdup (content);
|
||||
}
|
||||
if (R_STR_ISNOTEMPTY (thinking)) {
|
||||
message->reasoning_content = strdup (thinking);
|
||||
}
|
||||
} else if (is_native_ollama) {
|
||||
message_json = r_json_get (jres, "message");
|
||||
} else {
|
||||
const RJson *choices = r_json_get (jres, "choices");
|
||||
if (choices && choices->type == R_JSON_ARRAY) {
|
||||
const RJson *choice = r_json_item (choices, 0);
|
||||
message_json = choice? r_json_get (choice, "message"): NULL;
|
||||
}
|
||||
}
|
||||
|
||||
if (message_json) {
|
||||
const char *role = json_string (r_json_get (message_json, "role"));
|
||||
const char *content = json_string (r_json_get (message_json, "content"));
|
||||
const char *reasoning_content = json_string (r_json_get (message_json, "reasoning_content"));
|
||||
const char *thinking = json_string (r_json_get (message_json, "thinking"));
|
||||
const char *tool_call_id = json_string (r_json_get (message_json, "tool_call_id"));
|
||||
const RJson *tool_calls = r_json_get (message_json, "tool_calls");
|
||||
|
||||
message->role = strdup (role? role: "assistant");
|
||||
if (content) {
|
||||
message->content = strdup (content);
|
||||
}
|
||||
if (R_STR_ISNOTEMPTY (reasoning_content)) {
|
||||
message->reasoning_content = strdup (reasoning_content);
|
||||
} else if (R_STR_ISNOTEMPTY (thinking)) {
|
||||
message->reasoning_content = strdup (thinking);
|
||||
}
|
||||
if (R_STR_ISNOTEMPTY (tool_call_id)) {
|
||||
message->tool_call_id = strdup (tool_call_id);
|
||||
}
|
||||
|
||||
if (usage_json && usage_json->type == R_JSON_OBJECT) {
|
||||
if (provider_info && provider_info->api_type == R2AI_API_OLLAMA) {
|
||||
// Ollama field names
|
||||
const RJson *prompt_tokens = r_json_get (usage_json, "prompt_eval_count");
|
||||
const RJson *completion_tokens = r_json_get (usage_json, "eval_count");
|
||||
if (prompt_tokens && prompt_tokens->type == R_JSON_INTEGER) {
|
||||
usage->prompt_tokens = prompt_tokens->num.u_value;
|
||||
}
|
||||
if (completion_tokens && completion_tokens->type == R_JSON_INTEGER) {
|
||||
usage->completion_tokens = completion_tokens->num.u_value;
|
||||
}
|
||||
// Calculate total_tokens for ollama
|
||||
usage->total_tokens = usage->prompt_tokens + usage->completion_tokens;
|
||||
} else {
|
||||
// OpenAI field names
|
||||
const RJson *prompt_tokens = r_json_get (usage_json, "prompt_tokens");
|
||||
const RJson *completion_tokens = r_json_get (usage_json, "completion_tokens");
|
||||
const RJson *total_tokens = r_json_get (usage_json, "total_tokens");
|
||||
if (prompt_tokens && prompt_tokens->type == R_JSON_INTEGER) {
|
||||
usage->prompt_tokens = prompt_tokens->num.u_value;
|
||||
}
|
||||
if (completion_tokens && completion_tokens->type == R_JSON_INTEGER) {
|
||||
usage->completion_tokens = completion_tokens->num.u_value;
|
||||
}
|
||||
if (total_tokens && total_tokens->type == R_JSON_INTEGER) {
|
||||
usage->total_tokens = total_tokens->num.u_value;
|
||||
}
|
||||
}
|
||||
}
|
||||
const RJson *message_json = NULL;
|
||||
if (provider_info && provider_info->api_type == R2AI_API_OLLAMA) {
|
||||
// Ollama returns message directly
|
||||
message_json = r_json_get (jres, "message");
|
||||
} else {
|
||||
// OpenAI-style response with choices array
|
||||
const RJson *choices = r_json_get (jres, "choices");
|
||||
if (choices && choices->type == R_JSON_ARRAY) {
|
||||
const RJson *choice = r_json_item (choices, 0);
|
||||
if (choice) {
|
||||
message_json = r_json_get (choice, "message");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (message_json) {
|
||||
const RJson *role = r_json_get (message_json, "role");
|
||||
const RJson *content = r_json_get (message_json, "content");
|
||||
const RJson *reasoning_content = r_json_get (message_json, "reasoning_content");
|
||||
const RJson *thinking = r_json_get (message_json, "thinking");
|
||||
const RJson *tool_call_id = r_json_get (message_json, "tool_call_id");
|
||||
const RJson *tool_calls = r_json_get (message_json, "tool_calls");
|
||||
|
||||
// Set the basic message properties
|
||||
message->role = (role && role->type == R_JSON_STRING)? strdup (role->str_value): strdup ("assistant");
|
||||
|
||||
if (content && content->type == R_JSON_STRING) {
|
||||
message->content = strdup (content->str_value);
|
||||
}
|
||||
|
||||
if (reasoning_content && reasoning_content->type == R_JSON_STRING && R_STR_ISNOTEMPTY (reasoning_content->str_value)) {
|
||||
message->reasoning_content = strdup (reasoning_content->str_value);
|
||||
} else if (thinking && thinking->type == R_JSON_STRING && R_STR_ISNOTEMPTY (thinking->str_value)) {
|
||||
message->reasoning_content = strdup (thinking->str_value);
|
||||
}
|
||||
|
||||
if (tool_call_id && tool_call_id->type == R_JSON_STRING && R_STR_ISNOTEMPTY (tool_call_id->str_value)) {
|
||||
message->tool_call_id = strdup (tool_call_id->str_value);
|
||||
}
|
||||
|
||||
if (tool_calls && tool_calls->type == R_JSON_ARRAY && tool_calls->children.count > 0) {
|
||||
message->tool_calls = r_list_new ();
|
||||
if (message->tool_calls) {
|
||||
message->tool_calls->free = (RListFree)r2ai_tool_call_free;
|
||||
for (size_t i = 0; i < tool_calls->children.count; i++) {
|
||||
const RJson *tool_call = r_json_item (tool_calls, i);
|
||||
if (!tool_call || tool_call->type != R_JSON_OBJECT) {
|
||||
continue;
|
||||
}
|
||||
R2AI_ToolCall *tc = R_NEW0 (R2AI_ToolCall);
|
||||
if (!tc) {
|
||||
continue;
|
||||
}
|
||||
const RJson *tc_id = r_json_get (tool_call, "id");
|
||||
if (tc_id && tc_id->type == R_JSON_STRING && R_STR_ISNOTEMPTY (tc_id->str_value)) {
|
||||
tc->id = strdup (tc_id->str_value);
|
||||
}
|
||||
const RJson *function = r_json_get (tool_call, "function");
|
||||
const RJson *name_json = NULL;
|
||||
const RJson *arguments = NULL;
|
||||
if (function && function->type == R_JSON_OBJECT) {
|
||||
name_json = r_json_get (function, "name");
|
||||
arguments = r_json_get (function, "arguments");
|
||||
} else {
|
||||
name_json = r_json_get (tool_call, "name");
|
||||
arguments = r_json_get (tool_call, "arguments");
|
||||
}
|
||||
if (name_json && name_json->type == R_JSON_STRING && R_STR_ISNOTEMPTY (name_json->str_value)) {
|
||||
tc->name = strdup (name_json->str_value);
|
||||
}
|
||||
if (arguments) {
|
||||
if (arguments->type == R_JSON_STRING && R_STR_ISNOTEMPTY (arguments->str_value)) {
|
||||
tc->arguments = strdup (arguments->str_value);
|
||||
} else {
|
||||
char *arguments_json = r_json_to_string (arguments);
|
||||
if (arguments_json) {
|
||||
tc->arguments = arguments_json;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!tc->name || !tc->arguments) {
|
||||
r2ai_tool_call_free (tc);
|
||||
continue;
|
||||
}
|
||||
if (!tc->id) {
|
||||
tc->id = r_str_newf ("call_%zu", i);
|
||||
}
|
||||
r_list_append (message->tool_calls, tc);
|
||||
if (tool_calls && tool_calls->type == R_JSON_ARRAY && tool_calls->children.count > 0) {
|
||||
message->tool_calls = r_list_newf ((RListFree)r2ai_tool_call_free);
|
||||
if (message->tool_calls) {
|
||||
size_t i;
|
||||
for (i = 0; i < tool_calls->children.count; i++) {
|
||||
const RJson *tool_call = r_json_item (tool_calls, i);
|
||||
if (!tool_call || tool_call->type != R_JSON_OBJECT) {
|
||||
continue;
|
||||
}
|
||||
if (r_list_empty (message->tool_calls)) {
|
||||
r_list_free (message->tool_calls);
|
||||
message->tool_calls = NULL;
|
||||
const RJson *function = r_json_get (tool_call, "function");
|
||||
const RJson *source = (function && function->type == R_JSON_OBJECT)? function: tool_call;
|
||||
const char *name = json_string (r_json_get (source, "name"));
|
||||
const RJson *arguments = r_json_get (source, "arguments");
|
||||
if (R_STR_ISEMPTY (name) || !arguments) {
|
||||
continue;
|
||||
}
|
||||
R2AI_ToolCall *tc = R_NEW0 (R2AI_ToolCall);
|
||||
const char *tc_id = json_string (r_json_get (tool_call, "id"));
|
||||
if (R_STR_ISNOTEMPTY (tc_id)) {
|
||||
tc->id = strdup (tc_id);
|
||||
}
|
||||
if (!tc->id) {
|
||||
tc->id = r_str_newf ("call_%zu", i);
|
||||
}
|
||||
tc->name = strdup (name);
|
||||
if (arguments->type == R_JSON_STRING && R_STR_ISNOTEMPTY (arguments->str_value)) {
|
||||
tc->arguments = strdup (arguments->str_value);
|
||||
} else {
|
||||
tc->arguments = r_json_to_string (arguments);
|
||||
}
|
||||
if (!tc->name || !tc->arguments) {
|
||||
r2ai_tool_call_free (tc);
|
||||
continue;
|
||||
}
|
||||
r_list_append (message->tool_calls, tc);
|
||||
}
|
||||
if (r_list_empty (message->tool_calls)) {
|
||||
r_list_free (message->tool_calls);
|
||||
message->tool_calls = NULL;
|
||||
}
|
||||
}
|
||||
// TODO: Handle tool calls if present?
|
||||
}
|
||||
}
|
||||
r_json_free (jres);
|
||||
R2AI_ChatResponse *result = R_NEW0 (R2AI_ChatResponse);
|
||||
result->message = message;
|
||||
result->usage = usage;
|
||||
free (res_copy);
|
||||
free (auth_header);
|
||||
r2ai_msgs_free (temp_msgs);
|
||||
free (res);
|
||||
return result;
|
||||
}
|
||||
|
||||
free (auth_header);
|
||||
free (res);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
R_IPI char *r2ai_openai_stream(RCore *core, R2AIArgs args) {
|
||||
(void)core;
|
||||
(void)args;
|
||||
r2ai_msgs_free (temp_msgs);
|
||||
return NULL;
|
||||
}
|
||||
@@ -143,7 +143,6 @@ static char *replace_vars(RCore *core, const char *text) {
|
||||
p = end + 1;
|
||||
} else {
|
||||
r_strbuf_append (sb, "${");
|
||||
p += 2;
|
||||
}
|
||||
} else if (*p == '$' && *(p + 1) == '(') {
|
||||
p += 2;
|
||||
@@ -158,7 +157,6 @@ static char *replace_vars(RCore *core, const char *text) {
|
||||
p = end + 1;
|
||||
} else {
|
||||
r_strbuf_append (sb, "$(");
|
||||
p += 2;
|
||||
}
|
||||
} else {
|
||||
r_strbuf_append_n (sb, p, 1);
|
||||
|
||||
+11
-2
@@ -63,7 +63,7 @@ Enter the chat REPL.
|
||||
Launch the interactive setup wizard.
|
||||
.TP
|
||||
.B \-L
|
||||
Show chat logs (use -Lj for JSON). Only for auto mode.
|
||||
Show chat logs (use -Lj for JSON).
|
||||
.TP
|
||||
.B \-L-\fIN\fR
|
||||
Delete the last N messages from the chat history.
|
||||
@@ -86,7 +86,13 @@ Name of the provider (e.g., openai). List possibilities with \fBr2ai -e r2ai.api
|
||||
Model name. List possibilities with \fBr2ai -e r2ai.model=?\fR.
|
||||
.TP
|
||||
.B r2ai.baseurl
|
||||
Remote LLM base URL (e.g., http://127.0.0.1:11434).
|
||||
Provider API root. A /v1 suffix selects OpenAI-compatible chat completions; /api selects native Ollama.
|
||||
.TP
|
||||
.B r2ai.apitype
|
||||
Use chat completions (default) or the native Ollama generate operation.
|
||||
.TP
|
||||
.B r2ai.cacheck
|
||||
Warn when chat requests are not append-only and may miss provider prompt-cache hits.
|
||||
.TP
|
||||
.B r2ai.max_tokens
|
||||
Maximum output tokens or total tokens.
|
||||
@@ -109,6 +115,9 @@ User prompt for \fBr2ai -d\fR.
|
||||
.B r2ai.clippy
|
||||
Display chat replies using r2clippy instead of plain text.
|
||||
.TP
|
||||
.B r2ai.markdown
|
||||
Render LLM responses with the radare2 markdown colorizer (default: true).
|
||||
.TP
|
||||
.B r2ai.wizard
|
||||
Auto-start the setup wizard on the first interactive chat session.
|
||||
.TP
|
||||
|
||||
+138
-180
@@ -30,7 +30,7 @@ static RCoreHelpMessage help_msg_r2ai = {
|
||||
"r2ai", " -q [name] (inst)", "run predefined prompt with optional instructions",
|
||||
"r2ai", " -r", "enter the chat repl",
|
||||
"r2ai", " -s[?ijynak*]", "async task queue: list, show, interact, approve, decline, kill (see r2ai -s?)",
|
||||
"r2ai", " -L", "show chat logs (See -Lj for json). Only for auto mode.",
|
||||
"r2ai", " -L", "show chat logs (See -Lj for json)",
|
||||
"r2ai", " -C", "compact conversation history",
|
||||
"r2ai", " -LR", "create a log report",
|
||||
"r2ai", " -L-[N]", "delete the last (or N last messages from the chat history)",
|
||||
@@ -70,7 +70,6 @@ static char *r2ai_default_auto_prompt(void) {
|
||||
}
|
||||
|
||||
R_API char *r2ai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
RCore *core = cps->core;
|
||||
if (R_STR_ISEMPTY (args.input) && !args.messages) {
|
||||
if (args.error) {
|
||||
*args.error = r_str_newf ("Usage: r2ai [-h] [prompt]");
|
||||
@@ -78,10 +77,6 @@ R_API char *r2ai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (R_STR_ISEMPTY (args.system_prompt)) {
|
||||
args.system_prompt = r_config_get (core->config, "r2ai.system");
|
||||
}
|
||||
|
||||
RList *msgs = args.messages;
|
||||
bool own_msgs = false;
|
||||
if (args.input || !msgs) {
|
||||
@@ -107,8 +102,7 @@ R_API char *r2ai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
if (res->message && res->message->content) {
|
||||
content = strdup (res->message->content);
|
||||
}
|
||||
r2ai_message_free ((R2AI_Message *)res->message);
|
||||
free (res);
|
||||
r2ai_chat_response_free (res);
|
||||
}
|
||||
if (own_msgs) {
|
||||
r2ai_msgs_free (msgs);
|
||||
@@ -209,34 +203,56 @@ static char *r2ai_apply_offsets(const char *text, RList *offsets, int width) {
|
||||
return r_strbuf_drain (sb);
|
||||
}
|
||||
|
||||
// query the llm with rag and log the error if any, returns NULL on failure
|
||||
// when r2ai.history is set the exchange is kept in the conversation (see -L, -R)
|
||||
static char *ask(RCorePluginSession *cps, const char *input, bool chat) {
|
||||
R2AI_State *state = cps->data;
|
||||
RList *history = (chat && r_config_get_b (cps->core->config, "r2ai.history"))
|
||||
? r2ai_conversation_get (state)
|
||||
: NULL;
|
||||
char *err = NULL;
|
||||
char *res = r2ai (cps, (R2AIArgs){ .input = input, .messages = history, .error = &err, .dorag = true });
|
||||
if (err) {
|
||||
R_LOG_ERROR ("%s", err);
|
||||
free (err);
|
||||
R_FREE (res);
|
||||
}
|
||||
if (history && res) {
|
||||
R2AI_Message user = { .role = "user", .content = (char *)input };
|
||||
R2AI_Message assistant = { .role = "assistant", .content = res };
|
||||
r2ai_msgs_add (history, &user);
|
||||
r2ai_msgs_add (history, &assistant);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
static void add_block(RStrBuf *sb, char *dec, RList *offsets, int *width) {
|
||||
if (offsets) {
|
||||
r2ai_collect_offsets (offsets, dec, width);
|
||||
}
|
||||
r_strbuf_appendf (sb, "\n[BEGIN]\n%s[END]\n", dec);
|
||||
free (dec);
|
||||
}
|
||||
|
||||
static void cmd_r2ai_d(RCorePluginSession *cps, const char *input, const bool recursive, const bool offsets) {
|
||||
RCore *core = cps->core;
|
||||
const char *prompt = r_config_get (core->config, "r2ai.prompt");
|
||||
const char *lang = r_config_get (core->config, "r2ai.lang");
|
||||
char *full_prompt;
|
||||
if (!R_STR_ISEMPTY (input)) {
|
||||
R_LOG_DEBUG ("User question: %s", input);
|
||||
full_prompt = strdup (input);
|
||||
} else {
|
||||
if (!R_STR_ISEMPTY (lang)) {
|
||||
full_prompt = r_str_newf ("%s. Translate the code into %s programming language.", prompt, lang);
|
||||
} else {
|
||||
full_prompt = strdup (prompt);
|
||||
}
|
||||
RStrBuf *sb = r_strbuf_new (R_STR_ISNOTEMPTY (input)? input: r_config_get (core->config, "r2ai.prompt"));
|
||||
if (R_STR_ISEMPTY (input) && R_STR_ISNOTEMPTY (lang)) {
|
||||
r_strbuf_appendf (sb, ". Translate the code into %s programming language.", lang);
|
||||
}
|
||||
char *cmds = strdup (r_config_get (core->config, "r2ai.cmds"));
|
||||
RStrBuf *sb = r_strbuf_new (full_prompt);
|
||||
RList *cmdslist = r_str_split_list (cmds, ",", -1);
|
||||
RListIter *iter;
|
||||
const char *cmd;
|
||||
RList *refslist = NULL;
|
||||
char *refs = NULL;
|
||||
RList *offsetslist = offsets? r_list_newf (free): NULL;
|
||||
char *offset_fallback = NULL;
|
||||
int offset_width = 0;
|
||||
if (recursive) {
|
||||
char *refs = r_core_cmd_str (core, "axff~^C[2]~$$");
|
||||
refs = r_core_cmd_str (core, "axff~^C[2]~$$");
|
||||
refslist = r_str_split_list (refs, ",", 0);
|
||||
free (refs);
|
||||
}
|
||||
RConfigHold *hold = r_config_hold_new (core->config);
|
||||
r_config_hold (hold, "scr.color", "scr.utf8", NULL);
|
||||
@@ -246,16 +262,10 @@ static void cmd_r2ai_d(RCorePluginSession *cps, const char *input, const bool re
|
||||
char *ocmd = offsets? r2ai_offset_cmd (cmd): NULL;
|
||||
const char *dcmd = ocmd? ocmd: cmd;
|
||||
char *dec = r_core_cmd_str (core, dcmd);
|
||||
if (offsets) {
|
||||
if (!offset_fallback) {
|
||||
offset_fallback = strdup (dec);
|
||||
}
|
||||
r2ai_collect_offsets (offsetslist, dec, &offset_width);
|
||||
if (offsets && !offset_fallback) {
|
||||
offset_fallback = strdup (dec);
|
||||
}
|
||||
r_strbuf_append (sb, "\n[BEGIN]\n");
|
||||
r_strbuf_append (sb, dec);
|
||||
r_strbuf_append (sb, "[END]\n");
|
||||
free (dec);
|
||||
add_block (sb, dec, offsetslist, &offset_width);
|
||||
if (recursive) {
|
||||
RListIter *iter2;
|
||||
char *at;
|
||||
@@ -264,14 +274,7 @@ static void cmd_r2ai_d(RCorePluginSession *cps, const char *input, const bool re
|
||||
if (core->num->nc.errors) {
|
||||
continue;
|
||||
}
|
||||
char *dec = r_core_cmd_str_at (core, n, dcmd);
|
||||
if (offsets) {
|
||||
r2ai_collect_offsets (offsetslist, dec, &offset_width);
|
||||
}
|
||||
r_strbuf_append (sb, "\n[BEGIN]\n");
|
||||
r_strbuf_append (sb, dec);
|
||||
r_strbuf_append (sb, "[END]\n");
|
||||
free (dec);
|
||||
add_block (sb, r_core_cmd_str_at (core, n, dcmd), offsetslist, &offset_width);
|
||||
}
|
||||
}
|
||||
free (ocmd);
|
||||
@@ -279,26 +282,16 @@ static void cmd_r2ai_d(RCorePluginSession *cps, const char *input, const bool re
|
||||
r_config_hold_restore (hold);
|
||||
r_config_hold_free (hold);
|
||||
r_list_free (refslist);
|
||||
free (refs);
|
||||
char *s = r_strbuf_drain (sb);
|
||||
if (r_config_get_b (core->config, "r2ai.async")) {
|
||||
const char *sys = r_config_get (core->config, "r2ai.system");
|
||||
char *title = r_str_newf ("-d%s%s%s", recursive? "r": "", offsets? "o": "", R_STR_ISNOTEMPTY (input)? " ": "");
|
||||
if (R_STR_ISNOTEMPTY (input)) {
|
||||
char *n = r_str_newf ("%s%s", title, input);
|
||||
free (title);
|
||||
title = n;
|
||||
}
|
||||
int id = r2ai_async_query (cps, title, s, sys);
|
||||
r_cons_printf (core->cons, "[async] task %d queued (%s)\n", id, title);
|
||||
const char *q = r_str_get (input);
|
||||
char *title = r_str_newf ("-d%s%s%s%s", recursive? "r": "", offsets? "o": "", *q? " ": "", q);
|
||||
r2ai_async_query (cps, title, s, NULL);
|
||||
free (title);
|
||||
} else {
|
||||
char *error = NULL;
|
||||
R2AIArgs d_args = { .input = s, .error = &error, .dorag = true };
|
||||
char *res = r2ai (cps, d_args);
|
||||
if (error) {
|
||||
R_LOG_ERROR ("%s", error);
|
||||
free (error);
|
||||
} else if (offsets) {
|
||||
char *res = ask (cps, s, true);
|
||||
if (res && offsets) {
|
||||
char *ores = R_STR_ISNOTEMPTY (res)
|
||||
? r2ai_apply_offsets (res, offsetslist, offset_width)
|
||||
: (offset_fallback? strdup (offset_fallback): NULL);
|
||||
@@ -315,6 +308,27 @@ static void cmd_r2ai_d(RCorePluginSession *cps, const char *input, const bool re
|
||||
free (offset_fallback);
|
||||
r_list_free (offsetslist);
|
||||
r_list_free (cmdslist);
|
||||
free (cmds);
|
||||
}
|
||||
|
||||
R_IPI void r2ai_print_response(RCore *core, const char *text) {
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
#if R2_VERSION_NUMBER >= 60106
|
||||
if (r_config_get_b (core->config, "r2ai.markdown")) {
|
||||
char *md = r_core_md2txt (core, text, false);
|
||||
if (md) {
|
||||
r_cons_print (core->cons, md);
|
||||
if (!r_str_endswith (md, "\n")) {
|
||||
r_cons_newline (core->cons);
|
||||
}
|
||||
free (md);
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
r_cons_println (core->cons, text);
|
||||
}
|
||||
|
||||
static void cmd_r2ai_repl(RCorePluginSession *cps) {
|
||||
@@ -344,21 +358,15 @@ static void cmd_r2ai_repl(RCorePluginSession *cps) {
|
||||
}
|
||||
}
|
||||
r_strbuf_appendf (sb, "User: %s\n", ptr);
|
||||
const char *a = r_strbuf_tostring (sb);
|
||||
char *error = NULL;
|
||||
char *res =
|
||||
r2ai (cps, (R2AIArgs){ .input = a, .error = &error, .dorag = true });
|
||||
if (error) {
|
||||
R_LOG_ERROR ("%s", error);
|
||||
free (error);
|
||||
} else if (res) {
|
||||
char *res = ask (cps, r_strbuf_tostring (sb), false);
|
||||
if (res) {
|
||||
r_strbuf_appendf (sb, "Assistant: %s\n", res);
|
||||
if (r_config_get_b (core->config, "r2ai.clippy")) {
|
||||
char *cmd = r_str_newf ("?E %s", res);
|
||||
r_core_call (core, cmd);
|
||||
free (cmd);
|
||||
} else {
|
||||
r_cons_printf (core->cons, "%s\n", res);
|
||||
r2ai_print_response (core, res);
|
||||
}
|
||||
r_cons_flush (core->cons);
|
||||
}
|
||||
@@ -369,40 +377,30 @@ static void cmd_r2ai_repl(RCorePluginSession *cps) {
|
||||
r_config_hold_free (hold);
|
||||
}
|
||||
|
||||
static void cmd_r2ai_R(RCorePluginSession *cps, const char *q) {
|
||||
static void cmd_r2ai_R(RCorePluginSession *cps, const char *q, bool refresh) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
if (!r_config_get_b (core->config, "r2ai.data")) {
|
||||
if (refresh && !r_config_get_b (core->config, "r2ai.data")) {
|
||||
R_LOG_ERROR ("r2ai -e r2ai.data=true");
|
||||
return;
|
||||
}
|
||||
if (R_STR_ISEMPTY (q)) {
|
||||
if (state->db) {
|
||||
r_vdb_free (state->db);
|
||||
state->db = NULL;
|
||||
}
|
||||
if (refresh || !state->db) {
|
||||
r2ai_refresh_embeddings (cps);
|
||||
} else {
|
||||
r2ai_refresh_embeddings (cps);
|
||||
const int K = r_config_get_i (core->config, "r2ai.data.nth");
|
||||
RVdbResultSet *rs = r_vdb_query (state->db, q, K);
|
||||
|
||||
if (rs) {
|
||||
R_LOG_DEBUG ("Query: \"%s\"", q);
|
||||
R_LOG_DEBUG ("Found up to %d neighbors (actual found: %d)", K, rs->size);
|
||||
int i;
|
||||
for (i = 0; i < rs->size; i++) {
|
||||
RVdbResult *r = &rs->results[i];
|
||||
KDNode *n = r->node;
|
||||
float dist_sq = r->dist_sq;
|
||||
float cos_sim = 1.0f - (dist_sq * 0.5f); // for normalized vectors
|
||||
printf ("%2d) dist_sq=%.4f cos_sim=%.4f text=\"%s\"\n", i + 1, dist_sq, cos_sim, (n->text? n->text: "(null)"));
|
||||
}
|
||||
r_vdb_result_free (rs);
|
||||
} else {
|
||||
R_LOG_ERROR ("No vdb results found");
|
||||
}
|
||||
}
|
||||
if (R_STR_ISEMPTY (q)) {
|
||||
return;
|
||||
}
|
||||
RVdbResultSet *rs = r_vdb_query (state->db, q, r_config_get_i (core->config, "r2ai.data.nth"));
|
||||
if (!rs) {
|
||||
R_LOG_ERROR ("No vdb results found");
|
||||
return;
|
||||
}
|
||||
int i;
|
||||
for (i = 0; i < rs->size; i++) {
|
||||
RVdbResult *r = &rs->results[i];
|
||||
r_cons_printf (core->cons, "%2d) %.4f %s\n", i + 1, r->dist_sq, r->node->text);
|
||||
}
|
||||
r_vdb_result_free (rs);
|
||||
}
|
||||
|
||||
static void cmd_r2ai_i(RCorePluginSession *cps, const char *arg) {
|
||||
@@ -421,15 +419,9 @@ static void cmd_r2ai_i(RCorePluginSession *cps, const char *arg) {
|
||||
const char *prompt = R_STR_ISNOTEMPTY (query)? query: "Analyze the contents of the following file:";
|
||||
char *q = r_str_newf ("%s\n```\n%s\n```\n", prompt, s);
|
||||
free (s);
|
||||
char *error = NULL;
|
||||
char *res = r2ai (cps, (R2AIArgs){ .input = q, .error = &error, .dorag = true });
|
||||
char *res = ask (cps, q, true);
|
||||
free (q);
|
||||
if (error) {
|
||||
R_LOG_ERROR ("%s", error);
|
||||
free (error);
|
||||
} else {
|
||||
r_cons_printf (core->cons, "%s\n", res);
|
||||
}
|
||||
r2ai_print_response (core, res);
|
||||
free (fname);
|
||||
free (res);
|
||||
}
|
||||
@@ -443,41 +435,6 @@ static void cmd_r2ai_m(RCorePluginSession *cps, const char *input) {
|
||||
}
|
||||
}
|
||||
|
||||
static void load_embeddings(RCorePluginSession *cps) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
RListIter *iter, *iter2;
|
||||
char *line;
|
||||
char *file;
|
||||
// enumerate .txt files in directory
|
||||
const char *path = r_config_get (core->config, "r2ai.data.path");
|
||||
RList *files = r_sys_dir (path);
|
||||
if (r_list_empty (files)) {
|
||||
R_LOG_WARN ("Cannot find any file in r2ai.data.path");
|
||||
}
|
||||
r_list_foreach (files, iter, file) {
|
||||
if (!r_str_endswith (file, ".txt")) {
|
||||
continue;
|
||||
}
|
||||
R_LOG_DEBUG ("Index %s", file);
|
||||
char *filepath = r_file_new (path, file, NULL);
|
||||
char *text = r_file_slurp (filepath, NULL);
|
||||
if (text) {
|
||||
RList *lines = r_str_split_list (text, "\n", -1);
|
||||
r_list_foreach (lines, iter2, line) {
|
||||
if (r_str_trim_head_ro (line)[0] == 0) {
|
||||
continue;
|
||||
}
|
||||
r_vdb_insert (state->db, line);
|
||||
R_LOG_DEBUG ("Insert %s", line);
|
||||
}
|
||||
r_list_free (lines);
|
||||
}
|
||||
free (filepath);
|
||||
}
|
||||
r_list_free (files);
|
||||
}
|
||||
|
||||
static bool load_r2airc(RCorePluginSession *cps) {
|
||||
char *rc_path = r_file_home (".config/r2ai/rc");
|
||||
if (rc_path && r_file_exists (rc_path)) {
|
||||
@@ -491,12 +448,11 @@ static bool load_r2airc(RCorePluginSession *cps) {
|
||||
if (R_STR_ISEMPTY (trimmed) || *trimmed == '#') {
|
||||
continue;
|
||||
}
|
||||
if (r_str_startswith (trimmed, "r2ai -e ") || r_str_startswith (trimmed, "-e ")) {
|
||||
const char *arg = trimmed + (r_str_startswith (trimmed, "r2ai -e ")? 7: 3);
|
||||
cmd_r2ai (cps, r_str_newf ("-e%s", arg));
|
||||
} else if (r_str_startswith (trimmed, "r2ai -") || r_str_startswith (trimmed, "-")) {
|
||||
const char *arg = trimmed + (r_str_startswith (trimmed, "r2ai -")? 6: 1);
|
||||
cmd_r2ai (cps, r_str_newf ("-%s", arg));
|
||||
if (r_str_startswith (trimmed, "r2ai -")) {
|
||||
trimmed += strlen ("r2ai ");
|
||||
}
|
||||
if (*trimmed == '-') {
|
||||
cmd_r2ai (cps, trimmed);
|
||||
} else {
|
||||
R_LOG_ERROR ("Invalid line in r2ai rc: %s", trimmed);
|
||||
}
|
||||
@@ -526,14 +482,14 @@ R_API void cmd_r2ai(RCorePluginSession *cps, const char *input) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
if (*input == '?' || r_str_startswith (input, "-h")) {
|
||||
r_core_cmd_help (core, help_msg_r2ai);
|
||||
r2ai_cmd_help (core, help_msg_r2ai);
|
||||
} else if (r_str_startswith (input, "-w")) {
|
||||
r2ai_wizard (core);
|
||||
} else if (r_str_startswith (input, "-K")) {
|
||||
r2ai_apikeys_edit (cps);
|
||||
} else if (r_str_startswith (input, "-E")) {
|
||||
char *rc_path = r_file_home (".config/r2ai/rc");
|
||||
r_cons_editor (core->cons, rc_path, NULL);
|
||||
r2ai_cons_editor (core->cons, rc_path, NULL);
|
||||
free (rc_path);
|
||||
load_r2airc (cps);
|
||||
} else if (r_str_startswith (input, "-e")) {
|
||||
@@ -546,12 +502,9 @@ R_API void cmd_r2ai(RCorePluginSession *cps, const char *input) {
|
||||
} else if (r_str_startswith (input, "-a")) {
|
||||
const char *q = r_str_trim_head_ro (input + 2);
|
||||
if (r_config_get_b (core->config, "r2ai.async")) {
|
||||
char *system_prompt = r2ai_auto_system_prompt (cps);
|
||||
char *title = r_str_newf ("-a %s", q);
|
||||
int id = r2ai_async_auto (cps, title, q, system_prompt);
|
||||
r_cons_printf (core->cons, "[async] task %d queued (%s)\n", id, title);
|
||||
r2ai_async_auto (cps, title, q, NULL);
|
||||
free (title);
|
||||
free (system_prompt);
|
||||
} else {
|
||||
cmd_r2ai_a (cps, q);
|
||||
}
|
||||
@@ -587,36 +540,21 @@ R_API void cmd_r2ai(RCorePluginSession *cps, const char *input) {
|
||||
} else if (r_str_startswith (input, "-d")) {
|
||||
cmd_r2ai_d (cps, r_str_trim_head_ro (input + 2), false, false);
|
||||
} else if (r_str_startswith (input, "-S")) {
|
||||
if (state->db == NULL) {
|
||||
state->db = r_vdb_new (R2AI_DEFAULT_VECTORS);
|
||||
load_embeddings (cps);
|
||||
}
|
||||
const char *arg = r_str_trim_head_ro (input + 2);
|
||||
const int K = 10;
|
||||
RVdbResultSet *rs = r_vdb_query (state->db, arg, K);
|
||||
if (rs) {
|
||||
int i;
|
||||
eprintf ("Found up to %d neighbors (actual found: %d).\n", K, rs->size);
|
||||
for (i = 0; i < rs->size; i++) {
|
||||
RVdbResult *r = &rs->results[i];
|
||||
KDNode *n = r->node;
|
||||
r_cons_printf (core->cons, "- (%.4f) %s\n", r->dist_sq, n->text);
|
||||
}
|
||||
r_vdb_result_free (rs);
|
||||
}
|
||||
cmd_r2ai_R (cps, r_str_trim_head_ro (input + 2), false);
|
||||
} else if (cmd_r2ai_id (cps, input)) {
|
||||
} else if (r_str_startswith (input, "-i")) {
|
||||
cmd_r2ai_i (cps, r_str_trim_head_ro (input + 2));
|
||||
} else if (r_str_startswith (input, "-r")) {
|
||||
cmd_r2ai_repl (cps);
|
||||
} else if (r_str_startswith (input, "-Rq")) {
|
||||
cmd_r2ai_R (cps, r_str_trim_head_ro (input + 3));
|
||||
cmd_r2ai_R (cps, r_str_trim_head_ro (input + 3), true);
|
||||
} else if (r_str_startswith (input, "-R")) {
|
||||
RList *messages = r2ai_conversation_get (state);
|
||||
if (!messages || r_list_empty (messages)) {
|
||||
r_cons_printf (core->cons, "No conversation history to reset\n");
|
||||
} else {
|
||||
r2ai_msgs_clear (messages);
|
||||
R_FREE (state->cache_prefix);
|
||||
r_cons_printf (core->cons, "Chat conversation context has been reset\n");
|
||||
}
|
||||
} else if (r_str_startswith (input, "-m")) {
|
||||
@@ -633,24 +571,14 @@ R_API void cmd_r2ai(RCorePluginSession *cps, const char *input) {
|
||||
} else if (r_str_startswith (input, "-q")) {
|
||||
r2ai_cmd_q (cps, r_str_trim_head_ro (input + 2));
|
||||
} else if (r_str_startswith (input, "-")) {
|
||||
r_core_cmd_help (core, help_msg_r2ai);
|
||||
r2ai_cmd_help (core, help_msg_r2ai);
|
||||
} else {
|
||||
if (r_config_get_b (core->config, "r2ai.async")) {
|
||||
const char *sys = r_config_get (core->config, "r2ai.system");
|
||||
int id = r2ai_async_query (cps, input, input, sys);
|
||||
r_cons_printf (core->cons, "[async] task %d queued\n", id);
|
||||
r2ai_async_query (cps, input, input, NULL);
|
||||
} else {
|
||||
char *err = NULL;
|
||||
char *res =
|
||||
r2ai (cps, (R2AIArgs){ .input = input, .error = &err, .dorag = true });
|
||||
if (err) {
|
||||
R_LOG_ERROR ("%s", err);
|
||||
R_FREE (err);
|
||||
}
|
||||
if (res) {
|
||||
r_cons_printf (core->cons, "%s\n", res);
|
||||
free (res);
|
||||
}
|
||||
char *res = ask (cps, input, true);
|
||||
r2ai_print_response (core, res);
|
||||
free (res);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -753,6 +681,21 @@ static bool cb_r2ai_http_backend(void *user, void *data) {
|
||||
// return false; // always return false to test
|
||||
}
|
||||
|
||||
static bool cb_r2ai_apitype(void *user, void *data) {
|
||||
RCore *core = (RCore *)user;
|
||||
RConfigNode *node = (RConfigNode *)data;
|
||||
if (*node->value == '?') {
|
||||
r_cons_println (core->cons, "chat");
|
||||
r_cons_println (core->cons, "generate");
|
||||
return false;
|
||||
}
|
||||
if (!strcmp (node->value, "chat") || !strcmp (node->value, "generate")) {
|
||||
return true;
|
||||
}
|
||||
R_LOG_ERROR ("Invalid apitype '%s'. Use chat or generate.", node->value);
|
||||
return false;
|
||||
}
|
||||
|
||||
R_IPI bool r2ai_init(RCorePluginSession *cps) {
|
||||
RCore *core = cps->core;
|
||||
// Initialize state
|
||||
@@ -779,13 +722,19 @@ R_IPI bool r2ai_init(RCorePluginSession *cps) {
|
||||
r_config_set_cb (core->config, "r2ai.model", R2AI_DEFAULT_MODEL, &cb_r2ai_model);
|
||||
r_config_desc (core->config, "r2ai.model", "Model identifier for the selected provider (e.g. gpt-5-mini)");
|
||||
r_config_set (core->config, "r2ai.baseurl", "");
|
||||
r_config_desc (core->config, "r2ai.baseurl", "Base URL for provider API (overrides default endpoints)");
|
||||
r_config_desc (core->config, "r2ai.baseurl", "Provider API root (/v1 for OpenAI-compatible chat or /api for native Ollama)");
|
||||
r_config_set_cb (core->config, "r2ai.apitype", "chat", &cb_r2ai_apitype);
|
||||
r_config_desc (core->config, "r2ai.apitype", "Chat completions (default) or native Ollama generate operation");
|
||||
r_config_set_b (core->config, "r2ai.cacheck", false);
|
||||
r_config_desc (core->config, "r2ai.cacheck", "Warn when chat requests are not append-only and may miss provider prompt-cache hits");
|
||||
r_config_set_i (core->config, "r2ai.max_tokens", 4096); // max output tokens, or max total tokens
|
||||
r_config_desc (core->config, "r2ai.max_tokens", "Maximum tokens for LLM responses (output/total depending on provider)");
|
||||
r_config_set_i (core->config, "r2ai.thinking_tokens", 0);
|
||||
r_config_desc (core->config, "r2ai.thinking_tokens", "Number of tokens reserved for internal thinking/context messages");
|
||||
r_config_set (core->config, "r2ai.temperature", "0.01");
|
||||
r_config_desc (core->config, "r2ai.temperature", "Sampling temperature for LLM output (0 = deterministic)");
|
||||
r_config_set_b (core->config, "r2ai.deterministic", false);
|
||||
r_config_desc (core->config, "r2ai.deterministic", "Remove randomness from responses by forcing greedy sampling parameters (overrides r2ai.temperature)");
|
||||
r_config_set (core->config, "r2ai.cmds", "pdc");
|
||||
r_config_desc (core->config, "r2ai.cmds", "Default command sequence used by automation (e.g. 'pdc')");
|
||||
r_config_set (core->config, "r2ai.lang", "C");
|
||||
@@ -818,6 +767,8 @@ R_IPI bool r2ai_init(RCorePluginSession *cps) {
|
||||
r_config_desc (core->config, "r2ai.clippy", "Responses from the llm will be displayed by clippy");
|
||||
r_config_set_b (core->config, "r2ai.wizard", true);
|
||||
r_config_desc (core->config, "r2ai.wizard", "Run the setup wizard automatically on the first interactive chat session");
|
||||
r_config_set_b (core->config, "r2ai.markdown", true);
|
||||
r_config_desc (core->config, "r2ai.markdown", "Render LLM responses using the radare2 markdown colorizer");
|
||||
r_config_set_b (core->config, "r2ai.stream", false);
|
||||
r_config_desc (core->config, "r2ai.stream", "Enable streaming responses from the LLM (true/false)");
|
||||
r_config_set_i (core->config, "r2ai.auto.max_runs", 50);
|
||||
@@ -828,6 +779,8 @@ R_IPI bool r2ai_init(RCorePluginSession *cps) {
|
||||
r_config_desc (core->config, "r2ai.auto.init_commands", "Initial commands executed when auto mode starts (semicolon separated)");
|
||||
r_config_set_b (core->config, "r2ai.auto.yolo", false);
|
||||
r_config_desc (core->config, "r2ai.auto.yolo", "Execute potentially dangerous commands in auto mode without asking");
|
||||
r_config_set_b (core->config, "r2ai.history", false);
|
||||
r_config_desc (core->config, "r2ai.history", "Keep direct queries (r2ai [query], -d, -i) in the conversation context (see -L, -R)");
|
||||
r_config_set_b (core->config, "r2ai.auto.reset_on_query", false);
|
||||
r_config_desc (core->config, "r2ai.auto.reset_on_query", "Reset auto-mode conversation state on new user queries");
|
||||
r_config_set_b (core->config, "r2ai.auto.think", true);
|
||||
@@ -872,12 +825,16 @@ R_API bool r2ai_fini(RCorePluginSession *cps) {
|
||||
r_config_rm (core->config, "r2ai.api");
|
||||
r_config_rm (core->config, "r2ai.cmds");
|
||||
r_config_rm (core->config, "r2ai.model");
|
||||
r_config_rm (core->config, "r2ai.apitype");
|
||||
r_config_rm (core->config, "r2ai.cacheck");
|
||||
r_config_rm (core->config, "r2ai.prompt");
|
||||
r_config_rm (core->config, "r2ai.markdown");
|
||||
r_config_rm (core->config, "r2ai.stream");
|
||||
r_config_rm (core->config, "r2ai.system");
|
||||
r_config_rm (core->config, "r2ai.data");
|
||||
r_config_rm (core->config, "r2ai.data.path");
|
||||
r_config_rm (core->config, "r2ai.data.nth");
|
||||
r_config_rm (core->config, "r2ai.history");
|
||||
r_config_rm (core->config, "r2ai.auto.reset_on_query");
|
||||
r_config_rm (core->config, "r2ai.http.timeout");
|
||||
r_config_rm (core->config, "r2ai.http.max_retries");
|
||||
@@ -901,6 +858,7 @@ R_API bool r2ai_fini(RCorePluginSession *cps) {
|
||||
state->prompt_auto = NULL;
|
||||
free (state->vertex_token);
|
||||
state->vertex_token = NULL;
|
||||
R_FREE (state->cache_prefix);
|
||||
free (state);
|
||||
cps->data = NULL;
|
||||
}
|
||||
|
||||
+18
-15
@@ -2,7 +2,6 @@
|
||||
#define R2AI_H
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
|
||||
#include <time.h>
|
||||
#include <stdint.h>
|
||||
@@ -15,11 +14,21 @@
|
||||
#include "r_vdb.h"
|
||||
#include "markdown.h"
|
||||
|
||||
#ifndef R_LIB_ABIVERSION
|
||||
#define R_LIB_ABIVERSION R2_ABIVERSION
|
||||
#endif
|
||||
|
||||
#if R_LIB_ABIVERSION >= 125
|
||||
#define r2ai_cmd_help(core,msg) r_cons_cmd_help ((core)->cons, msg)
|
||||
#else
|
||||
#define r2ai_cmd_help(core,msg) r_core_cmd_help (core, msg)
|
||||
#endif
|
||||
|
||||
#if R2_ABIVERSION < 83
|
||||
#define r_core_call(x,y) r_core_cmd_call(x,y)
|
||||
#endif
|
||||
|
||||
#define R2AI_VERSION "1.4.0"
|
||||
#define R2AI_VERSION "1.5.4"
|
||||
#define R2AI_DEFAULT_MODEL "gpt-oss:20b"
|
||||
#define R2AI_DEFAULT_PROVIDER "ollama"
|
||||
|
||||
@@ -91,11 +100,6 @@ typedef struct {
|
||||
*/
|
||||
R_API RList *r2ai_content_blocks_new(void);
|
||||
|
||||
/**
|
||||
* Free a content blocks RList
|
||||
*/
|
||||
R_API void r2ai_content_blocks_free(RList *cb);
|
||||
|
||||
typedef struct {
|
||||
char *role;
|
||||
char *content;
|
||||
@@ -142,6 +146,7 @@ typedef struct {
|
||||
int max_tokens;
|
||||
int thinking_tokens;
|
||||
float temperature;
|
||||
bool deterministic; // greedy sampling to remove randomness from responses
|
||||
bool dorag;
|
||||
char **error;
|
||||
} R2AIArgs;
|
||||
@@ -173,6 +178,7 @@ typedef struct r2ai_state_t {
|
||||
char *prompt_auto; // Auto-mode system prompt
|
||||
char *vertex_token; // Cached Vertex AI OAuth2 token
|
||||
ut64 vertex_token_expiry; // Monotonic microseconds when the cached token expires
|
||||
char *cache_prefix; // Last chat request prefix used by r2ai.cacheck
|
||||
} R2AI_State;
|
||||
|
||||
// conversation
|
||||
@@ -184,13 +190,11 @@ R_API void r2ai_conversation_free(R2AI_State *state);
|
||||
R_API RList *r2ai_msgs_new(void);
|
||||
R_API void r2ai_msgs_clear(RList *msgs);
|
||||
R_API bool r2ai_msgs_add(RList *msgs, const R2AI_Message *msg);
|
||||
R_API bool r2ai_msgs_add_tool_call(RList *msgs, const R2AI_ToolCall *tc);
|
||||
R_API bool r2ai_msgs_from_response(RList *msgs, const char *json_str);
|
||||
R_API bool r2ai_msgs_from_json(RList *msgs, const RJson *json);
|
||||
R_API char *r2ai_msgs_to_json(const RList *msgs, bool raw_tool_args);
|
||||
R_API char *r2ai_msgs_to_anthropic_json(const RList *msgs);
|
||||
R_API void r2ai_msgs_free(RList *msgs);
|
||||
R_API void r2ai_message_free(R2AI_Message *msg);
|
||||
R_IPI void r2ai_chat_response_free(R2AI_ChatResponse *res);
|
||||
R_API void r2ai_message_fini(R2AI_Message *msg);
|
||||
R_API void r2ai_delete_last_messages(RList *messages, int n);
|
||||
|
||||
@@ -240,13 +244,13 @@ R_API char *r2ai_http_get(RCore *core, const char *url, const char **headers, in
|
||||
*
|
||||
* @param core RCore instance for configuration
|
||||
* @param provider The provider name (e.g., "openai", "anthropic", etc.)
|
||||
* @return Base URL for the provider, or NULL if unknown
|
||||
* @return Newly allocated base URL for the provider, or NULL if unknown
|
||||
*/
|
||||
R_IPI const char *r2ai_get_provider_url(RCore *core, const char *provider);
|
||||
R_IPI char *r2ai_get_provider_url(RCore *core, const char *provider);
|
||||
|
||||
// anthropic
|
||||
R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args);
|
||||
R_IPI R2AI_ChatResponse *r2ai_anthropic_parse_response(const char *json, char **error);
|
||||
R_IPI R2AI_ChatResponse *r2ai_anthropic_parse_response(char *json, char **error);
|
||||
|
||||
// openai
|
||||
R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args);
|
||||
@@ -257,6 +261,7 @@ R_IPI R2AI_ChatResponse *r2ai_gemini(RCorePluginSession *cps, R2AIArgs args);
|
||||
// auto mode
|
||||
R_IPI void cmd_r2ai_a(RCorePluginSession *cps, const char *user_query);
|
||||
R_IPI char *r2ai_auto_system_prompt(RCorePluginSession *cps);
|
||||
R_IPI void r2ai_print_response(RCore *core, const char *text);
|
||||
// R_API char *r2ai(RCore *core, R2AI_State *state, R2AIArgs args);
|
||||
R_API char *r2ai(RCorePluginSession *cps, R2AIArgs args);
|
||||
R_API bool r2ai_init(RCorePluginSession *cps);
|
||||
@@ -264,7 +269,6 @@ R_API bool r2ai_fini(RCorePluginSession *cps);
|
||||
|
||||
R_IPI R2AI_ChatResponse *r2ai_llmcall(RCorePluginSession *cps, R2AIArgs args);
|
||||
|
||||
R2AI_ChatResponse *r2ai_rawtools_llmcall(RCorePluginSession *cps, R2AIArgs args);
|
||||
|
||||
R_IPI void cmd_r2ai_c(RCorePluginSession *cps);
|
||||
R_IPI void cmd_r2ai_logs(RCorePluginSession *cps, const char *flags);
|
||||
@@ -277,7 +281,6 @@ R_API void process_messages(RCorePluginSession *cps, RList *messages, const char
|
||||
|
||||
// json helpers that must be moved into r2
|
||||
R_API char *r_json_to_string(const RJson *json);
|
||||
R_API PJ *r_json_to_pj(const RJson *json, PJ *existing_pj);
|
||||
|
||||
// commands
|
||||
R_API void r2ai_cmd_q(RCorePluginSession *cps, const char *input);
|
||||
|
||||
+20
-1
@@ -39,6 +39,8 @@ typedef struct r2ai_task_t {
|
||||
char *model;
|
||||
char *provider;
|
||||
RList *messages; /* R2AI_Message * - owned conversation */
|
||||
RList *tools; /* R2AI_Tool * - snapshot taken on the main thread, items not owned */
|
||||
int max_runs;
|
||||
RStrBuf *output; /* flushed by -si */
|
||||
char *error;
|
||||
char *pending_tool_name;
|
||||
@@ -50,6 +52,7 @@ typedef struct r2ai_task_t {
|
||||
RThreadLock *lock;
|
||||
RThreadSemaphore *gate;
|
||||
RCorePluginSession *cps;
|
||||
RCore *llm_core; /* shallow core view with an owned request config snapshot */
|
||||
time_t created;
|
||||
time_t started;
|
||||
time_t finished;
|
||||
@@ -79,8 +82,24 @@ typedef struct {
|
||||
} R2AIProvider;
|
||||
|
||||
R_API void cmd_r2ai(RCorePluginSession *cps, const char *input);
|
||||
static inline char *r2ai_cons_editor(RCons *cons, const char *file, const char *str) {
|
||||
#if R2_ABIVERSION >= 160
|
||||
return r_cons_editor (cons, file, str, 0, NULL);
|
||||
#elif R2_ABIVERSION >= 120
|
||||
return r_cons_editor (cons, file, str, NULL);
|
||||
#else
|
||||
return r_cons_editor (cons, file, str);
|
||||
#endif
|
||||
}
|
||||
R_IPI const R2AIProvider *r2ai_get_provider(const char *name);
|
||||
R_IPI const char *r2ai_get_provider_url(RCore *core, const char *provider);
|
||||
R_IPI const char *r2ai_system_prompt(RCore *core, const char *sysp);
|
||||
R_IPI R2AI_ChatResponse *r2ai_send(RCorePluginSession *cps, const R2AIProvider *p, R2AIArgs args);
|
||||
R_IPI char *r2ai_post(RCore *core, const char *name, const char *url, const char **headers, const char *data, int *code, char **error);
|
||||
R_IPI char *r2ai_gemini_request(const R2AIArgs *args);
|
||||
R_IPI R2AI_ChatResponse *r2ai_gemini_parse(char *json, char **error);
|
||||
R_IPI char *r2ai_anthropic_request(const R2AIArgs *args, bool vertex);
|
||||
R_IPI R2AI_ChatResponse *r2ai_rawtools_llmcall(RCorePluginSession *cps, const R2AIProvider *p, R2AIArgs args);
|
||||
R_IPI char *r2ai_get_provider_url(RCore *core, const char *provider);
|
||||
R_IPI RList *r2ai_fetch_available_models(RCore *core, const char *provider);
|
||||
R_IPI void r2ai_list_providers(RCore *core, RStrBuf *sb);
|
||||
R_IPI void r2ai_refresh_embeddings(RCorePluginSession *cps);
|
||||
|
||||
+49
-111
@@ -76,15 +76,8 @@ static bool parse_raw_tool_call(const char *response, char **tool_name, char **t
|
||||
}
|
||||
|
||||
// Function to handle rawtools mode in LLM call
|
||||
R2AI_ChatResponse *r2ai_rawtools_llmcall(RCorePluginSession *cps, R2AIArgs args) {
|
||||
if (!cps) {
|
||||
return NULL;
|
||||
}
|
||||
RCore *core = cps->core;
|
||||
|
||||
const char *user_system = R_STR_ISNOTEMPTY (args.system_prompt)
|
||||
? args.system_prompt
|
||||
: r_config_get (core->config, "r2ai.system");
|
||||
R_IPI R2AI_ChatResponse *r2ai_rawtools_llmcall(RCorePluginSession *cps, const R2AIProvider *p, R2AIArgs args) {
|
||||
const char *user_system = args.system_prompt;
|
||||
if (R_STR_ISEMPTY (user_system)) {
|
||||
user_system = "You are a reverse engineer using radare2. The binary is loaded. Use r2cmd tool for analysis.";
|
||||
}
|
||||
@@ -110,26 +103,12 @@ R2AI_ChatResponse *r2ai_rawtools_llmcall(RCorePluginSession *cps, R2AIArgs args)
|
||||
free (init_output);
|
||||
char *enhanced_system_prompt = r2ai_claw_system_prompt (base_prompt);
|
||||
free (base_prompt);
|
||||
// Temporarily modify args to use enhanced prompt and no tools (since we're using prompt engineering)
|
||||
|
||||
R2AIArgs rawtools_args = args;
|
||||
rawtools_args.system_prompt = enhanced_system_prompt;
|
||||
rawtools_args.tools = NULL; // Disable native tool calling
|
||||
|
||||
// Make the LLM call directly to provider
|
||||
const char *provider = rawtools_args.provider? rawtools_args.provider: r_config_get (core->config, "r2ai.api");
|
||||
if (!provider) {
|
||||
R_LOG_ERROR ("No provider defined");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
R2AI_ChatResponse *response = NULL;
|
||||
const R2AIProvider *p = r2ai_get_provider (provider);
|
||||
if (p && p->api_type == R2AI_API_ANTHROPIC) {
|
||||
response = r2ai_anthropic (cps, rawtools_args);
|
||||
} else {
|
||||
response = r2ai_openai (cps, rawtools_args);
|
||||
}
|
||||
rawtools_args.tools = NULL;
|
||||
|
||||
R2AI_ChatResponse *response = r2ai_send (cps, p, rawtools_args);
|
||||
free (enhanced_system_prompt);
|
||||
|
||||
if (!response || !response->message || !response->message->content) {
|
||||
@@ -138,14 +117,12 @@ R2AI_ChatResponse *r2ai_rawtools_llmcall(RCorePluginSession *cps, R2AIArgs args)
|
||||
|
||||
R_LOG_DEBUG ("[RAWTOOLS] Raw model response: %s", response->message->content);
|
||||
|
||||
// Check if the response contains a raw tool call
|
||||
char *tool_name = NULL;
|
||||
char *tool_args = NULL;
|
||||
|
||||
if (parse_raw_tool_call (response->message->content, &tool_name, &tool_args)) {
|
||||
R_LOG_DEBUG ("Raw tool call detected: %s with args: %s", tool_name, tool_args);
|
||||
|
||||
// Find the tool in our tools list
|
||||
RList *tools = r2ai_get_tools (cps->core, cps->data);
|
||||
bool tool_found = false;
|
||||
|
||||
@@ -160,67 +137,55 @@ R2AI_ChatResponse *r2ai_rawtools_llmcall(RCorePluginSession *cps, R2AIArgs args)
|
||||
}
|
||||
}
|
||||
|
||||
R2AI_Message *modified_message = R_NEW0 (R2AI_Message);
|
||||
if (!modified_message) {
|
||||
free (tool_name);
|
||||
free (tool_args);
|
||||
return response;
|
||||
}
|
||||
modified_message->role = strdup ("assistant");
|
||||
|
||||
if (tool_found) {
|
||||
// Modify the existing response to include tool call information
|
||||
R2AI_Message *modified_message = R_NEW0 (R2AI_Message);
|
||||
modified_message->role = strdup ("assistant");
|
||||
// Remove the TOOL line from content, or set to empty if that's all there is
|
||||
char *content = strdup (response->message->content? response->message->content: "");
|
||||
char *tool_line = strstr (content, "TOOL: ");
|
||||
if (tool_line) {
|
||||
// Find the end of the line
|
||||
char *line_end = strchr (tool_line, '\n');
|
||||
if (line_end) {
|
||||
// Remove the TOOL line
|
||||
memmove (tool_line, line_end + 1, strlen (line_end + 1) + 1);
|
||||
} else {
|
||||
// The TOOL line is the entire content
|
||||
*tool_line = '\0';
|
||||
if (content) {
|
||||
char *tool_line = strstr (content, "TOOL: ");
|
||||
if (tool_line) {
|
||||
char *line_end = strchr (tool_line, '\n');
|
||||
if (line_end) {
|
||||
memmove (tool_line, line_end + 1, strlen (line_end + 1) + 1);
|
||||
} else {
|
||||
*tool_line = '\0';
|
||||
}
|
||||
}
|
||||
modified_message->content = content;
|
||||
}
|
||||
modified_message->content = content;
|
||||
|
||||
// Set up proper tool call structure
|
||||
modified_message->tool_calls = r_list_new ();
|
||||
if (modified_message->tool_calls) {
|
||||
modified_message->tool_calls->free = (RListFree)r2ai_tool_call_free;
|
||||
R2AI_ToolCall *tc = R_NEW0 (R2AI_ToolCall);
|
||||
tc->name = tool_name;
|
||||
tc->arguments = tool_args;
|
||||
char id_buf[32];
|
||||
snprintf (id_buf, sizeof (id_buf), "rawtool_%d", (int)time (NULL));
|
||||
tc->id = strdup (id_buf);
|
||||
r_list_append (modified_message->tool_calls, tc);
|
||||
if (tc) {
|
||||
char id_buf[32];
|
||||
snprintf (id_buf, sizeof (id_buf), "rawtool_%d", (int)time (NULL));
|
||||
tc->name = tool_name;
|
||||
tc->arguments = tool_args;
|
||||
tc->id = strdup (id_buf);
|
||||
r_list_append (modified_message->tool_calls, tc);
|
||||
tool_name = NULL;
|
||||
tool_args = NULL;
|
||||
}
|
||||
}
|
||||
tool_name = NULL;
|
||||
tool_args = NULL;
|
||||
|
||||
// Replace the response message
|
||||
if (response->message) {
|
||||
r2ai_message_free ((R2AI_Message *)response->message);
|
||||
}
|
||||
*(R2AI_Message **)&response->message = modified_message;
|
||||
} else {
|
||||
// Unknown tool, modify content to indicate error
|
||||
R2AI_Message *modified_message = R_NEW0 (R2AI_Message);
|
||||
modified_message->role = strdup ("assistant");
|
||||
size_t result_len = strlen ("Unknown tool: ") + strlen (tool_name) + 1;
|
||||
modified_message->content = malloc (result_len);
|
||||
if (modified_message->content) {
|
||||
snprintf (modified_message->content, result_len, "Unknown tool: %s", tool_name);
|
||||
}
|
||||
|
||||
// Replace the response message
|
||||
if (response->message) {
|
||||
r2ai_message_free ((R2AI_Message *)response->message);
|
||||
}
|
||||
*(R2AI_Message **)&response->message = modified_message;
|
||||
free (tool_name);
|
||||
free (tool_args);
|
||||
tool_name = NULL;
|
||||
tool_args = NULL;
|
||||
char *errmsg = r_str_newf ("Unknown tool: %s", tool_name);
|
||||
modified_message->content = errmsg;
|
||||
}
|
||||
|
||||
if (response->message) {
|
||||
r2ai_message_free ((R2AI_Message *)response->message);
|
||||
}
|
||||
*(R2AI_Message **)&response->message = modified_message;
|
||||
free (tool_name);
|
||||
free (tool_args);
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -228,53 +193,26 @@ R2AI_ChatResponse *r2ai_rawtools_llmcall(RCorePluginSession *cps, R2AIArgs args)
|
||||
free (tool_name);
|
||||
free (tool_args);
|
||||
|
||||
// If the response has no content, try again with normal mode (without rawtools prompt)
|
||||
if (!response->message || !response->message->content || !*response->message->content) {
|
||||
R_LOG_DEBUG ("No tool call found and no content, falling back to normal mode");
|
||||
|
||||
// Free the current response
|
||||
if (response->message) {
|
||||
r2ai_message_free ((R2AI_Message *)response->message);
|
||||
}
|
||||
free (response);
|
||||
r2ai_chat_response_free (response);
|
||||
|
||||
// Modify args to use original system prompt without rawtools enhancement
|
||||
R2AIArgs fallback_args = args;
|
||||
fallback_args.tools = args.tools; // Keep tools for fallback
|
||||
|
||||
// Call provider directly
|
||||
const char *provider = fallback_args.provider? fallback_args.provider: r_config_get (core->config, "r2ai.api");
|
||||
if (!provider) {
|
||||
R_LOG_ERROR ("No provider defined");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
R2AI_ChatResponse *fallback_response = NULL;
|
||||
const R2AIProvider *p = r2ai_get_provider (provider);
|
||||
if (p && p->api_type == R2AI_API_ANTHROPIC) {
|
||||
fallback_response = r2ai_anthropic (cps, fallback_args);
|
||||
} else {
|
||||
fallback_response = r2ai_openai (cps, fallback_args);
|
||||
}
|
||||
R2AI_ChatResponse *fallback_response = r2ai_send (cps, p, args);
|
||||
|
||||
if (fallback_response && fallback_response->message && fallback_response->message->content && *fallback_response->message->content) {
|
||||
return fallback_response;
|
||||
}
|
||||
|
||||
// If still no content, create a response with warning
|
||||
if (fallback_response) {
|
||||
if (fallback_response->message) {
|
||||
r2ai_message_free ((R2AI_Message *)fallback_response->message);
|
||||
}
|
||||
free (fallback_response);
|
||||
}
|
||||
r2ai_chat_response_free (fallback_response);
|
||||
|
||||
// Create a warning response
|
||||
R2AI_ChatResponse *warning_response = R_NEW0 (R2AI_ChatResponse);
|
||||
R2AI_Message *msg = R_NEW0 (R2AI_Message);
|
||||
msg->role = strdup ("assistant");
|
||||
msg->content = strdup ("Warning: LLM provided no response content");
|
||||
*((R2AI_Message **)&warning_response->message) = msg;
|
||||
if (msg) {
|
||||
msg->role = strdup ("assistant");
|
||||
msg->content = strdup ("Warning: LLM provided no response content");
|
||||
*((R2AI_Message **)&warning_response->message) = msg;
|
||||
}
|
||||
R_LOG_WARN ("LLM provided no response content");
|
||||
return warning_response;
|
||||
}
|
||||
|
||||
+36
-72
@@ -1,6 +1,6 @@
|
||||
/* Copyright r2ai - 2023-2026 - pancake */
|
||||
|
||||
#include "r2ai.h"
|
||||
#include "r2ai_priv.h"
|
||||
|
||||
R_API char *strip_command_comment(const char *input, char **comment_out) {
|
||||
const char *hash_pos = strchr (input, '#');
|
||||
@@ -280,6 +280,24 @@ R_API void r2ai_tool_result_fini(R2AI_ToolResult *result) {
|
||||
memset (result, 0, sizeof (*result));
|
||||
}
|
||||
|
||||
// let the user review or edit the text before running it, returns NULL when cancelled
|
||||
static char *confirm(RCore *core, const char *text) {
|
||||
RCons *cons = core->cons;
|
||||
if (strchr (text, '\n')) {
|
||||
char *edited = r2ai_cons_editor (cons, NULL, text);
|
||||
return edited? edited: strdup (text);
|
||||
}
|
||||
r_cons_newline (cons);
|
||||
r_cons_readpush (cons, text, strlen (text));
|
||||
r_cons_readpush (cons, "\x05", 1); // ctrl+e moves the cursor to the end
|
||||
r_line_set_prompt (cons->line, "[r2ai]> ");
|
||||
const char *line = r_line_readline (cons);
|
||||
if (r_cons_is_breaked (cons) || R_STR_ISEMPTY (line)) {
|
||||
return NULL;
|
||||
}
|
||||
return strdup (line);
|
||||
}
|
||||
|
||||
static R2AI_ToolResult r2ai_r2cmd(RCore *core, RJson *args, bool verbose) {
|
||||
R2AI_ToolResult result = { 0 };
|
||||
if (!args) {
|
||||
@@ -308,41 +326,19 @@ static R2AI_ToolResult r2ai_r2cmd(RCore *core, RJson *args, bool verbose) {
|
||||
return result;
|
||||
}
|
||||
|
||||
bool ask_to_execute = r_config_get_b (core->config, "r2ai.auto.yolo") != true;
|
||||
if (ask_to_execute) {
|
||||
char *input_command = compose_command_with_comment (command, comment);
|
||||
if (!input_command) {
|
||||
if (!r_config_get_b (core->config, "r2ai.auto.yolo")) {
|
||||
char *input = compose_command_with_comment (command, comment);
|
||||
char *edited = confirm (core, input);
|
||||
free (input);
|
||||
if (!edited) {
|
||||
free (command);
|
||||
free (comment);
|
||||
result.output = strdup ("{ \"res\":\"Failed to prepare command\" }");
|
||||
result.output = strdup ("R2AI_SIGINT");
|
||||
return result;
|
||||
}
|
||||
bool is_multiline = strchr (input_command, '\n') != NULL;
|
||||
|
||||
if (is_multiline) {
|
||||
r_cons_editor (core->cons, NULL, input_command);
|
||||
} else {
|
||||
r_cons_newline (core->cons);
|
||||
r_cons_readpush (core->cons, input_command, strlen (input_command));
|
||||
r_cons_readpush (core->cons, "\x05", 1);
|
||||
r_line_set_prompt (core->cons->line, "[r2ai]> ");
|
||||
const char *readline_result = r_line_readline (core->cons);
|
||||
if (r_cons_is_breaked (core->cons) || R_STR_ISEMPTY (readline_result)) {
|
||||
R_LOG_INFO ("Command execution cancelled %s", readline_result);
|
||||
free (input_command);
|
||||
free (command);
|
||||
free (comment);
|
||||
result.output = strdup ("R2AI_SIGINT");
|
||||
return result;
|
||||
}
|
||||
if (R_STR_ISNOTEMPTY (readline_result)) {
|
||||
free (input_command);
|
||||
input_command = strdup (readline_result);
|
||||
}
|
||||
}
|
||||
|
||||
char *new_comment = NULL;
|
||||
char *new_command = strip_command_comment (input_command, &new_comment);
|
||||
char *new_command = strip_command_comment (edited, &new_comment);
|
||||
free (edited);
|
||||
if (new_command) {
|
||||
free (command);
|
||||
command = new_command;
|
||||
@@ -351,7 +347,6 @@ static R2AI_ToolResult r2ai_r2cmd(RCore *core, RJson *args, bool verbose) {
|
||||
} else {
|
||||
free (new_comment);
|
||||
}
|
||||
free (input_command);
|
||||
}
|
||||
|
||||
result.edited_command = strdup (command);
|
||||
@@ -437,44 +432,15 @@ static R2AI_ToolResult r2ai_qjs(RCore *core, R2AI_State *state, RJson *args, boo
|
||||
return result;
|
||||
}
|
||||
|
||||
bool ask_to_execute = r_config_get_b (core->config, "r2ai.auto.yolo") != true;
|
||||
const char *script = script_json->str_value;
|
||||
char *edited_script = NULL;
|
||||
|
||||
if (ask_to_execute) {
|
||||
// Check if script contains newlines to determine if it's multi-line
|
||||
bool is_multiline = strchr (script, '\n') != NULL;
|
||||
|
||||
if (is_multiline) {
|
||||
// Use editor for multi-line scripts
|
||||
edited_script = strdup (script);
|
||||
r_cons_editor (core->cons, NULL, edited_script);
|
||||
script = edited_script;
|
||||
} else {
|
||||
// For single-line scripts, push the script to input buffer
|
||||
|
||||
r_cons_readpush (core->cons, script, strlen (script));
|
||||
r_cons_readpush (core->cons, "\x05", 1); // Ctrl+E - move to end
|
||||
r_line_set_prompt (core->cons->line, "[r2ai]> ");
|
||||
const char *readline_result = r_line_readline (core->cons);
|
||||
|
||||
// Check if interrupted or ESC pressed (readline_result is NULL or empty)
|
||||
if (r_cons_is_breaked (core->cons) || R_STR_ISEMPTY (readline_result)) {
|
||||
free (edited_script); // Free if already allocated
|
||||
result.output = strdup ("R2AI_SIGINT");
|
||||
return result;
|
||||
}
|
||||
|
||||
// Process the result
|
||||
if (readline_result && *readline_result) {
|
||||
edited_script = strdup (readline_result);
|
||||
script = edited_script;
|
||||
} else {
|
||||
// If user just pressed enter, keep the original script
|
||||
edited_script = strdup (script);
|
||||
script = edited_script;
|
||||
}
|
||||
char *script = strdup (script_json->str_value);
|
||||
if (!r_config_get_b (core->config, "r2ai.auto.yolo")) {
|
||||
char *edited = confirm (core, script);
|
||||
free (script);
|
||||
if (!edited) {
|
||||
result.output = strdup ("R2AI_SIGINT");
|
||||
return result;
|
||||
}
|
||||
script = edited;
|
||||
}
|
||||
|
||||
result.edited_command = strdup (script);
|
||||
@@ -488,9 +454,7 @@ static R2AI_ToolResult r2ai_qjs(RCore *core, R2AI_State *state, RJson *args, boo
|
||||
free (print_script_rendered);
|
||||
}
|
||||
char *payload = r_str_newf ("var console = { log:r2log, warn:r2log, info:r2log, error:r2log, debug:r2log };%s", script);
|
||||
|
||||
// Free edited_script after we're done using it
|
||||
free (edited_script);
|
||||
free (script);
|
||||
|
||||
if (!payload) {
|
||||
r2ai_tool_result_fini (&result);
|
||||
|
||||
@@ -35,10 +35,16 @@ static float squared_distance(const Vector *a, const Vector *b) {
|
||||
/*-------------------------------
|
||||
KDNode Utility Functions
|
||||
-------------------------------*/
|
||||
static KDNode *create_kdnode(const Vector *v, const char *text, int split_dim) {
|
||||
KDNode *node = (KDNode *)malloc (sizeof (KDNode));
|
||||
static KDNode *create_kdnode(const Vector *v, const char *text, int split_dim) { KDNode *node = (KDNode *)calloc (1, sizeof (KDNode));
|
||||
if (!node) {
|
||||
return NULL;
|
||||
}
|
||||
node->point.dim = v->dim;
|
||||
node->point.data = (float *)malloc (sizeof (float) * v->dim);
|
||||
if (!node->point.data) {
|
||||
free (node);
|
||||
return NULL;
|
||||
}
|
||||
memcpy (node->point.data, v->data, sizeof (float) * v->dim);
|
||||
node->text = text? strdup (text): NULL;
|
||||
node->split_dim = split_dim;
|
||||
@@ -77,16 +83,21 @@ void r_vdb_free(RVdb *db) {
|
||||
}
|
||||
|
||||
/* Recursive KD-tree insertion */
|
||||
static KDNode *kd_insert_recursive(KDNode *node, const Vector *v, const char *text, int depth, int dimension) {
|
||||
if (node == NULL) {
|
||||
static KDNode *kd_insert_recursive(KDNode *node, const Vector *v, const char *text, int depth, int dimension) { if (node == NULL) {
|
||||
int split_dim = depth % dimension;
|
||||
return create_kdnode (v, text, split_dim);
|
||||
}
|
||||
int axis = node->split_dim;
|
||||
KDNode *child = kd_insert_recursive (
|
||||
(v->data[axis] < node->point.data[axis]) ? node->left : node->right,
|
||||
v, text, depth + 1, dimension);
|
||||
if (!child) {
|
||||
return node; // allocation failed, keep existing tree
|
||||
}
|
||||
if (v->data[axis] < node->point.data[axis]) {
|
||||
node->left = kd_insert_recursive (node->left, v, text, depth + 1, dimension);
|
||||
node->left = child;
|
||||
} else {
|
||||
node->right = kd_insert_recursive (node->right, v, text, depth + 1, dimension);
|
||||
node->right = child;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
@@ -98,8 +109,7 @@ void r_vdb_insert(RVdb *db, const char *text) {
|
||||
return;
|
||||
}
|
||||
float *embedding = (float *)calloc (db->dimension, sizeof (float));
|
||||
// New call: pass the db pointer so TF-IDF stats are updated.
|
||||
compute_embedding (db, text, embedding, db->dimension);
|
||||
compute_embedding (db, text, embedding, db->dimension, true);
|
||||
Vector v;
|
||||
v.dim = db->dimension;
|
||||
v.data = embedding;
|
||||
@@ -109,9 +119,15 @@ void r_vdb_insert(RVdb *db, const char *text) {
|
||||
}
|
||||
|
||||
// K-NN Search Data Structures and Helpers
|
||||
static RVdbResultSet *create_knn_result_set(int capacity) {
|
||||
RVdbResultSet *rs = (RVdbResultSet *)malloc (sizeof (RVdbResultSet));
|
||||
rs->results = (RVdbResult *)malloc (sizeof (RVdbResult) * capacity);
|
||||
static RVdbResultSet *create_knn_result_set(int capacity) { RVdbResultSet *rs = (RVdbResultSet *)calloc (1, sizeof (RVdbResultSet));
|
||||
if (!rs) {
|
||||
return NULL;
|
||||
}
|
||||
rs->results = (RVdbResult *)calloc (capacity, sizeof (RVdbResult));
|
||||
if (!rs->results) {
|
||||
free (rs);
|
||||
return NULL;
|
||||
}
|
||||
rs->capacity = capacity;
|
||||
rs->size = 0;
|
||||
return rs;
|
||||
@@ -227,16 +243,22 @@ void kd_search_knn_recursive(KDNode *node, const Vector *query, RVdbResultSet *r
|
||||
* Find the k nearest neighbors to the embedding computed from `query_data`.
|
||||
* Returns a RVdbResultSet that must be freed by the caller.
|
||||
*/
|
||||
RVdbResultSet *r_vdb_query_embedding(RVdb *db, const float *query_data, int k) {
|
||||
if (!db || db->size == 0 || k <= 0) {
|
||||
RVdbResultSet *r_vdb_query_embedding(RVdb *db, const float *query_data, int k) { if (!db || db->size == 0 || k <= 0) {
|
||||
return NULL;
|
||||
}
|
||||
Vector query_vec = vector_new (db->dimension);
|
||||
if (!query_vec.data) {
|
||||
return NULL;
|
||||
}
|
||||
for (int i = 0; i < db->dimension; i++) {
|
||||
query_vec.data[i] = query_data[i];
|
||||
}
|
||||
// query_vec is already normalized by compute_embedding ()
|
||||
RVdbResultSet *rs = create_knn_result_set (k);
|
||||
if (!rs) {
|
||||
vector_free (&query_vec);
|
||||
return NULL;
|
||||
}
|
||||
kd_search_knn_recursive (db->root, &query_vec, rs, 0, db->dimension);
|
||||
/* Optional: sort the result set in ascending order of distance */
|
||||
qsort (rs->results, rs->size, sizeof (RVdbResult), compare_knn_result);
|
||||
@@ -246,7 +268,7 @@ RVdbResultSet *r_vdb_query_embedding(RVdb *db, const float *query_data, int k) {
|
||||
|
||||
RVdbResultSet *r_vdb_query(RVdb *db, const char *text, int k) {
|
||||
float *query_embedding = (float *)calloc (db->dimension, sizeof (float));
|
||||
compute_embedding (db, text, query_embedding, db->dimension);
|
||||
compute_embedding (db, text, query_embedding, db->dimension, false);
|
||||
// No extra normalization is needed.
|
||||
RVdbResultSet *res = r_vdb_query_embedding (db, query_embedding, k);
|
||||
free (query_embedding);
|
||||
|
||||
+16
-15
@@ -6,9 +6,10 @@
|
||||
|
||||
// Create a new global DF entry.
|
||||
static void gtfidf_add(RList *db_tokens, const char *token) {
|
||||
RVdbToken *t = R_NEW (RVdbToken);
|
||||
RVdbToken *t = R_NEW0 (RVdbToken);
|
||||
t->token = r_str_trim_dup (token);
|
||||
t->count = 1;
|
||||
t->df = 1.0f;
|
||||
r_list_append (db_tokens, t);
|
||||
}
|
||||
|
||||
@@ -48,7 +49,7 @@ static bool valid_token(const char *a) {
|
||||
|
||||
#if USE_OLLAMA_EMBED
|
||||
// experimental ollama
|
||||
static void compute_embedding(RVdb *db, const char *text, float *embedding, unsigned int dim) {
|
||||
static void compute_embedding(RVdb *db, const char *text, float *embedding, unsigned int dim, bool update_stats) {
|
||||
// curl http://localhost:11434/api/embed -d '{ "model": "llama3:latest", "input": "text" }' |jq -r '.embeddings[0]'
|
||||
char *json_text = r_str_escape_utf8_for_json (text, -1);
|
||||
const char *model = "llama3:latest";
|
||||
@@ -74,7 +75,7 @@ static void compute_embedding(RVdb *db, const char *text, float *embedding, unsi
|
||||
free (s);
|
||||
}
|
||||
#else
|
||||
static void compute_embedding(RVdb *db, const char *text, float *embedding, unsigned int dim) {
|
||||
static void compute_embedding(RVdb *db, const char *text, float *embedding, unsigned int dim, bool update_stats) {
|
||||
// gtfidf_list (db);
|
||||
|
||||
// Zero the embedding vector.
|
||||
@@ -111,24 +112,24 @@ static void compute_embedding(RVdb *db, const char *text, float *embedding, unsi
|
||||
token = strtok_r (NULL, " \t\r\n", &saveptr);
|
||||
}
|
||||
free (buffer);
|
||||
db->total_docs++;
|
||||
|
||||
/* --- Step 2. Update Global Document Frequencies --- */
|
||||
// Here we use the global definition of token_df (do not re-declare it locally).
|
||||
/* --- Step 2. Update Global Document Frequencies (only when indexing, never for queries) --- */
|
||||
RListIter *iter;
|
||||
RVdbToken *dt_token;
|
||||
r_list_foreach (doc_tokens, iter, dt_token) {
|
||||
RVdbToken *t = gtfidf_find (db->tokens, dt_token->token);
|
||||
if (t) {
|
||||
if (valid_token (dt_token->token)) {
|
||||
t->count++;
|
||||
t->df += 1.0f;
|
||||
if (update_stats) {
|
||||
db->total_docs++;
|
||||
r_list_foreach (doc_tokens, iter, dt_token) {
|
||||
RVdbToken *t = gtfidf_find (db->tokens, dt_token->token);
|
||||
if (t) {
|
||||
if (valid_token (dt_token->token)) {
|
||||
t->count++;
|
||||
t->df += 1.0f;
|
||||
}
|
||||
} else {
|
||||
gtfidf_add (db->tokens, dt_token->token);
|
||||
}
|
||||
} else {
|
||||
gtfidf_add (db->tokens, dt_token->token);
|
||||
}
|
||||
}
|
||||
// Increment the total number of documents.
|
||||
|
||||
/* --- Step 3. Compute TF-IDF for Each Token and Update the Embedding --- */
|
||||
RVdbToken *dt;
|
||||
|
||||
+35
-352
@@ -1,6 +1,7 @@
|
||||
/* Copyright r2ai - 2023-2026 - pancake */
|
||||
|
||||
#include "r2ai.h"
|
||||
#include "r2ai_priv.h"
|
||||
|
||||
#define R2AI_VERTEX_TOKEN_TTL_US (3300ULL * R_USEC_PER_SEC) /* 55 minutes, tokens are valid for ~60 */
|
||||
|
||||
@@ -38,372 +39,54 @@ R_IPI const char *r2ai_vertex_get_token(R2AI_State *state) {
|
||||
return state->vertex_token;
|
||||
}
|
||||
|
||||
R_IPI R2AI_ChatResponse *r2ai_vertex_gemini(RCorePluginSession *cps, R2AIArgs args) {
|
||||
RCore *core = cps->core;
|
||||
char **error = args.error;
|
||||
|
||||
// returns the publisher endpoint url or NULL when the cloud project or region are not set
|
||||
static char *vertex_url(const char *publisher, const char *model, const char *method, char **error) {
|
||||
char *project = r_sys_getenv ("GOOGLE_CLOUD_PROJECT");
|
||||
char *region = r_sys_getenv ("GOOGLE_CLOUD_REGION");
|
||||
if (R_STR_ISEMPTY (project) || R_STR_ISEMPTY (region)) {
|
||||
if (error) {
|
||||
*error = strdup ("Set GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_REGION env vars first");
|
||||
}
|
||||
free (project);
|
||||
free (region);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char *model_name = R_STR_ISNOTEMPTY (args.model)
|
||||
? args.model
|
||||
: r_config_get (core->config, "r2ai.model");
|
||||
|
||||
RList *temp_msgs = r2ai_msgs_new ();
|
||||
if (!temp_msgs) {
|
||||
if (error) {
|
||||
*error = strdup ("Failed to create temporary messages array");
|
||||
}
|
||||
free (project);
|
||||
free (region);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char *system_prompt = NULL;
|
||||
if (R_STR_ISNOTEMPTY (args.system_prompt)) {
|
||||
system_prompt = args.system_prompt;
|
||||
char *url = NULL;
|
||||
if (R_STR_ISNOTEMPTY (project) && R_STR_ISNOTEMPTY (region)) {
|
||||
url = r_str_newf ("https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/%s/models/%s:%s",
|
||||
region, project, region, publisher, model, method);
|
||||
} else {
|
||||
system_prompt = r_config_get (core->config, "r2ai.system");
|
||||
*error = strdup ("Set GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_REGION env vars first");
|
||||
}
|
||||
|
||||
RListIter *iter;
|
||||
R2AI_Message *msg;
|
||||
|
||||
if (args.messages) {
|
||||
r_list_foreach (args.messages, iter, msg) {
|
||||
r2ai_msgs_add (temp_msgs, msg);
|
||||
}
|
||||
} else {
|
||||
R_LOG_WARN ("No messages");
|
||||
}
|
||||
|
||||
if (error) {
|
||||
*error = NULL;
|
||||
}
|
||||
|
||||
char *auth_header = r_str_newf ("Authorization: Bearer %s", args.api_key);
|
||||
const char *headers[] = {
|
||||
"Content-Type: application/json",
|
||||
auth_header,
|
||||
NULL
|
||||
};
|
||||
|
||||
char *vertex_url = r_str_newf (
|
||||
"https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s"
|
||||
"/publishers/google/models/%s:generateContent",
|
||||
region,
|
||||
project,
|
||||
region,
|
||||
model_name);
|
||||
|
||||
PJ *pj = pj_new ();
|
||||
pj_o (pj);
|
||||
|
||||
if (R_STR_ISNOTEMPTY (system_prompt)) {
|
||||
pj_ko (pj, "systemInstruction");
|
||||
pj_ka (pj, "parts");
|
||||
pj_o (pj);
|
||||
pj_ks (pj, "text", system_prompt);
|
||||
pj_end (pj);
|
||||
pj_end (pj);
|
||||
pj_end (pj);
|
||||
}
|
||||
|
||||
pj_ka (pj, "contents");
|
||||
r_list_foreach (temp_msgs, iter, msg) {
|
||||
pj_o (pj);
|
||||
const char *role = "user";
|
||||
if (!strcmp (msg->role, "assistant")) {
|
||||
role = "model";
|
||||
}
|
||||
pj_ks (pj, "role", role);
|
||||
pj_ka (pj, "parts");
|
||||
pj_o (pj);
|
||||
pj_ks (pj, "text", msg->content? msg->content: "");
|
||||
pj_end (pj);
|
||||
pj_end (pj);
|
||||
pj_end (pj);
|
||||
}
|
||||
pj_end (pj);
|
||||
|
||||
pj_ko (pj, "generationConfig");
|
||||
if (args.max_tokens > 0) {
|
||||
pj_kn (pj, "maxOutputTokens", args.max_tokens);
|
||||
}
|
||||
if (args.temperature > 0) {
|
||||
pj_kd (pj, "temperature", args.temperature);
|
||||
}
|
||||
pj_end (pj);
|
||||
|
||||
pj_end (pj);
|
||||
|
||||
char *request_json = pj_drain (pj);
|
||||
|
||||
char *tmpdir = r_file_tmpdir ();
|
||||
char *req_path = r_file_new (tmpdir, "r2ai_vertex_gemini_request.json", NULL);
|
||||
r_file_dump (req_path, (const ut8 *)request_json, -1, 0);
|
||||
R_LOG_DEBUG ("Full request saved to %s", req_path);
|
||||
free (req_path);
|
||||
free (tmpdir);
|
||||
|
||||
R_LOG_DEBUG ("Vertex Gemini API request data: %s", request_json);
|
||||
|
||||
int code = 0;
|
||||
char *response = r2ai_http_post (core, vertex_url, headers, request_json, &code, NULL);
|
||||
|
||||
free (request_json);
|
||||
free (vertex_url);
|
||||
free (auth_header);
|
||||
free (project);
|
||||
free (region);
|
||||
|
||||
if (code != 200) {
|
||||
R_LOG_ERROR ("Vertex Gemini API error %d", code);
|
||||
if (response) {
|
||||
R_LOG_ERROR ("Vertex Gemini API error response: %s", response);
|
||||
}
|
||||
free (response);
|
||||
r2ai_msgs_free (temp_msgs);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
tmpdir = r_file_tmpdir ();
|
||||
char *res_path = r_file_new (tmpdir, "r2ai_vertex_gemini_response.json", NULL);
|
||||
r_file_dump (res_path, (const ut8 *)response, -1, 0);
|
||||
R_LOG_DEBUG ("Vertex Gemini API response saved to %s", res_path);
|
||||
free (res_path);
|
||||
free (tmpdir);
|
||||
|
||||
RJson *jres = r_json_parse (response);
|
||||
if (!jres) {
|
||||
if (error) {
|
||||
*error = strdup ("Failed to parse Vertex Gemini response JSON");
|
||||
}
|
||||
free (response);
|
||||
r2ai_msgs_free (temp_msgs);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const RJson *candidates = r_json_get (jres, "candidates");
|
||||
if (!candidates || candidates->type != R_JSON_ARRAY || candidates->children.count == 0) {
|
||||
r_json_free (jres);
|
||||
free (response);
|
||||
r2ai_msgs_free (temp_msgs);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const RJson *candidate = r_json_item (candidates, 0);
|
||||
const RJson *content = r_json_get (candidate, "content");
|
||||
const RJson *parts = r_json_get (content, "parts");
|
||||
|
||||
if (!parts || parts->type != R_JSON_ARRAY || parts->children.count == 0) {
|
||||
r_json_free (jres);
|
||||
free (response);
|
||||
r2ai_msgs_free (temp_msgs);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const RJson *part = r_json_item (parts, 0);
|
||||
const RJson *text = r_json_get (part, "text");
|
||||
|
||||
char *response_text = NULL;
|
||||
if (text && text->type == R_JSON_STRING) {
|
||||
response_text = strdup (text->str_value);
|
||||
}
|
||||
|
||||
R2AI_Usage *usage = R_NEW0 (R2AI_Usage);
|
||||
const RJson *usage_meta = r_json_get (jres, "usageMetadata");
|
||||
if (usage_meta && usage_meta->type == R_JSON_OBJECT) {
|
||||
const RJson *pt = r_json_get (usage_meta, "promptTokenCount");
|
||||
const RJson *ct = r_json_get (usage_meta, "candidatesTokenCount");
|
||||
const RJson *tt = r_json_get (usage_meta, "totalTokenCount");
|
||||
if (pt && pt->type == R_JSON_INTEGER) {
|
||||
usage->prompt_tokens = pt->num.u_value;
|
||||
}
|
||||
if (ct && ct->type == R_JSON_INTEGER) {
|
||||
usage->completion_tokens = ct->num.u_value;
|
||||
}
|
||||
if (tt && tt->type == R_JSON_INTEGER) {
|
||||
usage->total_tokens = tt->num.u_value;
|
||||
}
|
||||
}
|
||||
|
||||
r_json_free (jres);
|
||||
free (response);
|
||||
r2ai_msgs_free (temp_msgs);
|
||||
|
||||
if (!response_text) {
|
||||
free (usage);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
R2AI_Message *message = R_NEW0 (R2AI_Message);
|
||||
message->role = strdup ("assistant");
|
||||
message->content = response_text;
|
||||
|
||||
R2AI_ChatResponse *result = R_NEW0 (R2AI_ChatResponse);
|
||||
result->message = message;
|
||||
result->usage = usage;
|
||||
|
||||
return result;
|
||||
return url;
|
||||
}
|
||||
|
||||
/* Vertex Anthropic uses anthropic_version in body instead of header,
|
||||
* and omits the model field from the request body */
|
||||
|
||||
R_IPI R2AI_ChatResponse *r2ai_vertex_anthropic(RCorePluginSession *cps, R2AIArgs args) {
|
||||
RCore *core = cps->core;
|
||||
const char *model = args.model;
|
||||
char **error = args.error;
|
||||
RList *tools = args.tools;
|
||||
RList *messages_input = args.messages;
|
||||
|
||||
char *project = r_sys_getenv ("GOOGLE_CLOUD_PROJECT");
|
||||
char *region = r_sys_getenv ("GOOGLE_CLOUD_REGION");
|
||||
if (R_STR_ISEMPTY (project) || R_STR_ISEMPTY (region)) {
|
||||
if (error) {
|
||||
*error = strdup ("Set GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_REGION env vars first");
|
||||
}
|
||||
free (project);
|
||||
free (region);
|
||||
static char *vertex_post(RCorePluginSession *cps, R2AIArgs *args, const char *name, const char *publisher, const char *method, const char *data) {
|
||||
char *url = vertex_url (publisher, args->model, method, args->error);
|
||||
if (!url) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char *model_name = R_STR_ISNOTEMPTY (model)
|
||||
? model
|
||||
: r_config_get (core->config, "r2ai.model");
|
||||
|
||||
if (error) {
|
||||
*error = NULL;
|
||||
}
|
||||
|
||||
char *auth_header = r_str_newf ("Authorization: Bearer %s", args.api_key);
|
||||
const char *headers[] = {
|
||||
"Content-Type: application/json",
|
||||
auth_header,
|
||||
NULL
|
||||
};
|
||||
|
||||
char *vertex_url = r_str_newf (
|
||||
"https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s"
|
||||
"/publishers/anthropic/models/%s:rawPredict",
|
||||
region,
|
||||
project,
|
||||
region,
|
||||
model_name);
|
||||
|
||||
const char *system_message = NULL;
|
||||
if (R_STR_ISNOTEMPTY (args.system_prompt)) {
|
||||
system_message = args.system_prompt;
|
||||
} else {
|
||||
system_message = r_config_get (core->config, "r2ai.system");
|
||||
}
|
||||
|
||||
char *messages_json = NULL;
|
||||
if (messages_input && !r_list_empty (messages_input)) {
|
||||
messages_json = r2ai_msgs_to_anthropic_json (messages_input);
|
||||
if (!messages_json) {
|
||||
if (error) {
|
||||
*error = strdup ("Failed to convert messages to JSON");
|
||||
}
|
||||
free (auth_header);
|
||||
free (vertex_url);
|
||||
free (project);
|
||||
free (region);
|
||||
return NULL;
|
||||
}
|
||||
} else {
|
||||
if (error) {
|
||||
*error = strdup ("No input or messages provided");
|
||||
}
|
||||
free (auth_header);
|
||||
free (vertex_url);
|
||||
free (project);
|
||||
free (region);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char *anthropic_tools_json = NULL;
|
||||
if (tools && !r_list_empty (tools)) {
|
||||
anthropic_tools_json = r2ai_tools_to_anthropic_json (tools);
|
||||
}
|
||||
|
||||
PJ *pj = pj_new ();
|
||||
pj_o (pj);
|
||||
pj_ks (pj, "anthropic_version", "vertex-2023-10-16");
|
||||
pj_kn (pj, "max_tokens", args.max_tokens? args.max_tokens: 4096);
|
||||
if (args.thinking_tokens >= 1024) {
|
||||
pj_ko (pj, "thinking");
|
||||
pj_ks (pj, "type", "enabled");
|
||||
pj_kn (pj, "budget_tokens", args.thinking_tokens);
|
||||
pj_end (pj);
|
||||
}
|
||||
if (system_message) {
|
||||
pj_ks (pj, "system", system_message);
|
||||
}
|
||||
|
||||
pj_k (pj, "messages");
|
||||
pj_raw (pj, messages_json);
|
||||
|
||||
if (anthropic_tools_json) {
|
||||
pj_k (pj, "tools");
|
||||
pj_raw (pj, anthropic_tools_json);
|
||||
free (anthropic_tools_json);
|
||||
}
|
||||
|
||||
pj_kb (pj, "stream", false);
|
||||
|
||||
pj_end (pj);
|
||||
|
||||
char *data = pj_drain (pj);
|
||||
free (messages_json);
|
||||
|
||||
char *tmpdir = r_file_tmpdir ();
|
||||
char *req_path = r_file_new (tmpdir, "r2ai_vertex_anthropic_request.json", NULL);
|
||||
r_file_dump (req_path, (const ut8 *)data, -1, 0);
|
||||
R_LOG_DEBUG ("Full request saved to %s", req_path);
|
||||
free (req_path);
|
||||
free (tmpdir);
|
||||
|
||||
R_LOG_DEBUG ("Vertex Anthropic API request data: %s", data);
|
||||
|
||||
char *auth = r_str_newf ("Authorization: Bearer %s", args->api_key);
|
||||
const char *headers[] = { "Content-Type: application/json", auth, NULL };
|
||||
int code = 0;
|
||||
char *res = r2ai_http_post (core, vertex_url, headers, data, &code, NULL);
|
||||
char *res = r2ai_post (cps->core, name, url, headers, data, &code, args->error);
|
||||
free (auth);
|
||||
free (url);
|
||||
return res;
|
||||
}
|
||||
|
||||
R_IPI R2AI_ChatResponse *r2ai_vertex_gemini(RCorePluginSession *cps, R2AIArgs args) {
|
||||
char *data = r2ai_gemini_request (&args);
|
||||
char *res = vertex_post (cps, &args, "vertex_gemini", "google", "generateContent", data);
|
||||
free (data);
|
||||
free (auth_header);
|
||||
free (vertex_url);
|
||||
free (project);
|
||||
free (region);
|
||||
|
||||
if (!res || code != 200) {
|
||||
R_LOG_ERROR ("Vertex Anthropic API error %d", code);
|
||||
if (error && res) {
|
||||
*error = strdup (res);
|
||||
} else if (error) {
|
||||
*error = strdup ("Failed to get response from Vertex Anthropic API");
|
||||
}
|
||||
free (res);
|
||||
R2AI_ChatResponse *result = res? r2ai_gemini_parse (res, args.error): NULL;
|
||||
free (res);
|
||||
return result;
|
||||
}
|
||||
|
||||
R_IPI R2AI_ChatResponse *r2ai_vertex_anthropic(RCorePluginSession *cps, R2AIArgs args) {
|
||||
char *data = r2ai_anthropic_request (&args, true);
|
||||
if (!data) {
|
||||
*args.error = strdup ("No input or messages provided");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
tmpdir = r_file_tmpdir ();
|
||||
char *res_path = r_file_new (tmpdir, "r2ai_vertex_anthropic_response.json", NULL);
|
||||
r_file_dump (res_path, (const ut8 *)res, -1, 0);
|
||||
R_LOG_DEBUG ("Vertex Anthropic API response saved to %s", res_path);
|
||||
free (res_path);
|
||||
free (tmpdir);
|
||||
|
||||
R_LOG_DEBUG ("Vertex Anthropic API response: %s", res);
|
||||
|
||||
R2AI_ChatResponse *result = r2ai_anthropic_parse_response (res, error);
|
||||
char *res = vertex_post (cps, &args, "vertex_anthropic", "anthropic", "rawPredict", data);
|
||||
free (data);
|
||||
R2AI_ChatResponse *result = res? r2ai_anthropic_parse_response (res, args.error): NULL;
|
||||
free (res);
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
all:
|
||||
r2r -u db/extras
|
||||
|
||||
.PHONY: all
|
||||
@@ -0,0 +1,33 @@
|
||||
NAME=OpenAI-compatible base URL
|
||||
FILE=-
|
||||
TIMEOUT=10
|
||||
CMDS=<<EOF
|
||||
!HOME=/tmp/r2ai-r2r-no-home R2_RCFILE=/dev/null ../src/r2ai -p ollama -m fixture-model -b http://127.0.0.1:1/v1/ -e debug=true -e http.backend=socket -e http.max_retries=0 -e http.timeout=1 'fixture prompt' 2>&1 | sed -n '/^Curl command:/{h;s/.*\(Content-Type: application\/json\).*/\1/p;g;s/.*"\(http[^\"]*\)".*/\1/p;}'
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
Content-Type: application/json
|
||||
http://127.0.0.1:1/v1/chat/completions
|
||||
EOF
|
||||
RUN
|
||||
|
||||
NAME=Native Ollama API root
|
||||
FILE=-
|
||||
TIMEOUT=10
|
||||
CMDS=<<EOF
|
||||
!HOME=/tmp/r2ai-r2r-no-home R2_RCFILE=/dev/null ../src/r2ai -p ollama -m fixture-model -b http://127.0.0.1:1/api -e debug=true -e http.backend=socket -e http.max_retries=0 -e http.timeout=1 'fixture prompt' 2>&1 | sed -n '/^Curl command:/s/.*"\(http[^\"]*\)".*/\1/p'
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
http://127.0.0.1:1/api/chat
|
||||
EOF
|
||||
RUN
|
||||
|
||||
NAME=Native Ollama generate API
|
||||
FILE=-
|
||||
TIMEOUT=10
|
||||
CMDS=<<EOF
|
||||
!HOME=/tmp/r2ai-r2r-no-home R2_RCFILE=/dev/null ../src/r2ai -p ollama -m fixture-model -b http://127.0.0.1:1/v1 -e apitype=generate -e debug=true -e http.backend=socket -e http.max_retries=0 -e http.timeout=1 'fixture prompt' 2>&1 | sed -n '/^Curl command:/s/.*"\(http[^\"]*\)".*/\1/p'
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
http://127.0.0.1:1/api/generate
|
||||
EOF
|
||||
RUN
|
||||
Executable
+54
@@ -0,0 +1,54 @@
|
||||
#!/bin/sh
|
||||
# version.sh - print current version or bump to a new one
|
||||
# usage: ./version.sh [new-version]
|
||||
# Updates VERSION, decai/src/constants.ts, src/r2ai.h, and src/meson.build
|
||||
|
||||
VERSION_FILE="VERSION"
|
||||
TS_FILE="decai/src/constants.ts"
|
||||
R2AI_H="src/r2ai.h"
|
||||
MESON_FILE="src/meson.build"
|
||||
|
||||
NEW_VERSION="${1:-}"
|
||||
|
||||
get_version() {
|
||||
if [ -f "$VERSION_FILE" ]; then
|
||||
tr -d '[:space:]' < "$VERSION_FILE"
|
||||
else
|
||||
cat <<EOF >&2
|
||||
error: $VERSION_FILE not found
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
CURRENT=$(get_version)
|
||||
|
||||
if [ -z "$NEW_VERSION" ]; then
|
||||
printf '%s\n' "$CURRENT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# basic semver-ish validation: digits.digits.digits with optional pre-release/build
|
||||
if ! printf '%s' "$NEW_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([-+.]?[A-Za-z0-9.-]*)?$'; then
|
||||
cat <<EOF >&2
|
||||
error: invalid version '$NEW_VERSION'
|
||||
expected format: MAJOR.MINOR.PATCH[-prerelease]
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '%s\n' "$NEW_VERSION" > "$VERSION_FILE"
|
||||
|
||||
if [ -f "$TS_FILE" ]; then
|
||||
sed -i -E "s/export const VERSION = \"[^\"]+\";/export const VERSION = \"$NEW_VERSION\";/" "$TS_FILE"
|
||||
fi
|
||||
|
||||
if [ -f "$R2AI_H" ]; then
|
||||
sed -i -E "s/#define R2AI_VERSION \"[^\"]+\"/#define R2AI_VERSION \"$NEW_VERSION\"/" "$R2AI_H"
|
||||
fi
|
||||
|
||||
if [ -f "$MESON_FILE" ]; then
|
||||
sed -i -E "s/version : '[^']+'/version : '$NEW_VERSION'/" "$MESON_FILE"
|
||||
fi
|
||||
|
||||
printf '%s -> %s\n' "$CURRENT" "$NEW_VERSION"
|
||||
Reference in new issue
Block a user