Mercurial
diff mrjunejune/inference_bridge.c @ 260:1f9877b637e9
Add Copilot-powered cyberpunk JRPG chat
Integrate the production JRPG chat with Seobeo streaming, Deita persistence, and a Bazel-managed Copilot SDK and LiteLLM inference stack.
Co-authored-by: Copilot <[email protected]>
| author | MrJuneJune <mrjunejune@users.noreply.github.com> |
|---|---|
| date | Wed, 05 Aug 2026 09:19:41 -0700 |
| parents | |
| children | b401627fc49e |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/inference_bridge.c Wed Aug 05 09:19:41 2026 -0700 @@ -0,0 +1,501 @@ +#include "mrjunejune/inference_bridge.h" + +#include <errno.h> +#include <pthread.h> +#include <signal.h> +#include <stdatomic.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/wait.h> +#include <time.h> +#include <unistd.h> + +#define INFERENCE_EVENT_MAX (1024 * 1024 + 4096) +#define INFERENCE_READY_TIMEOUT_MS 15000 + +struct Inference_Bridge { + char *sidecar_path; + char *copilot_cli_path; + FILE *p_commands; + FILE *p_events; + pid_t child_pid; + pthread_t reader_thread; + pthread_mutex_t write_mutex; + pthread_mutex_t ready_mutex; + pthread_cond_t ready_condition; + boolean write_mutex_initialized; + boolean ready_mutex_initialized; + boolean ready_condition_initialized; + boolean reader_started; + _Atomic boolean running; + _Atomic boolean ready; + Inference_Event_Handler handler; + void *p_user_data; +}; + +static int64 Inference_Monotonic_Milliseconds(void) +{ + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + return (int64)now.tv_sec * 1000 + now.tv_nsec / 1000000; +} + +static const char *Inference_JSON_String( + Dowa_JSON_Entry *object, + const char *key) +{ + char *value = Dowa_JSON_Get_String(object, key); + return value ? value : ""; +} + +static boolean Inference_JSON_Boolean( + Dowa_JSON_Entry *object, + const char *key) +{ + Dowa_JSON_Value *p_value = Dowa_JSON_Get(object, key); + return p_value && p_value->type == DOWA_JSON_BOOL + ? p_value->bool_val + : FALSE; +} + +static int64 Inference_JSON_Integer( + Dowa_JSON_Entry *object, + const char *key) +{ + Dowa_JSON_Value *p_value = Dowa_JSON_Get(object, key); + return p_value && p_value->type == DOWA_JSON_NUMBER + ? (int64)p_value->num_val + : 0; +} + +static void Inference_Bridge_Handle_Line( + Inference_Bridge *p_bridge, + const char *line, + size_t length) +{ + Dowa_Arena *p_arena = Dowa_Arena_Create(length * 3 + 4096); + if (!p_arena) + return; + Dowa_JSON_Value parsed = Dowa_JSON_Parse(line, (int32)length, p_arena); + if (parsed.type != DOWA_JSON_OBJECT) + { + Dowa_Arena_Free(p_arena); + return; + } + Dowa_JSON_Entry *object = parsed.object_val; + const char *type = Inference_JSON_String(object, "type"); + if (strcmp(type, "ready") == 0) + { + atomic_store(&p_bridge->ready, TRUE); + pthread_mutex_lock(&p_bridge->ready_mutex); + pthread_cond_broadcast(&p_bridge->ready_condition); + pthread_mutex_unlock(&p_bridge->ready_mutex); + } + + Inference_Event event = { + .type = type, + .request_id = Inference_JSON_String(object, "request_id"), + .conversation_id = Inference_JSON_String(object, "conversation_id"), + .delta = Inference_JSON_String(object, "delta"), + .content = Inference_JSON_String(object, "content"), + .failed = Inference_JSON_Boolean(object, "failed"), + .aborted = Inference_JSON_Boolean(object, "aborted"), + }; + + Dowa_JSON_Value *p_error = Dowa_JSON_Get(object, "error"); + if (p_error && p_error->type == DOWA_JSON_OBJECT) + { + Dowa_JSON_Entry *error = p_error->object_val; + event.error_code = Inference_JSON_String(error, "code"); + event.error_message = Inference_JSON_String(error, "message"); + } + else + { + event.error_code = ""; + event.error_message = ""; + } + + Dowa_JSON_Value *p_usage = Dowa_JSON_Get(object, "usage"); + if (p_usage && p_usage->type == DOWA_JSON_OBJECT) + { + Dowa_JSON_Entry *usage = p_usage->object_val; + event.input_tokens = Inference_JSON_Integer(usage, "input_tokens"); + event.output_tokens = Inference_JSON_Integer(usage, "output_tokens"); + } + + if (p_bridge->handler) + p_bridge->handler(&event, p_bridge->p_user_data); + Dowa_Arena_Free(p_arena); +} + +static void *Inference_Bridge_Read_Events(void *p_context) +{ + Inference_Bridge *p_bridge = p_context; + char *line = NULL; + size_t capacity = 0; + while (atomic_load(&p_bridge->running)) + { + ssize_t amount = getline(&line, &capacity, p_bridge->p_events); + if (amount < 0) + break; + if ((size_t)amount > INFERENCE_EVENT_MAX) + continue; + size_t length = (size_t)amount; + while (length > 0 && + (line[length - 1] == '\n' || line[length - 1] == '\r')) + length--; + if (length > 0) + Inference_Bridge_Handle_Line(p_bridge, line, length); + } + free(line); + atomic_store(&p_bridge->ready, FALSE); + atomic_store(&p_bridge->running, FALSE); + if (p_bridge->handler) + { + Inference_Event closed = { + .type = "bridge.closed", + .error_code = "sidecar_closed", + .error_message = "Inference sidecar connection closed", + .failed = TRUE, + }; + p_bridge->handler(&closed, p_bridge->p_user_data); + } + pthread_mutex_lock(&p_bridge->ready_mutex); + pthread_cond_broadcast(&p_bridge->ready_condition); + pthread_mutex_unlock(&p_bridge->ready_mutex); + return NULL; +} + +static boolean Inference_Bridge_Write( + Inference_Bridge *p_bridge, + const char *payload) +{ + if (!p_bridge || !payload || !atomic_load(&p_bridge->running)) + return FALSE; + pthread_mutex_lock(&p_bridge->write_mutex); + boolean success = + fputs(payload, p_bridge->p_commands) >= 0 && + fputc('\n', p_bridge->p_commands) != EOF && + fflush(p_bridge->p_commands) == 0; + pthread_mutex_unlock(&p_bridge->write_mutex); + return success; +} + +static boolean Inference_Bridge_Command( + Inference_Bridge *p_bridge, + const char *command, + const char *request_id, + const char *conversation_id, + const char *prompt) +{ + if (!command || !request_id) + return FALSE; + size_t input_length = + strlen(command) + strlen(request_id) + + strlen(conversation_id ? conversation_id : "") + + strlen(prompt ? prompt : ""); + if (input_length > (((size_t)-1) - 4096) / 12) + return FALSE; + Dowa_Arena *p_arena = Dowa_Arena_Create(input_length * 12 + 4096); + if (!p_arena) + return FALSE; + char *escaped_command = Dowa_JSON_Escape_String(command, 0, p_arena); + char *escaped_request = Dowa_JSON_Escape_String(request_id, 0, p_arena); + char *escaped_conversation = Dowa_JSON_Escape_String( + conversation_id ? conversation_id : "", 0, p_arena); + char *escaped_prompt = prompt + ? Dowa_JSON_Escape_String(prompt, 0, p_arena) + : NULL; + if (!escaped_command || !escaped_request || !escaped_conversation || + (prompt && !escaped_prompt)) + { + Dowa_Arena_Free(p_arena); + return FALSE; + } + size_t capacity = + strlen(escaped_command) + strlen(escaped_request) + + strlen(escaped_conversation) + + (escaped_prompt ? strlen(escaped_prompt) : 0) + 160; + char *payload = Dowa_Arena_Allocate(p_arena, capacity); + if (!payload) + { + Dowa_Arena_Free(p_arena); + return FALSE; + } + if (escaped_prompt) + { + snprintf( + payload, + capacity, + "{\"command\":\"%s\",\"request_id\":\"%s\"," + "\"conversation_id\":\"%s\",\"prompt\":\"%s\"}", + escaped_command, + escaped_request, + escaped_conversation, + escaped_prompt); + } + else + { + snprintf( + payload, + capacity, + "{\"command\":\"%s\",\"request_id\":\"%s\"," + "\"conversation_id\":\"%s\"}", + escaped_command, + escaped_request, + escaped_conversation); + } + boolean success = Inference_Bridge_Write(p_bridge, payload); + Dowa_Arena_Free(p_arena); + return success; +} + +Inference_Bridge *Inference_Bridge_Create( + const char *sidecar_path, + const char *copilot_cli_path, + Inference_Event_Handler handler, + void *p_user_data) +{ + if (!sidecar_path || !copilot_cli_path) + return NULL; + Inference_Bridge *p_bridge = calloc(1, sizeof(*p_bridge)); + if (!p_bridge) + return NULL; + p_bridge->sidecar_path = strdup(sidecar_path); + p_bridge->copilot_cli_path = strdup(copilot_cli_path); + p_bridge->handler = handler; + p_bridge->p_user_data = p_user_data; + p_bridge->child_pid = -1; + if (!p_bridge->sidecar_path || !p_bridge->copilot_cli_path) + { + Inference_Bridge_Destroy(p_bridge); + return NULL; + } + if (pthread_mutex_init(&p_bridge->write_mutex, NULL) != 0) + { + Inference_Bridge_Destroy(p_bridge); + return NULL; + } + p_bridge->write_mutex_initialized = TRUE; + if (pthread_mutex_init(&p_bridge->ready_mutex, NULL) != 0) + { + Inference_Bridge_Destroy(p_bridge); + return NULL; + } + p_bridge->ready_mutex_initialized = TRUE; + if (pthread_cond_init(&p_bridge->ready_condition, NULL) != 0) + { + Inference_Bridge_Destroy(p_bridge); + return NULL; + } + p_bridge->ready_condition_initialized = TRUE; + return p_bridge; +} + +static void Inference_Bridge_Stop_Process(Inference_Bridge *p_bridge) +{ + if (!p_bridge) + return; + if (atomic_load(&p_bridge->running) && p_bridge->p_commands) + Inference_Bridge_Command( + p_bridge, "shutdown", "server-shutdown", "", NULL); + if (p_bridge->p_commands) + { + fclose(p_bridge->p_commands); + p_bridge->p_commands = NULL; + } + + if (p_bridge->child_pid > 0) + { + int status = 0; + pid_t result = 0; + for (int attempt = 0; attempt < 20; attempt++) + { + result = waitpid(p_bridge->child_pid, &status, WNOHANG); + if (result != 0) + break; + usleep(50000); + } + if (result == 0) + { + kill(p_bridge->child_pid, SIGTERM); + waitpid(p_bridge->child_pid, &status, 0); + } + p_bridge->child_pid = -1; + } + atomic_store(&p_bridge->running, FALSE); + atomic_store(&p_bridge->ready, FALSE); + if (p_bridge->reader_started) + { + pthread_join(p_bridge->reader_thread, NULL); + p_bridge->reader_started = FALSE; + } + if (p_bridge->p_events) + { + fclose(p_bridge->p_events); + p_bridge->p_events = NULL; + } +} + +boolean Inference_Bridge_Start(Inference_Bridge *p_bridge) +{ + if (!p_bridge || atomic_load(&p_bridge->running)) + return FALSE; + int commands[2]; + int events[2]; + if (pipe(commands) != 0) + return FALSE; + if (pipe(events) != 0) + { + close(commands[0]); + close(commands[1]); + return FALSE; + } + + pid_t child = fork(); + if (child < 0) + { + close(commands[0]); + close(commands[1]); + close(events[0]); + close(events[1]); + return FALSE; + } + if (child == 0) + { + dup2(commands[0], STDIN_FILENO); + dup2(events[1], STDOUT_FILENO); + close(commands[0]); + close(commands[1]); + close(events[0]); + close(events[1]); + execl( + p_bridge->sidecar_path, + p_bridge->sidecar_path, + p_bridge->copilot_cli_path, + NULL); + _exit(127); + } + + close(commands[0]); + close(events[1]); + p_bridge->p_commands = fdopen(commands[1], "w"); + p_bridge->p_events = fdopen(events[0], "r"); + if (!p_bridge->p_commands || !p_bridge->p_events) + { + if (p_bridge->p_commands) + fclose(p_bridge->p_commands); + else + close(commands[1]); + if (p_bridge->p_events) + fclose(p_bridge->p_events); + else + close(events[0]); + kill(child, SIGTERM); + waitpid(child, NULL, 0); + return FALSE; + } + setvbuf(p_bridge->p_commands, NULL, _IOLBF, 0); + p_bridge->child_pid = child; + atomic_store(&p_bridge->running, TRUE); + atomic_store(&p_bridge->ready, FALSE); + if (pthread_create( + &p_bridge->reader_thread, + NULL, + Inference_Bridge_Read_Events, + p_bridge) != 0) + { + atomic_store(&p_bridge->running, FALSE); + fclose(p_bridge->p_commands); + fclose(p_bridge->p_events); + kill(child, SIGTERM); + waitpid(child, NULL, 0); + p_bridge->p_commands = NULL; + p_bridge->p_events = NULL; + p_bridge->child_pid = -1; + return FALSE; + } + p_bridge->reader_started = TRUE; + + int64 deadline = + Inference_Monotonic_Milliseconds() + INFERENCE_READY_TIMEOUT_MS; + pthread_mutex_lock(&p_bridge->ready_mutex); + while (atomic_load(&p_bridge->running) && + !atomic_load(&p_bridge->ready)) + { + int64 remaining = deadline - Inference_Monotonic_Milliseconds(); + if (remaining <= 0) + break; + struct timespec timeout; + clock_gettime(CLOCK_REALTIME, &timeout); + timeout.tv_sec += remaining / 1000; + timeout.tv_nsec += (remaining % 1000) * 1000000; + if (timeout.tv_nsec >= 1000000000) + { + timeout.tv_sec++; + timeout.tv_nsec -= 1000000000; + } + pthread_cond_timedwait( + &p_bridge->ready_condition, &p_bridge->ready_mutex, &timeout); + } + boolean ready = atomic_load(&p_bridge->ready); + pthread_mutex_unlock(&p_bridge->ready_mutex); + if (!ready) + { + Inference_Bridge_Stop_Process(p_bridge); + return FALSE; + } + return TRUE; +} + +boolean Inference_Bridge_Is_Ready(const Inference_Bridge *p_bridge) +{ + return p_bridge && + atomic_load(&p_bridge->running) && + atomic_load(&p_bridge->ready); +} + +boolean Inference_Bridge_Start_Turn( + Inference_Bridge *p_bridge, + const char *request_id, + const char *conversation_id, + const char *prompt) +{ + return Inference_Bridge_Command( + p_bridge, "turn.start", request_id, conversation_id, prompt); +} + +boolean Inference_Bridge_Abort_Turn( + Inference_Bridge *p_bridge, + const char *request_id, + const char *conversation_id) +{ + return Inference_Bridge_Command( + p_bridge, "turn.abort", request_id, conversation_id, NULL); +} + +boolean Inference_Bridge_Delete_Conversation( + Inference_Bridge *p_bridge, + const char *request_id, + const char *conversation_id) +{ + return Inference_Bridge_Command( + p_bridge, "conversation.delete", request_id, conversation_id, NULL); +} + +void Inference_Bridge_Destroy(Inference_Bridge *p_bridge) +{ + if (!p_bridge) + return; + Inference_Bridge_Stop_Process(p_bridge); + if (p_bridge->ready_condition_initialized) + pthread_cond_destroy(&p_bridge->ready_condition); + if (p_bridge->ready_mutex_initialized) + pthread_mutex_destroy(&p_bridge->ready_mutex); + if (p_bridge->write_mutex_initialized) + pthread_mutex_destroy(&p_bridge->write_mutex); + free(p_bridge->sidecar_path); + free(p_bridge->copilot_cli_path); + free(p_bridge); +}