summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-07-06 16:39:32 -0600
committert <t@tjp.lol>2026-07-07 10:44:20 -0600
commit0fa6d4209ff9b4a95e7d1955887aa4c73ee3423c (patch)
treebb07dc02986bd67f6564f85949ad2cd784363514
parent715fc34d833e043e9919fcce503ce3ec7cae5aea (diff)
expose openai_responses API style to C and Go bindings
-rw-r--r--libpanto-c/include/panto.h5
-rw-r--r--libpanto-c/src/lib.zig10
-rw-r--r--libpanto-go/panto.go40
-rw-r--r--libpanto-go/panto_test.go30
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()