Custom setting values were JSON-escaped but emitted unquoted, so any non-numeric value (e.g. "high") produced an invalid JSON payload and failed the whole request; quoting the value manually could not work either, since the escaper turns quotes into \". Emit RFC 8259 scalars (number/true/false/null) bare and everything else as a quoted JSON string. Reserved payload keys (model, messages, stream) would duplicate the fixed fields with parser-dependent precedence; reject them in ai_set_provider_setting (surfaced as an error by /ai set custom) and skip them at payload-build time for settings loaded from hand-edited prefs. provider->settings was mutated on the main thread while the request thread iterates it when building the payload; guard both sides with a new per-provider settings_lock. Also refresh stale docs: ai_parse_response no longer mentions the dropped Perplexity "text" path, the payload docstring says "messages" instead of "input", and the /ai help example uses a realistic scalar setting.
295 lines
9.5 KiB
C
295 lines
9.5 KiB
C
#ifndef AI_CLIENT_H
|
|
#define AI_CLIENT_H
|
|
|
|
#include <glib.h>
|
|
|
|
/**
|
|
* @brief AI message structure for conversation history.
|
|
*/
|
|
typedef struct ai_message_t
|
|
{
|
|
gchar* role; /* "user" or "assistant" */
|
|
gchar* content; /* Message content */
|
|
} AIMessage;
|
|
|
|
/**
|
|
* @brief AI provider configuration.
|
|
*/
|
|
typedef struct ai_provider_t
|
|
{
|
|
gchar* name; /* Provider name (e.g., "openai", "perplexity") */
|
|
gchar* api_url; /* API endpoint URL */
|
|
gchar* project_id; /* Optional project ID (for some providers) */
|
|
gchar* default_model; /* Default model for this provider */
|
|
pthread_mutex_t settings_lock; /* Protects settings (iterated by the request thread) */
|
|
GHashTable* settings; /* Extensible per-provider settings (e.g., temperature=0.7) */
|
|
GList* models; /* Cached models (gchar*) */
|
|
gboolean models_fresh; /* Whether models cache is current */
|
|
gint32 ref_count; /* Reference count (atomic) */
|
|
} AIProvider;
|
|
|
|
/**
|
|
* @brief AI chat session structure.
|
|
*/
|
|
typedef struct ai_session_t
|
|
{
|
|
pthread_mutex_t lock; /* Protects all session fields below */
|
|
gchar* provider_name; /* Provider name */
|
|
AIProvider* provider; /* Provider configuration */
|
|
gchar* model; /* Model identifier (e.g., "gpt-4", "sonar") */
|
|
gchar* api_key; /* API key for this session */
|
|
GList* history; /* Conversation history (GList of AIMessage*) */
|
|
gint32 ref_count; /* Reference count (atomic) */
|
|
} AISession;
|
|
|
|
/* ========================================================================
|
|
* Provider Management
|
|
* ======================================================================== */
|
|
|
|
/**
|
|
* Initialize the AI client and load default providers.
|
|
*/
|
|
void ai_client_init(void);
|
|
|
|
/**
|
|
* Shutdown the AI client and free resources.
|
|
*/
|
|
void ai_client_shutdown(void);
|
|
|
|
/**
|
|
* Get a provider by name.
|
|
* @param name The provider name (e.g., "openai", "perplexity")
|
|
* @return AIProvider*, or NULL if not found
|
|
*/
|
|
AIProvider* ai_get_provider(const gchar* name);
|
|
|
|
/**
|
|
* Add or update a provider configuration.
|
|
* @param name The provider name
|
|
* @param api_url The API endpoint URL
|
|
* @return New AIProvider* (caller must unref when done)
|
|
*/
|
|
AIProvider* ai_add_provider(const gchar* name, const gchar* api_url);
|
|
|
|
/**
|
|
* Remove a provider by name.
|
|
* @param name The provider name
|
|
* @return TRUE if provider was removed, FALSE if not found
|
|
*/
|
|
gboolean ai_remove_provider(const gchar* name);
|
|
|
|
/**
|
|
* Increment the reference count of a provider.
|
|
* @param provider The provider to reference
|
|
* @return The same provider pointer
|
|
*/
|
|
AIProvider* ai_provider_ref(AIProvider* provider);
|
|
|
|
/**
|
|
* Decrement the reference count of a provider.
|
|
* @param provider The provider to unreference
|
|
*/
|
|
void ai_provider_unref(AIProvider* provider);
|
|
|
|
/**
|
|
* List all configured providers.
|
|
* @return GList of AIProvider* (caller must not free the list or providers,
|
|
* and must not unref the returned providers)
|
|
*/
|
|
GList* ai_list_providers(void);
|
|
|
|
/**
|
|
* Find a provider name for autocomplete.
|
|
* @param search_str The search string
|
|
* @param previous Whether to go to previous match
|
|
* @param context Unused
|
|
* @return Provider name, or NULL if not found
|
|
*/
|
|
gchar* ai_providers_find(const char* const search_str, gboolean previous, void* context);
|
|
|
|
/**
|
|
* Reset the provider autocomplete state.
|
|
* Called from cmd_ac_reset() to clear autocomplete state.
|
|
*/
|
|
void ai_providers_reset_ac(void);
|
|
|
|
/**
|
|
* Get the API key for a provider.
|
|
* @param provider_name The provider name
|
|
* @return The API key, or NULL if not set (caller must free)
|
|
*/
|
|
gchar* ai_get_provider_key(const gchar* provider_name);
|
|
|
|
/**
|
|
* Set the API key for a provider.
|
|
* @param provider_name The provider name
|
|
* @param api_key The API key to set
|
|
*/
|
|
void ai_set_provider_key(const gchar* provider_name, const gchar* api_key);
|
|
|
|
/**
|
|
* Set the default model for a provider.
|
|
* @param provider_name The provider name
|
|
* @param model The default model name
|
|
*/
|
|
void ai_set_provider_default_model(const gchar* provider_name, const gchar* model);
|
|
|
|
/**
|
|
* Get the default model for a provider.
|
|
* @param provider_name The provider name
|
|
* @return The default model name (caller must not free), or NULL if not set
|
|
*/
|
|
const gchar* ai_get_provider_default_model(const gchar* provider_name);
|
|
|
|
/**
|
|
* Set a custom setting for a provider.
|
|
* Reserved payload keys (model, messages, stream) are rejected.
|
|
* @param provider_name The provider name
|
|
* @param setting The setting key (e.g., "temperature")
|
|
* @param value The setting value, or NULL to remove
|
|
* @return TRUE on success, FALSE on unknown provider or reserved key
|
|
*/
|
|
gboolean ai_set_provider_setting(const gchar* provider_name, const gchar* setting, const gchar* value);
|
|
|
|
/**
|
|
* Get a custom setting for a provider.
|
|
* @param provider_name The provider name
|
|
* @param setting The setting key
|
|
* @return The setting value (caller must free), or NULL if not set
|
|
*/
|
|
gchar* ai_get_provider_setting(const gchar* provider_name, const gchar* setting);
|
|
|
|
/**
|
|
* Fetch available models from a provider's API.
|
|
* @param provider_name The provider name
|
|
* @param user_data User data (ProfAiWin* for UI display)
|
|
* @return TRUE if the request was successfully queued, FALSE otherwise
|
|
*/
|
|
gboolean ai_fetch_models(const gchar* provider_name, gpointer user_data);
|
|
|
|
/**
|
|
* Check if models cache is fresh for a provider.
|
|
* @param provider_name The provider name
|
|
* @return TRUE if models are fresh, FALSE otherwise
|
|
*/
|
|
gboolean ai_models_are_fresh(const gchar* provider_name);
|
|
|
|
/* ========================================================================
|
|
* Parsing helpers (exposed for testing)
|
|
* ======================================================================== */
|
|
|
|
/**
|
|
* Parse model IDs from an OpenAI-compatible API response.
|
|
* Expected format: {"object":"list","data":[{"id":"model1",...},...]}
|
|
* @param provider The provider to add models to
|
|
* @param json The JSON response from the API
|
|
*/
|
|
void ai_parse_models_from_json(AIProvider* provider, const gchar* json);
|
|
|
|
/**
|
|
* Extract assistant content from an OpenAI chat-completions response body
|
|
* ({"choices":[{"message":{"content":"..."}}]}). Decodes the \" and \n
|
|
* escape sequences only.
|
|
* @param response_json The JSON body from the provider
|
|
* @return Newly allocated content string, or NULL if not found (caller frees)
|
|
*/
|
|
gchar* ai_parse_response(const gchar* response_json);
|
|
|
|
/**
|
|
* Extract human-readable error message from an API error envelope.
|
|
* Expected format: {"error":{"message":"...","type":"...","code":...}}
|
|
* Decodes \" \n \\ \t escapes.
|
|
* @param error_json The JSON body of the error response
|
|
* @return Newly allocated error message, or NULL if not parseable (caller frees)
|
|
*/
|
|
gchar* ai_parse_error_message(const gchar* error_json);
|
|
|
|
/* ========================================================================
|
|
* Session Management
|
|
* ======================================================================== */
|
|
|
|
/**
|
|
* Create a new AI session with the specified provider and model.
|
|
* @param provider_name The provider name (e.g., "openai")
|
|
* @param model The model identifier (e.g., "gpt-4")
|
|
* @return New AISession*, or NULL on failure
|
|
*/
|
|
AISession* ai_session_create(const gchar* provider_name, const gchar* model);
|
|
|
|
/**
|
|
* Increment the reference count of an AI session.
|
|
* @param session The session to reference
|
|
* @return The same session pointer
|
|
*/
|
|
AISession* ai_session_ref(AISession* session);
|
|
|
|
/**
|
|
* Decrement the reference count and free the session when it reaches zero.
|
|
* @param session The session to unreference
|
|
*/
|
|
void ai_session_unref(AISession* session);
|
|
|
|
/**
|
|
* Add a message to the session history.
|
|
* @param session The session
|
|
* @param role The message role ("user" or "assistant")
|
|
* @param content The message content
|
|
*/
|
|
void ai_session_add_message(AISession* session, const gchar* role, const gchar* content);
|
|
|
|
/**
|
|
* Clear the conversation history.
|
|
* @param session The session
|
|
*/
|
|
void ai_session_clear_history(AISession* session);
|
|
|
|
/**
|
|
* Get the current model for a session.
|
|
* @param session The session
|
|
* @return The model name (caller must not free)
|
|
*/
|
|
const gchar* ai_session_get_model(AISession* session);
|
|
|
|
/**
|
|
* Set the model for a session.
|
|
* @param session The session
|
|
* @param model The model name
|
|
*/
|
|
void ai_session_set_model(AISession* session, const gchar* model);
|
|
|
|
/**
|
|
* Atomically switch session provider, model, and API key.
|
|
* All mutations happen under the session lock to prevent races with
|
|
* _ai_request_thread() which snapshots session state before making requests.
|
|
*
|
|
* @param session The session
|
|
* @param provider_name New provider name
|
|
* @param model New model identifier
|
|
* @param api_key New API key (caller must free after calling this)
|
|
*/
|
|
void ai_session_switch(AISession* session, const gchar* provider_name,
|
|
const gchar* model, gchar* api_key);
|
|
|
|
/* ========================================================================
|
|
* Request Handling
|
|
* ======================================================================== */
|
|
|
|
/**
|
|
* Send a prompt to the AI provider asynchronously.
|
|
* @param session The AI session containing provider and model
|
|
* @param prompt The prompt to send
|
|
* @param user_data User data (ProfAiWin* for UI display)
|
|
* @return TRUE if the request was successfully queued, FALSE otherwise
|
|
*/
|
|
gboolean ai_send_prompt(AISession* session, const gchar* prompt,
|
|
gpointer user_data);
|
|
|
|
/**
|
|
* Escape a string for JSON embedding.
|
|
* @param str The string to escape
|
|
* @return Newly allocated escaped string (caller must free)
|
|
*/
|
|
gchar* ai_json_escape(const gchar* str);
|
|
|
|
#endif /* AI_CLIENT_H */
|