Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion samples/c/text_generation/benchmark_genai_c.c
Original file line number Diff line number Diff line change
Expand Up @@ -188,4 +188,4 @@ int main(int argc, char* argv[]) {
if (results)
ov_genai_decoded_results_free(results);
return EXIT_SUCCESS;
}
}
148 changes: 137 additions & 11 deletions samples/c/text_generation/chat_sample_c.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,74 @@
#include <string.h>

#include "openvino/genai/c/llm_pipeline.h"
#include "openvino/genai/c/chat_history.h"
#include "openvino/genai/c/json_container.h"

#define MAX_PROMPT_LENGTH 64
#define MAX_PROMPT_LENGTH 1024
#define MAX_JSON_LENGTH 4096

#define CHECK_STATUS(return_status) \
if (return_status != OK) { \
fprintf(stderr, "[ERROR] return status %d, line %d\n", return_status, __LINE__); \
goto err; \
}

#define CHECK_CHAT_HISTORY_STATUS(return_status) \
if (return_status != OV_GENAI_CHAT_HISTORY_OK) { \
fprintf(stderr, "[ERROR] chat history status %d, line %d\n", return_status, __LINE__); \
goto err; \
}

#define CHECK_JSON_CONTAINER_STATUS(return_status) \
if (return_status != OV_GENAI_JSON_CONTAINER_OK) { \
fprintf(stderr, "[ERROR] json container status %d, line %d\n", return_status, __LINE__); \
goto err; \
}

static void json_escape_string(const char* input, char* output, size_t output_size) {
size_t i = 0;
size_t j = 0;
while (input[i] != '\0' && j < output_size - 1) {
switch (input[i]) {
case '"':
if (j < output_size - 2) {
output[j++] = '\\';
output[j++] = '"';
}
break;
case '\\':
if (j < output_size - 2) {
output[j++] = '\\';
output[j++] = '\\';
}
break;
case '\n':
if (j < output_size - 2) {
output[j++] = '\\';
output[j++] = 'n';
}
break;
case '\r':
if (j < output_size - 2) {
output[j++] = '\\';
output[j++] = 'r';
}
break;
case '\t':
if (j < output_size - 2) {
output[j++] = '\\';
output[j++] = 't';
}
break;
default:
output[j++] = input[i];
break;
}
i++;
}
output[j] = '\0';
Comment on lines +37 to +74
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The json_escape_string function does not handle all JSON special characters that need escaping. Missing characters include backspace (\b), form feed (\f), and control characters (U+0000 to U+001F). Additionally, the function should handle buffer overflow more gracefully - it currently silently truncates without warning when the buffer is too small, which could lead to malformed JSON strings.

Suggested change
switch (input[i]) {
case '"':
if (j < output_size - 2) {
output[j++] = '\\';
output[j++] = '"';
}
break;
case '\\':
if (j < output_size - 2) {
output[j++] = '\\';
output[j++] = '\\';
}
break;
case '\n':
if (j < output_size - 2) {
output[j++] = '\\';
output[j++] = 'n';
}
break;
case '\r':
if (j < output_size - 2) {
output[j++] = '\\';
output[j++] = 'r';
}
break;
case '\t':
if (j < output_size - 2) {
output[j++] = '\\';
output[j++] = 't';
}
break;
default:
output[j++] = input[i];
break;
}
i++;
}
output[j] = '\0';
unsigned char c = (unsigned char)input[i];
if (c == '\"' || c == '\\') {
if (j + 2 < output_size) {
output[j++] = '\\';
output[j++] = c;
} else {
break;
}
} else if (c == '\b') {
if (j + 2 < output_size) {
output[j++] = '\\';
output[j++] = 'b';
} else {
break;
}
} else if (c == '\f') {
if (j + 2 < output_size) {
output[j++] = '\\';
output[j++] = 'f';
} else {
break;
}
} else if (c == '\n') {
if (j + 2 < output_size) {
output[j++] = '\\';
output[j++] = 'n';
} else {
break;
}
} else if (c == '\r') {
if (j + 2 < output_size) {
output[j++] = '\\';
output[j++] = 'r';
} else {
break;
}
} else if (c == '\t') {
if (j + 2 < output_size) {
output[j++] = '\\';
output[j++] = 't';
} else {
break;
}
} else if (c < 0x20) {
// Control characters: use \u00XX
if (j + 6 < output_size) {
snprintf(&output[j], output_size - j, "\\u%04x", c);
j += 6;
} else {
break;
}
} else {
if (j + 1 < output_size) {
output[j++] = c;
} else {
break;
}
}
i++;
}
// Always null-terminate
if (j < output_size)
output[j] = '\0';
else if (output_size > 0)
output[output_size - 1] = '\0';

Copilot uses AI. Check for mistakes.
}

