mirror of
https://github.com/radareorg/r2ai.git
synced 2026-11-07 06:25:57 -10:00
Fix OpenAI-compatible endpoint selection for Ollama
This commit is contained in:
1 parent
02c561c415
commit
dad02ac243
11 files changed
+298
-173
No files matched your search
@@ -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
|
||||
+2
-2
@@ -94,8 +94,8 @@ 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.apitype | Ollama API endpoint type to use: `chat` (default) or `generate`. |
|
||||
| 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 |
|
||||
|
||||
+3
-1
@@ -9,7 +9,7 @@ R_IPI R2AI_ChatResponse *r2ai_gemini(RCorePluginSession *cps, R2AIArgs args) {
|
||||
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);
|
||||
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;
|
||||
|
||||
@@ -19,6 +19,7 @@ R_IPI R2AI_ChatResponse *r2ai_gemini(RCorePluginSession *cps, R2AIArgs args) {
|
||||
if (error) {
|
||||
*error = strdup ("Failed to create temporary messages array");
|
||||
}
|
||||
free (base_url);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -69,6 +70,7 @@ R_IPI R2AI_ChatResponse *r2ai_gemini(RCorePluginSession *cps, R2AIArgs args) {
|
||||
base_url,
|
||||
model_name,
|
||||
args.api_key);
|
||||
free (base_url);
|
||||
|
||||
// Create Gemini-style request JSON
|
||||
PJ *pj = pj_new ();
|
||||
|
||||
@@ -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 },
|
||||
@@ -214,7 +214,29 @@ 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;
|
||||
@@ -224,27 +246,21 @@ R_IPI const char *r2ai_get_provider_url(RCore *core, const char *provider) {
|
||||
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) {
|
||||
@@ -255,7 +271,7 @@ R_IPI RList *r2ai_fetch_available_models(RCore *core, const char *provider) {
|
||||
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;
|
||||
}
|
||||
@@ -271,10 +287,12 @@ R_IPI RList *r2ai_fetch_available_models(RCore *core, const char *provider) {
|
||||
char *models_url = NULL;
|
||||
int code = 0;
|
||||
char *response = NULL;
|
||||
const bool usetags = p && p->api_type == R2AI_API_OLLAMA && r_str_endswith (purl, "/api");
|
||||
|
||||
// Special handling for Gemini
|
||||
if (!strcmp (provider, "gemini")) {
|
||||
if (!api_key) {
|
||||
free (purl);
|
||||
return NULL;
|
||||
}
|
||||
models_url = r_str_newf ("%s/models?key=%s", purl, api_key);
|
||||
@@ -283,8 +301,6 @@ R_IPI RList *r2ai_fetch_available_models(RCore *core, const char *provider) {
|
||||
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) {
|
||||
@@ -319,6 +335,7 @@ R_IPI RList *r2ai_fetch_available_models(RCore *core, const char *provider) {
|
||||
|
||||
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);
|
||||
@@ -341,8 +358,6 @@ R_IPI RList *r2ai_fetch_available_models(RCore *core, const char *provider) {
|
||||
// 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");
|
||||
}
|
||||
|
||||
@@ -372,8 +387,7 @@ R_IPI RList *r2ai_fetch_available_models(RCore *core, const char *provider) {
|
||||
free (s);
|
||||
}
|
||||
} else {
|
||||
const R2AIProvider *prov = r2ai_get_provider (provider);
|
||||
if (prov && prov->api_type == R2AI_API_OLLAMA) {
|
||||
if (usetags) {
|
||||
id = r_json_get (model_item, "model");
|
||||
} else {
|
||||
id = r_json_get (model_item, "id");
|
||||
|
||||
+207
-138
@@ -11,6 +11,12 @@ 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)) {
|
||||
@@ -105,6 +111,129 @@ static void check_chat_cache(RCorePluginSession *cps, const char *provider, cons
|
||||
state->cache_prefix = prefix;
|
||||
}
|
||||
|
||||
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;
|
||||
char *generate_prompt = NULL;
|
||||
char *generate_system = NULL;
|
||||
if (use_generate) {
|
||||
generate_prompt = ollama_generate_prompt_from_messages (messages, &generate_system);
|
||||
} else {
|
||||
messages_json = r2ai_msgs_to_json (messages, use_ollama);
|
||||
}
|
||||
if (!messages_json && !generate_prompt) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
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 {
|
||||
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->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);
|
||||
|
||||
if (!use_generate && check_cache) {
|
||||
check_chat_cache (cps, provider, model, tools_json, messages_json);
|
||||
}
|
||||
free (messages_json);
|
||||
free (generate_prompt);
|
||||
free (generate_system);
|
||||
free (tools_json);
|
||||
return pj_drain (pj);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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 = R_STR_ISNOTEMPTY (args.provider)
|
||||
@@ -115,19 +244,28 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
: r_config_get (core->config, "r2ai.model");
|
||||
|
||||
const R2AIProvider *provider_info = r2ai_get_provider (provider_name);
|
||||
const bool is_ollama = provider_info && provider_info->api_type == R2AI_API_OLLAMA;
|
||||
const bool use_generate = is_ollama && is_generate_api (core);
|
||||
const char *base_url = r2ai_get_provider_url (core, 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) {
|
||||
if (args.error) {
|
||||
*args.error = strdup ("Failed to resolve provider URL");
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
R2AIChatAPI chat_api = use_generate
|
||||
? R2AI_GENERATE_OLLAMA
|
||||
: (is_ollama_provider && r_str_endswith (base_url, "/api")? R2AI_CHAT_OLLAMA: R2AI_CHAT_OPENAI);
|
||||
// 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");
|
||||
}
|
||||
free (base_url);
|
||||
return NULL;
|
||||
}
|
||||
R2AI_Message system_msg = {
|
||||
@@ -177,165 +315,93 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
*error = NULL;
|
||||
}
|
||||
|
||||
const char **headers = 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);
|
||||
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 = is_ollama
|
||||
? (use_generate? "%s/generate": "%s/chat")
|
||||
: "%s/chat/completions";
|
||||
char *openai_url = r_str_newf (urlfmt, base_url);
|
||||
|
||||
// Prepare the input messages in the shape required by the selected endpoint.
|
||||
char *chat_messages_json = NULL;
|
||||
char *generate_prompt = NULL;
|
||||
char *generate_system = NULL;
|
||||
|
||||
if (temp_msgs && !r_list_empty (temp_msgs)) {
|
||||
R_LOG_DEBUG ("Using input messages: %d messages", r_list_length (temp_msgs));
|
||||
if (use_generate) {
|
||||
generate_prompt = ollama_generate_prompt_from_messages (temp_msgs, &generate_system);
|
||||
} else {
|
||||
chat_messages_json = r2ai_msgs_to_json (temp_msgs, is_ollama);
|
||||
}
|
||||
if (!chat_messages_json && !generate_prompt) {
|
||||
if (error) {
|
||||
*error = strdup ("Failed to prepare messages for request");
|
||||
}
|
||||
free (auth_header);
|
||||
return NULL;
|
||||
}
|
||||
} else {
|
||||
if (r_list_empty (temp_msgs)) {
|
||||
if (error) {
|
||||
*error = strdup ("No messages provided");
|
||||
}
|
||||
free (auth_header);
|
||||
free (base_url);
|
||||
r2ai_msgs_free (temp_msgs);
|
||||
return NULL;
|
||||
}
|
||||
R_LOG_DEBUG ("Using input messages: %d messages", r_list_length (temp_msgs));
|
||||
|
||||
// Convert tools to OpenAI format if available
|
||||
char *openai_tools_json = NULL;
|
||||
if (tools && !r_list_empty (tools)) {
|
||||
if (use_generate) {
|
||||
R_LOG_DEBUG ("Skipping native tool definitions for /api/generate payload");
|
||||
} else {
|
||||
openai_tools_json = r2ai_tools_to_openai_json (tools);
|
||||
char *res = NULL;
|
||||
int code = 0;
|
||||
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) {
|
||||
if (error) {
|
||||
*error = strdup ("Failed to create request JSON");
|
||||
}
|
||||
free (fallback_base);
|
||||
free (auth_header);
|
||||
free (base_url);
|
||||
r2ai_msgs_free (temp_msgs);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
char *api_url = chat_api_url (request_base, chat_api);
|
||||
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 *)request_json, strlen (request_json), 0);
|
||||
R_LOG_DEBUG ("Full request saved to %s", req_path);
|
||||
R_LOG_DEBUG ("LLM API request data: %s", request_json);
|
||||
free (req_path);
|
||||
free (tmpdir);
|
||||
|
||||
// Create the model settings part
|
||||
PJ *pj = pj_new ();
|
||||
pj_o (pj);
|
||||
pj_ks (pj, "model", model_name);
|
||||
pj_kb (pj, "stream", false);
|
||||
|
||||
if (is_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);
|
||||
} 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, chat_messages_json);
|
||||
if (openai_tools_json) {
|
||||
pj_k (pj, "tools");
|
||||
pj_raw (pj, openai_tools_json);
|
||||
}
|
||||
}
|
||||
pj_end (pj);
|
||||
|
||||
char *complete_json = pj_drain (pj);
|
||||
if (!use_generate) {
|
||||
check_chat_cache (cps, provider_name, model_name, openai_tools_json, chat_messages_json);
|
||||
}
|
||||
free (chat_messages_json);
|
||||
free (generate_prompt);
|
||||
free (generate_system);
|
||||
free (openai_tools_json);
|
||||
|
||||
if (!complete_json) {
|
||||
if (error) {
|
||||
*error = strdup ("Failed to create complete request JSON");
|
||||
}
|
||||
free (auth_header);
|
||||
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) {
|
||||
if (r_config_get_b (core->config, "r2ai.debug")) {
|
||||
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\"", request_json, api_url);
|
||||
eprintf ("Curl command: %s\n", r_strbuf_get (curl_cmd));
|
||||
r_strbuf_free (curl_cmd);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
res = r2ai_http_post (core, api_url, headers, request_json, &code, NULL);
|
||||
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");
|
||||
free (res);
|
||||
res = NULL;
|
||||
request_base = fallback_base;
|
||||
chat_api = R2AI_CHAT_OLLAMA;
|
||||
first_request = false;
|
||||
}
|
||||
free (fallback_base);
|
||||
free (base_url);
|
||||
|
||||
if (code != 200) {
|
||||
R_LOG_ERROR ("OpenAI API error %d", code);
|
||||
R_LOG_ERROR ("LLM API error %d", code);
|
||||
if (res) {
|
||||
R_LOG_ERROR ("OpenAI API error response: %s", res);
|
||||
R_LOG_ERROR ("LLM API error response: %s", res);
|
||||
}
|
||||
free (auth_header);
|
||||
free (res);
|
||||
r2ai_msgs_free (temp_msgs);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Save the response for inspection
|
||||
tmpdir = r_file_tmpdir ();
|
||||
char *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")) {
|
||||
@@ -349,13 +415,14 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
char *res_copy = strdup (res);
|
||||
RJson *jres = r_json_parse (res_copy);
|
||||
if (jres) {
|
||||
const bool is_native_ollama = chat_api != R2AI_CHAT_OPENAI;
|
||||
R2AI_Message *message = R_NEW0 (R2AI_Message);
|
||||
R2AI_Usage *usage = R_NEW0 (R2AI_Usage);
|
||||
const RJson *usage_json = is_ollama? jres: 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_ollama? "prompt_eval_count": "prompt_tokens"));
|
||||
usage->completion_tokens = json_integer (r_json_get (usage_json, is_ollama? "eval_count": "completion_tokens"));
|
||||
usage->total_tokens = is_ollama
|
||||
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"));
|
||||
}
|
||||
@@ -371,7 +438,7 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
if (R_STR_ISNOTEMPTY (thinking)) {
|
||||
message->reasoning_content = strdup (thinking);
|
||||
}
|
||||
} else if (is_ollama) {
|
||||
} else if (is_native_ollama) {
|
||||
message_json = r_json_get (jres, "message");
|
||||
} else {
|
||||
const RJson *choices = r_json_get (jres, "choices");
|
||||
@@ -456,8 +523,10 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
return result;
|
||||
}
|
||||
|
||||
free (res_copy);
|
||||
free (auth_header);
|
||||
free (res);
|
||||
r2ai_msgs_free (temp_msgs);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -86,10 +86,10 @@ 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
|
||||
Ollama API endpoint type to use: chat (default) or generate.
|
||||
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.
|
||||
|
||||
+2
-2
@@ -795,9 +795,9 @@ 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", "Ollama API endpoint type to use (chat or generate)");
|
||||
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
|
||||
|
||||
+2
-2
@@ -251,9 +251,9 @@ 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);
|
||||
|
||||
+1
-1
@@ -87,7 +87,7 @@ static inline char *r2ai_cons_editor(RCons *cons, const char *file, const char *
|
||||
#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 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);
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user