Unit tests (tests/unittests/test_ai_client.c): +50 cases on top of the
existing 50 — total 100. Covers:
- chat response parser (ai_parse_response): OpenAI content + Perplexity
text formats, escape decoding, empty/null/missing inputs, format-string
safety, multiline content
- error envelope parser (ai_parse_error_message): standard envelope,
nested escapes, missing fields, null/empty
- extended JSON escape: \b, \f, \r, all-specials, UTF-8 pass-through
- provider autocomplete cycling with >=2 matches and wrap-around
- session edge cases: NULL args, 100-message order preservation,
set_model(NULL), ref/unref(NULL)
- provider edge cases: get/remove with NULL, double-remove, survival
via session ref after ai_remove_provider
- settings: multi-key independence, missing key, cross-provider
isolation
- model parsing edges: data not array, empty data, "id" outside data,
multiple models
- prefs round-trip: set token -> shutdown -> init -> token reloaded
from disk (uses load_preferences fixture)
Functional tests:
- Console only (test_ai.c): 15 cases for the /ai command surface that
don't need HTTP. Covers /ai help, providers list, set provider/token,
start with/without key/unknown provider, clear, remove, default
provider/model, switch without window, bad subcommand.
Infrastructure:
- TEST_GROUPS bumped to 5 in proftest.c; AI tests live in their own
Group 5 because mixing them with stabber-driven tests in Group 4
poisoned stbbr_stop() teardown.
- PROF_FUNC_TEST_AI macro in functionaltests.c registers AI tests
with ai_init_test() which wraps init_prof_test with a prof_connect()
so stabber sees a graceful disconnect at teardown.
Source-level changes to enable testing:
- src/ai/ai_client.{c,h}: dropped 'static' from _parse_ai_response
(renamed ai_parse_response) and _parse_error_response (renamed
ai_parse_error_message); declared under a "Parsing helpers (exposed
for testing)" section. Same approach as ai_parse_models_from_json.
Results in Docker (cproof-debian image):
- 595/595 unit tests pass
- 15/15 AI functional tests pass (Group 5)
301 lines
9.6 KiB
C
301 lines
9.6 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 */
|
|
GHashTable* settings; /* Extensible per-provider settings (e.g., tools=enabled, search=disabled) */
|
|
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);
|
|
|
|
/**
|
|
* Find a model name for autocomplete.
|
|
* @param search_str The search string
|
|
* @param previous Whether to go to previous match
|
|
* @param context ProfAiWin* for the current session
|
|
* @return Model name, or NULL if not found
|
|
*/
|
|
gchar* ai_models_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.
|
|
* @param provider_name The provider name
|
|
* @param setting The setting key (e.g., "tools", "search")
|
|
* @param value The setting value
|
|
*/
|
|
void 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 LLM response body. Tries Perplexity
|
|
* /v1/responses "text" first, falls back to OpenAI "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 */
|