Skip to content
Open
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.
}
Comment on lines +33 to +75
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

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

The JSON escape function has incomplete escape handling. It's missing essential JSON escape sequences like '\b' (backspace) and '\f' (form feed). Additionally, control characters (0x00-0x1F) should be escaped as "\uXXXX" according to JSON specification. Consider using a more complete JSON escaping implementation or a JSON library.

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) {
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

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

The loop continues when an empty line is encountered, but this will cause "question:" to not be printed after skipping. This means multiple empty lines will result in no prompt being shown. Consider printing the prompt after the continue statement, or restructure the logic to handle empty lines more gracefully.

Suggested change
if (strlen(prompt) == 0) {
if (strlen(prompt) == 0) {
printf("question:\n");

Copilot uses AI. Check for mistakes.
continue;
}

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

snprintf(message_json, sizeof(message_json),
"{\"role\": \"user\", \"content\": \"%s\"}", escaped_prompt);
Comment on lines +132 to +133
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

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

The snprintf call doesn't check for truncation. If the formatted string exceeds MAX_JSON_LENGTH, snprintf will truncate it and return a value >= MAX_JSON_LENGTH. This could result in malformed JSON being passed to ov_genai_json_container_create_from_json_string, leading to JSON parsing errors. Consider checking the return value of snprintf to ensure the formatted string fits within the buffer.

Copilot uses AI. Check for mistakes.
Comment on lines +132 to +133
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

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

Potential buffer overflow: snprintf is called with sizeof(message_json) (4096 bytes) but writes escaped_prompt which can be up to (MAX_PROMPT_LENGTH - 1) * 2 + 1 = 2047 bytes, plus the JSON template string {"role": "user", "content": ""} (32 bytes). If the user enters close to MAX_PROMPT_LENGTH characters with many escapable characters, the result could exceed the 4096 byte buffer. The buffer size calculation should account for the worst-case scenario of the escaped string plus JSON formatting overhead.

Copilot uses AI. Check for mistakes.

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 +41 to +47
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

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

[nitpick] Inconsistent parameter ordering with similar functions. This function has parameters ordered as (output, input), while ov_genai_json_container_create_from_json_string uses (input, output) ordering. For consistency across the API, consider reordering to match the pattern where input parameters come before output parameters.

Suggested change
* @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
* @param messages A JsonContainer containing an array of message objects.
* @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_from_json_container(
const ov_genai_json_container* messages,
ov_genai_chat_history** history

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@yatarkan yatarkan Dec 5, 2025

Choose a reason for hiding this comment

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

Please address Copilot's proposal - align order of input/output params

);

/**
* @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