Fix OpenAI-compatible endpoint selection for Ollama

This commit is contained in:
pancake committed 2026-08-05 00:42:46 +02:00
1 parent 02c561c415
commit dad02ac243
11 files changed
+298 -173

No files matched your search

+4 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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 ();
+38 -24
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+4
View File
@@ -0,0 +1,4 @@
all:
r2r -u db/extras
.PHONY: all
+33
View File
@@ -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