From 0fa6d4209ff9b4a95e7d1955887aa4c73ee3423c Mon Sep 17 00:00:00 2001 From: t Date: Mon, 6 Jul 2026 16:39:32 -0600 Subject: expose openai_responses API style to C and Go bindings --- libpanto-c/include/panto.h | 5 +++-- libpanto-c/src/lib.zig | 10 +++++----- libpanto-go/panto.go | 40 +++++++++++++++++++++++++++++++++++++--- libpanto-go/panto_test.go | 30 ++++++++++++++++++++++++++++++ 4 files changed, 75 insertions(+), 10 deletions(-) diff --git a/libpanto-c/include/panto.h b/libpanto-c/include/panto.h index 5457715..4302bb8 100644 --- a/libpanto-c/include/panto.h +++ b/libpanto-c/include/panto.h @@ -25,7 +25,7 @@ typedef struct { const uint8_t *ptr; size_t len; } PantoSlice; typedef enum { PANTO_OK = 0, PANTO_ERROR = 1 } PantoStatus; typedef enum { PANTO_NEXT_EVENT = 0, PANTO_NEXT_DONE = 1, PANTO_NEXT_ERROR = 2 } PantoNextStatus; -typedef enum { PANTO_OPENAI_CHAT = 0, PANTO_ANTHROPIC_MESSAGES = 1 } PantoAPIStyle; +typedef enum { PANTO_OPENAI_CHAT = 0, PANTO_ANTHROPIC_MESSAGES = 1, PANTO_OPENAI_RESPONSES = 2, PANTO_OPENAI_CODEX_RESPONSES = 3 } PantoAPIStyle; typedef enum { PANTO_REASONING_DEFAULT = 0, PANTO_REASONING_OFF, PANTO_REASONING_MINIMAL, PANTO_REASONING_LOW, PANTO_REASONING_MEDIUM, PANTO_REASONING_HIGH } PantoReasoningEffort; typedef enum { PANTO_THINKING_DISABLED = 0, PANTO_THINKING_ENABLED, PANTO_THINKING_ADAPTIVE } PantoThinking; typedef enum { PANTO_EFFORT_LOW = 0, PANTO_EFFORT_MEDIUM, PANTO_EFFORT_HIGH, PANTO_EFFORT_XHIGH, PANTO_EFFORT_MAX } PantoEffort; @@ -40,8 +40,9 @@ typedef struct { PantoSlice id, created, modified, last_user_message, base_url, typedef struct { PantoSessionInfo *ptr; size_t len; } PantoSessionInfoList; typedef struct { bool found; PantoSession *session; } PantoSessionResult; typedef struct { const char *api_key, *base_url, *model; PantoReasoningEffort reasoning; uint32_t max_tokens; } PantoOpenAIChatConfig; +typedef struct { const char *api_key, *base_url, *model; PantoReasoningEffort reasoning; uint32_t max_tokens; } PantoOpenAIResponsesConfig; typedef struct { const char *api_key, *base_url, *model, *api_version; uint32_t max_tokens; PantoThinking thinking; PantoEffort effort; bool has_thinking_budget_tokens; uint32_t thinking_budget_tokens; bool thinking_interleaved; } PantoAnthropicMessagesConfig; -typedef struct { PantoAPIStyle tag; union { PantoOpenAIChatConfig openai_chat; PantoAnthropicMessagesConfig anthropic_messages; } data; } PantoProviderConfig; +typedef struct { PantoAPIStyle tag; union { PantoOpenAIChatConfig openai_chat; PantoAnthropicMessagesConfig anthropic_messages; PantoOpenAIResponsesConfig openai_responses; } data; } PantoProviderConfig; typedef struct { uint32_t keep_verbatim; bool has_model; PantoProviderConfig model; bool has_compaction_prompt; const char *compaction_prompt; } PantoCompactionConfig; typedef struct { size_t max_attempts; uint64_t initial_delay_ms, max_delay_ms; double multiplier; bool jitter; } PantoRetryConfig; typedef struct { PantoProviderConfig provider; PantoCompactionConfig compaction; PantoRetryConfig retry; } PantoConfig; diff --git a/libpanto-c/src/lib.zig b/libpanto-c/src/lib.zig index 7f61ae3..4fcf94b 100644 --- a/libpanto-c/src/lib.zig +++ b/libpanto-c/src/lib.zig @@ -26,7 +26,7 @@ const MessageBuilder = struct { role: panto.MessageRole, blocks: std.ArrayList(p pub const PantoSlice = extern struct { ptr: ?[*]const u8, len: usize }; pub const PantoStatus = enum(c_int) { ok = 0, err = 1 }; pub const PantoNextStatus = enum(c_int) { event = 0, done = 1, err = 2 }; -pub const PantoAPIStyle = enum(c_int) { openai_chat = 0, anthropic_messages = 1 }; +pub const PantoAPIStyle = enum(c_int) { openai_chat = 0, anthropic_messages = 1, openai_responses = 2, openai_codex_responses = 3 }; pub const PantoReasoningEffort = enum(c_int) { default = 0, off, minimal, low, medium, high }; pub const PantoThinking = enum(c_int) { disabled = 0, enabled, adaptive }; pub const PantoEffort = enum(c_int) { low = 0, medium, high, xhigh, max }; @@ -40,8 +40,9 @@ pub const PantoSessionInfo = extern struct { id: PantoSlice, created: PantoSlice pub const PantoSessionInfoList = extern struct { ptr: ?[*]PantoSessionInfo, len: usize }; pub const PantoSessionResult = extern struct { found: bool, session: ?*PantoSession }; pub const PantoOpenAIChatConfig = extern struct { api_key: ?[*:0]const u8, base_url: ?[*:0]const u8, model: ?[*:0]const u8, reasoning: PantoReasoningEffort, max_tokens: u32 }; +pub const PantoOpenAIResponsesConfig = extern struct { api_key: ?[*:0]const u8, base_url: ?[*:0]const u8, model: ?[*:0]const u8, reasoning: PantoReasoningEffort, max_tokens: u32 }; pub const PantoAnthropicMessagesConfig = extern struct { api_key: ?[*:0]const u8, base_url: ?[*:0]const u8, model: ?[*:0]const u8, api_version: ?[*:0]const u8, max_tokens: u32, thinking: PantoThinking, effort: PantoEffort, has_thinking_budget_tokens: bool, thinking_budget_tokens: u32, thinking_interleaved: bool }; -pub const PantoProviderConfig = extern struct { tag: PantoAPIStyle, data: extern union { openai_chat: PantoOpenAIChatConfig, anthropic_messages: PantoAnthropicMessagesConfig } }; +pub const PantoProviderConfig = extern struct { tag: PantoAPIStyle, data: extern union { openai_chat: PantoOpenAIChatConfig, anthropic_messages: PantoAnthropicMessagesConfig, openai_responses: PantoOpenAIResponsesConfig } }; pub const PantoCompactionConfig = extern struct { keep_verbatim: u32, has_model: bool, model: PantoProviderConfig, has_compaction_prompt: bool, compaction_prompt: ?[*:0]const u8 }; pub const PantoRetryConfig = extern struct { max_attempts: usize, initial_delay_ms: u64, max_delay_ms: u64, multiplier: f64, jitter: bool }; pub const PantoConfig = extern struct { provider: PantoProviderConfig, compaction: PantoCompactionConfig, retry: PantoRetryConfig }; @@ -485,6 +486,8 @@ fn convertProvider(p: PantoProviderConfig) !panto.ProviderConfig { return switch (p.tag) { .openai_chat => .{ .openai_chat = .{ .api_key = try dupCStr(p.data.openai_chat.api_key), .base_url = try dupCStr(p.data.openai_chat.base_url), .model = try dupCStr(p.data.openai_chat.model), .reasoning = @enumFromInt(@intFromEnum(p.data.openai_chat.reasoning)), .max_tokens = p.data.openai_chat.max_tokens } }, .anthropic_messages => .{ .anthropic_messages = .{ .api_key = try dupCStr(p.data.anthropic_messages.api_key), .base_url = try dupCStr(p.data.anthropic_messages.base_url), .model = try dupCStr(p.data.anthropic_messages.model), .api_version = if (cstr(p.data.anthropic_messages.api_version).len == 0) "2023-06-01" else try dupCStr(p.data.anthropic_messages.api_version), .max_tokens = p.data.anthropic_messages.max_tokens, .thinking = @enumFromInt(@intFromEnum(p.data.anthropic_messages.thinking)), .effort = @enumFromInt(@intFromEnum(p.data.anthropic_messages.effort)), .thinking_budget_tokens = if (p.data.anthropic_messages.has_thinking_budget_tokens) p.data.anthropic_messages.thinking_budget_tokens else null, .thinking_interleaved = p.data.anthropic_messages.thinking_interleaved } }, + .openai_responses => .{ .openai_responses = .{ .api_key = try dupCStr(p.data.openai_responses.api_key), .base_url = try dupCStr(p.data.openai_responses.base_url), .model = try dupCStr(p.data.openai_responses.model), .reasoning = @enumFromInt(@intFromEnum(p.data.openai_responses.reasoning)), .max_tokens = p.data.openai_responses.max_tokens } }, + .openai_codex_responses => return error.UnsupportedProvider, }; } fn freeConfig(c: *panto.Config) void { @@ -506,9 +509,6 @@ fn freeProvider(pv: *panto.ProviderConfig) void { allocator.free(c.model); if (c.api_version.ptr != "2023-06-01".ptr) allocator.free(c.api_version); }, - // The Responses styles are not constructible across the C ABI (no - // PantoAPIStyle variant), so `convertProvider` never builds them; these - // cases exist only to keep the union switch exhaustive. .openai_responses, .openai_codex_responses => |c| { allocator.free(c.api_key); allocator.free(c.base_url); diff --git a/libpanto-go/panto.go b/libpanto-go/panto.go index 35bc5fa..30caaca 100644 --- a/libpanto-go/panto.go +++ b/libpanto-go/panto.go @@ -57,8 +57,10 @@ type ( // drift from the ABI: if a variant is inserted in panto.h, the values shift // with it instead of desyncing. Keep one Go constant per C value. const ( - OpenAIChat APIStyle = APIStyle(C.PANTO_OPENAI_CHAT) - AnthropicMessages APIStyle = APIStyle(C.PANTO_ANTHROPIC_MESSAGES) + OpenAIChat APIStyle = APIStyle(C.PANTO_OPENAI_CHAT) + AnthropicMessages APIStyle = APIStyle(C.PANTO_ANTHROPIC_MESSAGES) + OpenAIResponses APIStyle = APIStyle(C.PANTO_OPENAI_RESPONSES) + OpenAICodexResponses APIStyle = APIStyle(C.PANTO_OPENAI_CODEX_RESPONSES) ) const ( @@ -126,12 +128,18 @@ type Config struct { type ProviderConfig struct { OpenAIChat *OpenAIChatConfig AnthropicMessages *AnthropicMessagesConfig + OpenAIResponses *OpenAIResponsesConfig } type OpenAIChatConfig struct { APIKey, BaseURL, Model string Reasoning ReasoningEffort MaxTokens uint32 } +type OpenAIResponsesConfig struct { + APIKey, BaseURL, Model string + Reasoning ReasoningEffort + MaxTokens uint32 +} type AnthropicMessagesConfig struct { APIKey, BaseURL, Model, APIVersion string MaxTokens uint32 @@ -1106,7 +1114,17 @@ func (cfg Config) c() (C.PantoConfig, cStrings, error) { func (p ProviderConfig) c() (C.PantoProviderConfig, cStrings, error) { var out C.PantoProviderConfig var cs cStrings - if p.OpenAIChat != nil == (p.AnthropicMessages != nil) { + n := 0 + if p.OpenAIChat != nil { + n++ + } + if p.AnthropicMessages != nil { + n++ + } + if p.OpenAIResponses != nil { + n++ + } + if n != 1 { return out, nil, errors.New("exactly one provider config must be set") } if p.OpenAIChat != nil { @@ -1114,6 +1132,11 @@ func (p ProviderConfig) c() (C.PantoProviderConfig, cStrings, error) { setOpenAI(&out, p.OpenAIChat, &cs) return out, cs, nil } + if p.OpenAIResponses != nil { + out.tag = C.PANTO_OPENAI_RESPONSES + setOpenAIResponses(&out, p.OpenAIResponses, &cs) + return out, cs, nil + } out.tag = C.PANTO_ANTHROPIC_MESSAGES setAnthropic(&out, p.AnthropicMessages, &cs) return out, cs, nil @@ -1130,6 +1153,17 @@ func setOpenAI(out *C.PantoProviderConfig, cfg *OpenAIChatConfig, cs *cStrings) c.max_tokens = C.uint32_t(cfg.MaxTokens) } +func setOpenAIResponses(out *C.PantoProviderConfig, cfg *OpenAIResponsesConfig, cs *cStrings) { + apiKey, baseURL, model := C.CString(cfg.APIKey), C.CString(cfg.BaseURL), C.CString(cfg.Model) + *cs = append(*cs, apiKey, baseURL, model) + c := (*C.PantoOpenAIResponsesConfig)(unsafe.Pointer(&out.data[0])) + c.api_key = apiKey + c.base_url = baseURL + c.model = model + c.reasoning = C.PantoReasoningEffort(cfg.Reasoning) + c.max_tokens = C.uint32_t(cfg.MaxTokens) +} + func setAnthropic(out *C.PantoProviderConfig, cfg *AnthropicMessagesConfig, cs *cStrings) { apiKey, baseURL, model, ver := C.CString(cfg.APIKey), C.CString(cfg.BaseURL), C.CString(cfg.Model), C.CString(cfg.APIVersion) *cs = append(*cs, apiKey, baseURL, model, ver) diff --git a/libpanto-go/panto_test.go b/libpanto-go/panto_test.go index af8cafc..1270ead 100644 --- a/libpanto-go/panto_test.go +++ b/libpanto-go/panto_test.go @@ -164,6 +164,36 @@ func testConfig() Config { } } +func TestOpenAIResponsesProviderConfigPassesThrough(t *testing.T) { + provider := ProviderConfig{ + OpenAIResponses: &OpenAIResponsesConfig{ + APIKey: "test-key", + BaseURL: "http://localhost", + Model: "test-model", + Reasoning: ReasoningMedium, + MaxTokens: 128, + }, + } + c, strings, err := provider.c() + if err != nil { + t.Fatalf("ProviderConfig.c: %v", err) + } + defer strings.free() + + if APIStyle(c.tag) != OpenAIResponses { + t.Fatalf("tag = %v, want %v", APIStyle(c.tag), OpenAIResponses) + } + + Init() + defer Deinit() + + a, err := NewAgent(Config{Provider: provider}) + if err != nil { + t.Fatalf("NewAgent: %v", err) + } + defer a.Close() +} + func TestNewAgentSetSystemPromptDoesNotCrash(t *testing.T) { Init() defer Deinit() -- cgit v1.3