ov_genai_streaming_status_e print_callback(const char* str, void* args) {
if (str) {
// If args is not null, it needs to be cast to its actual type.
Expand All @@ -27,37 +87,103 @@ ov_genai_streaming_status_e print_callback(const char* str, void* args) {
}

int main(int argc, char* argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <MODEL_DIR>\n", argv[0]);
if (argc < 2 || argc > 3) {
fprintf(stderr, "Usage: %s <MODEL_DIR> [DEVICE]\n", argv[0]);
return EXIT_FAILURE;
}
const char* models_path = argv[1];
const char* device = "CPU"; // GPU, NPU can be used as well
const char* device = (argc == 3) ? argv[2] : "CPU"; // GPU, NPU can be used as well

ov_genai_generation_config* config = NULL;
ov_genai_llm_pipeline* pipeline = NULL;
ov_genai_chat_history* chat_history = NULL;
ov_genai_decoded_results* results = NULL;
ov_genai_json_container* message_container = NULL;
ov_genai_json_container* assistant_message_container = NULL;
streamer_callback streamer;
streamer.callback_func = print_callback;
streamer.args = NULL;
char prompt[MAX_PROMPT_LENGTH];
char message_json[MAX_JSON_LENGTH];
char output_buffer[MAX_JSON_LENGTH];
size_t output_size = 0;
char assistant_message_json[MAX_JSON_LENGTH];
char escaped_prompt[(MAX_PROMPT_LENGTH - 1) * 2 + 1];
char escaped_output[(MAX_JSON_LENGTH - 1) * 2 + 1];

CHECK_STATUS(ov_genai_llm_pipeline_create(models_path, device, 0, &pipeline));
CHECK_STATUS(ov_genai_generation_config_create(&config));
CHECK_STATUS(ov_genai_generation_config_set_max_new_tokens(config, 100));

CHECK_STATUS(ov_genai_llm_pipeline_start_chat(pipeline));
CHECK_CHAT_HISTORY_STATUS(ov_genai_chat_history_create(&chat_history));

printf("question:\n");
while (fgets(prompt, MAX_PROMPT_LENGTH, stdin)) {
// Remove newline character
prompt[strcspn(prompt, "\n")] = 0;
CHECK_STATUS(ov_genai_llm_pipeline_generate(pipeline,
prompt,
config,
&streamer,
NULL)); // Only the streamer functionality is used here.

// Skip empty lines
if (strlen(prompt) == 0) {
continue;
}

json_escape_string(prompt, escaped_prompt, sizeof(escaped_prompt));

snprintf(message_json, sizeof(message_json),
"{\"role\": \"user\", \"content\": \"%s\"}", escaped_prompt);

if (message_container) {
ov_genai_json_container_free(message_container);
message_container = NULL;
}
CHECK_JSON_CONTAINER_STATUS(ov_genai_json_container_create_from_json_string(
message_json, &message_container));

// Push message using JsonContainer
CHECK_CHAT_HISTORY_STATUS(ov_genai_chat_history_push_back(chat_history, message_container));

results = NULL;
CHECK_STATUS(ov_genai_llm_pipeline_generate_with_history(pipeline,
chat_history,
config,
&streamer,
&results));

if (results) {
output_size = sizeof(output_buffer);
CHECK_STATUS(ov_genai_decoded_results_get_string(results, output_buffer, &output_size));

Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing validation of output_size before using output_buffer. If ov_genai_decoded_results_get_string modifies output_size to be larger than sizeof(output_buffer), or if it fails, the buffer content may be invalid. The return status should be checked before proceeding to use output_buffer.

Suggested change
if (output_size > sizeof(output_buffer)) {
fprintf(stderr, "[ERROR] output_size (%zu) exceeds output_buffer size (%zu), line %d\n", output_size, sizeof(output_buffer), __LINE__);
goto err;
}

Copilot uses AI. Check for mistakes.
json_escape_string(output_buffer, escaped_output, sizeof(escaped_output));

snprintf(assistant_message_json, sizeof(assistant_message_json),
"{\"role\": \"assistant\", \"content\": \"%s\"}", escaped_output);
Comment on lines +132 to +159
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The snprintf calls on lines 132-133 and 158-159 are vulnerable to buffer overflow. If the escaped string is too long, snprintf will truncate the JSON, resulting in malformed JSON (e.g., missing closing quote or brace). This could cause the subsequent ov_genai_json_container_create_from_json_string to fail. Consider checking the return value of snprintf and handling cases where the buffer is too small.

Copilot uses AI. Check for mistakes.

if (assistant_message_container) {
ov_genai_json_container_free(assistant_message_container);
assistant_message_container = NULL;
}
CHECK_JSON_CONTAINER_STATUS(ov_genai_json_container_create_from_json_string(
assistant_message_json, &assistant_message_container));

// Push message using JsonContainer
CHECK_CHAT_HISTORY_STATUS(ov_genai_chat_history_push_back(chat_history, assistant_message_container));

ov_genai_decoded_results_free(results);
results = NULL;
}

printf("\n----------\nquestion:\n");
}
CHECK_STATUS(ov_genai_llm_pipeline_finish_chat(pipeline));

err:
if (results)
ov_genai_decoded_results_free(results);
if (message_container)
ov_genai_json_container_free(message_container);
if (assistant_message_container)
ov_genai_json_container_free(assistant_message_container);
if (chat_history)
ov_genai_chat_history_free(chat_history);
if (pipeline)
ov_genai_llm_pipeline_free(pipeline);
if (config)
Expand Down
2 changes: 1 addition & 1 deletion samples/c/text_generation/greedy_causal_lm_c.c
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,4 @@ int main(int argc, char* argv[]) {
free(output);

return EXIT_SUCCESS;
}
}
181 changes: 181 additions & 0 deletions src/c/include/openvino/genai/c/chat_history.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// Copyright (C) 2025 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
//
// This is a C wrapper for ov::genai::ChatHistory class.

#pragma once

#include "visibility.h"
#include <stddef.h>

// Forward declaration for JsonContainer
typedef struct ov_genai_json_container_opaque ov_genai_json_container;

/**
* @struct ov_genai_chat_history
* @brief Opaque type for ChatHistory
*/
typedef struct ov_genai_chat_history_opaque ov_genai_chat_history;

/**
* @brief Status codes for chat history operations
*/
typedef enum {
OV_GENAI_CHAT_HISTORY_OK = 0,
OV_GENAI_CHAT_HISTORY_INVALID_PARAM = -1,
OV_GENAI_CHAT_HISTORY_OUT_OF_BOUNDS = -2,
OV_GENAI_CHAT_HISTORY_EMPTY = -3,
OV_GENAI_CHAT_HISTORY_INVALID_JSON = -4,
OV_GENAI_CHAT_HISTORY_ERROR = -5
} ov_genai_chat_history_status_e;

/**
* @brief Create a new empty ChatHistory instance.
* @param history A pointer to the newly created ov_genai_chat_history.
* @return ov_genai_chat_history_status_e A status code, return OK(0) if successful.
*/
OPENVINO_GENAI_C_EXPORTS ov_genai_chat_history_status_e ov_genai_chat_history_create(ov_genai_chat_history** history);

/**
* @brief Create a ChatHistory instance from a JsonContainer (array).
* @param history A pointer to the newly created ov_genai_chat_history.
* @param messages A JsonContainer containing an array of message objects.
* @return ov_genai_chat_history_status_e A status code, return OK(0) if successful.
*/
OPENVINO_GENAI_C_EXPORTS ov_genai_chat_history_status_e ov_genai_chat_history_create_from_json_container(
ov_genai_chat_history** history,
const ov_genai_json_container* messages
);
Comment on lines +47 to +48
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Inconsistent spacing: there's a trailing space after the closing parenthesis on line 48. The closing parenthesis on line 48 should align with line 46 without trailing whitespace.

Copilot uses AI. Check for mistakes.

/**
* @brief Release the memory allocated by ov_genai_chat_history.
* @param history A pointer to the ov_genai_chat_history to free memory.
*/
OPENVINO_GENAI_C_EXPORTS void ov_genai_chat_history_free(ov_genai_chat_history* history);

/**
* @brief Add a message to the chat history from a JsonContainer.
* @param history A pointer to the ov_genai_chat_history instance.
* @param message A JsonContainer containing a message object (e.g., {"role": "user", "content": "Hello"}).
* @return ov_genai_chat_history_status_e A status code, return OK(0) if successful.
*/
OPENVINO_GENAI_C_EXPORTS ov_genai_chat_history_status_e ov_genai_chat_history_push_back(
ov_genai_chat_history* history,
const ov_genai_json_container* message);

/**
* @brief Remove the last message from the chat history.
* @param history A pointer to the ov_genai_chat_history instance.
* @return ov_genai_chat_history_status_e A status code, return OK(0) if successful.
*/
OPENVINO_GENAI_C_EXPORTS ov_genai_chat_history_status_e ov_genai_chat_history_pop_back(ov_genai_chat_history* history);

/**
* @brief Get all messages as a JsonContainer (array).
* @param history A pointer to the ov_genai_chat_history instance.
* @param messages A pointer to store the returned JsonContainer containing all messages.
* @return ov_genai_chat_history_status_e A status code, return OK(0) if successful.
*/
OPENVINO_GENAI_C_EXPORTS ov_genai_chat_history_status_e ov_genai_chat_history_get_messages(
const ov_genai_chat_history* history,
ov_genai_json_container** messages);

/**
* @brief Get a message at a specific index as a JsonContainer.
* @param history A pointer to the ov_genai_chat_history instance.
* @param index The index of the message to retrieve.
* @param message A pointer to store the returned JsonContainer containing the message.
* @return ov_genai_chat_history_status_e A status code, return OK(0) if successful.
*/
OPENVINO_GENAI_C_EXPORTS ov_genai_chat_history_status_e ov_genai_chat_history_get_message(
const ov_genai_chat_history* history,
size_t index,
ov_genai_json_container** message);

/**
* @brief Get the first message as a JsonContainer.
* @param history A pointer to the ov_genai_chat_history instance.
* @param message A pointer to store the returned JsonContainer containing the first message.
* @return ov_genai_chat_history_status_e A status code, return OK(0) if successful.
*/
OPENVINO_GENAI_C_EXPORTS ov_genai_chat_history_status_e ov_genai_chat_history_get_first(
const ov_genai_chat_history* history,
ov_genai_json_container** message);

/**
* @brief Get the last message as a JsonContainer.
* @param history A pointer to the ov_genai_chat_history instance.
* @param message A pointer to store the returned JsonContainer containing the last message.
* @return ov_genai_chat_history_status_e A status code, return OK(0) if successful.
*/
OPENVINO_GENAI_C_EXPORTS ov_genai_chat_history_status_e ov_genai_chat_history_get_last(
const ov_genai_chat_history* history,
ov_genai_json_container** message);

/**
* @brief Clear all messages from the chat history.
* @param history A pointer to the ov_genai_chat_history instance.
* @return ov_genai_chat_history_status_e A status code, return OK(0) if successful.
*/
OPENVINO_GENAI_C_EXPORTS ov_genai_chat_history_status_e ov_genai_chat_history_clear(ov_genai_chat_history* history);

/**
* @brief Get the number of messages in the chat history.
* @param history A pointer to the ov_genai_chat_history instance.
* @param size A pointer to store the size (number of messages).
* @return ov_genai_chat_history_status_e A status code, return OK(0) if successful.
*/
OPENVINO_GENAI_C_EXPORTS ov_genai_chat_history_status_e ov_genai_chat_history_size(
const ov_genai_chat_history* history,
size_t* size);

/**
* @brief Check if the chat history is empty.
* @param history A pointer to the ov_genai_chat_history instance.
* @param empty A pointer to store the boolean result (1 for empty, 0 for not empty).
* @return ov_genai_chat_history_status_e A status code, return OK(0) if successful.
*/
OPENVINO_GENAI_C_EXPORTS ov_genai_chat_history_status_e ov_genai_chat_history_empty(
const ov_genai_chat_history* history,
int* empty);

/**
* @brief Set tools definitions (for function calling) as a JsonContainer (array).
* @param history A pointer to the ov_genai_chat_history instance.
* @param tools A JsonContainer containing an array of tool definitions.
* @return ov_genai_chat_history_status_e A status code, return OK(0) if successful.
*/
OPENVINO_GENAI_C_EXPORTS ov_genai_chat_history_status_e ov_genai_chat_history_set_tools(
ov_genai_chat_history* history,
const ov_genai_json_container* tools);

/**
* @brief Get tools definitions as a JsonContainer (array).
* @param history A pointer to the ov_genai_chat_history instance.
* @param tools A pointer to store the returned JsonContainer containing tools definitions.
* @return ov_genai_chat_history_status_e A status code, return OK(0) if successful.
*/
OPENVINO_GENAI_C_EXPORTS ov_genai_chat_history_status_e ov_genai_chat_history_get_tools(
const ov_genai_chat_history* history,
ov_genai_json_container** tools);

/**
* @brief Set extra context (for custom template variables) as a JsonContainer (object).
* @param history A pointer to the ov_genai_chat_history instance.
* @param extra_context A JsonContainer containing an object with extra context.
* @return ov_genai_chat_history_status_e A status code, return OK(0) if successful.
*/
OPENVINO_GENAI_C_EXPORTS ov_genai_chat_history_status_e ov_genai_chat_history_set_extra_context(
ov_genai_chat_history* history,
const ov_genai_json_container* extra_context);

/**
* @brief Get extra context as a JsonContainer (object).
* @param history A pointer to the ov_genai_chat_history instance.
* @param extra_context A pointer to store the returned JsonContainer containing extra context.
* @return ov_genai_chat_history_status_e A status code, return OK(0) if successful.
*/
OPENVINO_GENAI_C_EXPORTS ov_genai_chat_history_status_e ov_genai_chat_history_get_extra_context(
const ov_genai_chat_history* history,
ov_genai_json_container** extra_context);

Loading
Loading