chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:35:43 +08:00
commit 0e2cc6b102
927 changed files with 260929 additions and 0 deletions
@@ -0,0 +1,138 @@
# Host Model Callback Plugin
This Go-only plugin demonstrates how a plugin-owned browser resource can call the host model execution callbacks instead of sending any external HTTP request itself.
## Purpose and Scope
The plugin registers a Management API resource named `Host Model Callback` at `/status`. CPA exposes it under:
```text
/v0/resource/plugins/host-model-callback/status
```
The resource examples are query-based. The resource reads URL query parameters, builds an OpenAI-compatible chat request, and calls:
- `host.model.execute` for non-streaming model execution.
- `host.model.execute_stream`, `host.model.stream_read`, and `host.model.stream_close` for streaming execution.
This example is intentionally limited to host model callbacks. It does not implement an executor, translator, normalizer, auth provider, scheduler, or any direct outbound HTTP client.
## Build
From this directory:
```bash
cd go
go build -buildmode=c-shared -o host-model-callback.dylib .
rm -f host-model-callback.dylib host-model-callback.h
```
Use the platform extension expected by your target system:
- `.dylib` on macOS
- `.so` on Linux
- `.dll` on Windows
## Configuration
Build the dynamic library and place it under the configured plugin directory with a basename that matches the plugin ID. For example, `plugins/host-model-callback.dylib` maps to `plugins.configs.host-model-callback`.
```yaml
plugins:
enabled: true
dir: "plugins"
configs:
host-model-callback:
enabled: true
priority: 1
```
This plugin does not define plugin-specific configuration fields.
## Resource URL Examples
Non-streaming request with defaults:
```text
http://localhost:8080/v0/resource/plugins/host-model-callback/status
```
Non-streaming request with explicit protocol and prompt:
```text
http://localhost:8080/v0/resource/plugins/host-model-callback/status?entry_protocol=openai&exit_protocol=openai&model=gpt-5.5&prompt=Say%20hello%20in%20one%20sentence
```
Streaming request with explicit close:
```text
http://localhost:8080/v0/resource/plugins/host-model-callback/status?stream=true&model=gpt-5.5&prompt=Write%20three%20short%20tokens
```
Streaming request that relies on RPC-scope implicit close:
```text
http://localhost:8080/v0/resource/plugins/host-model-callback/status?stream=true&implicit_close=true
```
The default model ID is `gpt-5.5` to match the current nearby Codex example documentation and code. It is only an example model identifier; the request succeeds only when your CPA configuration can route that model.
## Parameters
- `entry_protocol`: inbound client protocol passed to the host model execution path. The default is `openai`.
- `exit_protocol`: target provider protocol passed to the host model execution path. The default is `openai`.
- `model`: model identifier passed in the host model execution request. The default is `gpt-5.5`; availability depends on the configured model registry and auth records.
- `stream`: boolean flag. The default is `false`; set `stream=true` to use `host.model.execute_stream`.
- `prompt`: text used to build the default OpenAI-compatible request body.
- `body`: optional JSON string in the URL query used as the raw model request body. When `body` is provided, it replaces the generated body.
- `alt`: optional alternate route or mode suffix passed through the host model request.
- `implicit_close`: streaming-only boolean flag. The default is `false`.
The generated default body is OpenAI-compatible:
```json
{
"model": "gpt-5.5",
"stream": false,
"messages": [
{
"role": "user",
"content": "Summarize host model callbacks in one short sentence."
}
]
}
```
For example, a URL-encoded `body` query value can provide the raw OpenAI-compatible request:
```text
http://localhost:8080/v0/resource/plugins/host-model-callback/status?body=%7B%22model%22%3A%22gpt-5.5%22%2C%22stream%22%3Afalse%2C%22messages%22%3A%5B%7B%22role%22%3A%22user%22%2C%22content%22%3A%22Say%20hello%20in%20one%20sentence%22%7D%5D%7D
```
## Stream Close Semantics
By default, streaming mode explicitly closes the host-owned stream with `host.model.stream_close` through a deferred close call. This is the preferred pattern for plugins because it releases stream resources as soon as the plugin has finished reading.
When `implicit_close=true` is set, the plugin intentionally skips the explicit close call. CPA injects `host_callback_id` into the `management.handle` request, and this example forwards that callback ID to `host.model.execute_stream` so the host can close the stream when the `management.handle` RPC callback scope returns. This mode exists only to demonstrate host cleanup behavior; normal plugin code should explicitly close streams it opens.
## Recursion Guard
This example forwards the `host_callback_id` received from `management.handle` when it calls `host.model.execute` or `host.model.execute_stream`. CPA uses that callback scope to identify the plugin that initiated the host model callback and skips that same plugin's request, response, and stream interceptors for the nested model execution.
Host model callbacks are therefore not recursive for the caller. Other enabled plugins can still intercept the nested request.
## Billing and Usage
The callback uses the existing CPA model executor path. Usage collection, request accounting, and billing metadata are handled by the same executor and usage reporter path as normal proxied requests. The callback layer does not bill twice and does not create an additional usage record by itself.
## Error Handling and Troubleshooting
The page displays the model status, response headers, body, stream chunks, close mode, and any callback error returned by the host envelope.
Common issues:
- `host model executor is unavailable`: the host model executor path is not initialized for this plugin callback context.
- `unsupported model` or provider-specific routing errors: the `model` value is not routable with the current CPA model/auth configuration.
- `host.model.execute requires stream=false`: non-stream execution was called with a streaming request.
- `host.model.execute_stream requires stream=true`: streaming execution was called without `stream=true`.
- Empty or partial stream output: inspect the page error section and host logs; upstream stream errors are returned through `host.model.stream_read`.
@@ -0,0 +1,7 @@
module github.com/router-for-me/CLIProxyAPI/v7/examples/plugin/host-model-callback/go
go 1.26.0
require github.com/router-for-me/CLIProxyAPI/v7 v7.0.0
replace github.com/router-for-me/CLIProxyAPI/v7 => ../../../..
@@ -0,0 +1,731 @@
package main
/*
#include <stdint.h>
#include <stdlib.h>
typedef struct {
void* ptr;
size_t len;
} cliproxy_buffer;
typedef int (*cliproxy_host_call_fn)(void*, const char*, const uint8_t*, size_t, cliproxy_buffer*);
typedef void (*cliproxy_host_free_fn)(void*, size_t);
typedef struct {
uint32_t abi_version;
void* host_ctx;
cliproxy_host_call_fn call;
cliproxy_host_free_fn free_buffer;
} cliproxy_host_api;
typedef int (*cliproxy_plugin_call_fn)(char*, uint8_t*, size_t, cliproxy_buffer*);
typedef void (*cliproxy_plugin_free_fn)(void*, size_t);
typedef void (*cliproxy_plugin_shutdown_fn)(void);
typedef struct {
uint32_t abi_version;
cliproxy_plugin_call_fn call;
cliproxy_plugin_free_fn free_buffer;
cliproxy_plugin_shutdown_fn shutdown;
} cliproxy_plugin_api;
extern int cliproxyPluginCall(char*, uint8_t*, size_t, cliproxy_buffer*);
extern void cliproxyPluginFree(void*, size_t);
extern void cliproxyPluginShutdown(void);
static const cliproxy_host_api* stored_host;
static void store_host_api(const cliproxy_host_api* host) {
stored_host = host;
}
static int call_host_api(const char* method, const uint8_t* request, size_t request_len, cliproxy_buffer* response) {
if (stored_host == NULL || stored_host->call == NULL) {
return 1;
}
return stored_host->call(stored_host->host_ctx, method, request, request_len, response);
}
static void free_host_buffer(void* ptr, size_t len) {
if (stored_host != NULL && stored_host->free_buffer != NULL && ptr != NULL) {
stored_host->free_buffer(ptr, len);
}
}
*/
import "C"
import (
"bytes"
"encoding/json"
"fmt"
"html"
"net/http"
"net/url"
"strconv"
"strings"
"unsafe"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginabi"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi"
)
const (
defaultModel = "gpt-5.5"
defaultPrompt = "Summarize host model callbacks in one short sentence."
pluginName = "host-model-callback"
resourcePath = "/status"
resourceContentType = "text/html; charset=utf-8"
)
type envelope struct {
OK bool `json:"ok"`
Result json.RawMessage `json:"result,omitempty"`
Error *envelopeError `json:"error,omitempty"`
}
type envelopeError struct {
Code string `json:"code"`
Message string `json:"message"`
}
type registration struct {
SchemaVersion uint32 `json:"schema_version"`
Metadata pluginapi.Metadata `json:"metadata"`
Capabilities registrationCapabilities `json:"capabilities"`
}
type registrationCapabilities struct {
ManagementAPI bool `json:"management_api"`
}
type managementRegistration struct {
Resources []managementResource `json:"resources,omitempty"`
}
type managementResource struct {
Path string `json:"Path"`
Menu string `json:"Menu"`
Description string `json:"Description"`
}
type managementRequest struct {
Method string
Path string
Headers http.Header
Query url.Values
Body []byte
HostCallbackID string `json:"host_callback_id,omitempty"`
}
type managementResponse struct {
StatusCode int `json:"StatusCode"`
Headers http.Header `json:"Headers"`
Body []byte `json:"Body"`
}
type managementBodyOptions struct {
Model string `json:"model"`
Mode string `json:"mode"`
EntryProtocol string `json:"entry_protocol"`
ExitProtocol string `json:"exit_protocol"`
Prompt string `json:"prompt"`
Stream *bool `json:"stream"`
Body json.RawMessage `json:"body"`
Headers http.Header `json:"headers"`
Query url.Values `json:"query"`
Alt string `json:"alt"`
ImplicitClose *bool `json:"implicit_close"`
}
type hostModelExecutionRequest struct {
pluginapi.HostModelExecutionRequest
HostCallbackID string `json:"host_callback_id,omitempty"`
}
type runOptions struct {
Model string
Mode string
EntryProtocol string
ExitProtocol string
Prompt string
Stream bool
Body []byte
Headers http.Header
Query url.Values
Alt string
ImplicitClose bool
HostCallbackID string
}
type chatCompletionRequest struct {
Model string `json:"model"`
Stream bool `json:"stream"`
Messages []chatMessage `json:"messages"`
}
type chatMessage struct {
Role string `json:"role"`
Content string `json:"content"`
}
type streamPageData struct {
StatusCode int
Headers http.Header
StreamID string
Chunks []string
Error string
CloseMode string
CloseError string
}
func main() {}
//export cliproxy_plugin_init
func cliproxy_plugin_init(host *C.cliproxy_host_api, plugin *C.cliproxy_plugin_api) C.int {
if plugin == nil {
return 1
}
C.store_host_api(host)
plugin.abi_version = C.uint32_t(pluginabi.ABIVersion)
plugin.call = C.cliproxy_plugin_call_fn(C.cliproxyPluginCall)
plugin.free_buffer = C.cliproxy_plugin_free_fn(C.cliproxyPluginFree)
plugin.shutdown = C.cliproxy_plugin_shutdown_fn(C.cliproxyPluginShutdown)
return 0
}
//export cliproxyPluginCall
func cliproxyPluginCall(method *C.char, request *C.uint8_t, requestLen C.size_t, response *C.cliproxy_buffer) C.int {
if response != nil {
response.ptr = nil
response.len = 0
}
if method == nil {
writeResponse(response, errorEnvelope("invalid_method", "method is required"))
return 1
}
var requestBytes []byte
if request != nil && requestLen > 0 {
requestBytes = C.GoBytes(unsafe.Pointer(request), C.int(requestLen))
}
raw, errHandle := handleMethod(C.GoString(method), requestBytes)
if errHandle != nil {
writeResponse(response, errorEnvelope("plugin_error", errHandle.Error()))
return 1
}
writeResponse(response, raw)
return 0
}
//export cliproxyPluginFree
func cliproxyPluginFree(ptr unsafe.Pointer, len C.size_t) {
if ptr != nil {
C.free(ptr)
}
_ = len
}
//export cliproxyPluginShutdown
func cliproxyPluginShutdown() {}
func handleMethod(method string, request []byte) ([]byte, error) {
switch method {
case pluginabi.MethodPluginRegister, pluginabi.MethodPluginReconfigure:
return okEnvelope(pluginRegistration())
case pluginabi.MethodManagementRegister:
return okEnvelope(managementRegistration{
Resources: []managementResource{{
Path: resourcePath,
Menu: "Host Model Callback",
Description: "Runs a model request through host.model callbacks and displays the result.",
}},
})
case pluginabi.MethodManagementHandle:
return handleManagement(request)
default:
return errorEnvelope("unknown_method", "unknown method: "+method), nil
}
}
func pluginRegistration() registration {
return registration{
SchemaVersion: pluginabi.SchemaVersion,
Metadata: pluginapi.Metadata{
Name: pluginName,
Version: "0.1.0",
Author: "router-for-me",
GitHubRepository: "https://github.com/router-for-me/CLIProxyAPI",
Logo: "https://raw.githubusercontent.com/router-for-me/CLIProxyAPI/main/docs/logo.png",
ConfigFields: []pluginapi.ConfigField{},
},
Capabilities: registrationCapabilities{
ManagementAPI: true,
},
}
}
func handleManagement(raw []byte) ([]byte, error) {
var req managementRequest
if len(raw) > 0 {
if errUnmarshal := json.Unmarshal(raw, &req); errUnmarshal != nil {
return nil, fmt.Errorf("decode management request: %w", errUnmarshal)
}
}
opts, errOptions := optionsFromManagementRequest(req)
if errOptions != nil {
page := renderPage(opts, 0, nil, nil, nil, errOptions.Error(), "", "")
return okEnvelope(htmlResponse(http.StatusBadRequest, page))
}
if opts.Stream {
data := executeStream(opts)
page := renderPage(opts, data.StatusCode, data.Headers, nil, data.Chunks, data.Error, data.CloseMode, data.CloseError)
return okEnvelope(htmlResponse(http.StatusOK, page))
}
resp, errExecute := executeOnce(opts)
if errExecute != nil {
page := renderPage(opts, 0, nil, nil, nil, errExecute.Error(), "", "")
return okEnvelope(htmlResponse(http.StatusOK, page))
}
page := renderPage(opts, resp.StatusCode, resp.Headers, resp.Body, nil, "", "", "")
return okEnvelope(htmlResponse(http.StatusOK, page))
}
func optionsFromManagementRequest(req managementRequest) (runOptions, error) {
opts := runOptions{
Model: defaultModel,
Mode: "non-stream",
EntryProtocol: "openai",
ExitProtocol: "openai",
Prompt: defaultPrompt,
Headers: http.Header{},
Query: url.Values{},
}
opts.HostCallbackID = strings.TrimSpace(req.HostCallbackID)
if len(req.Body) > 0 {
if errApplyBody := applyBodyOptions(&opts, req.Body); errApplyBody != nil {
return opts, errApplyBody
}
}
if errApplyQuery := applyQueryOptions(&opts, req.Query); errApplyQuery != nil {
return opts, errApplyQuery
}
if opts.Stream {
opts.Mode = "stream"
} else {
opts.Mode = "non-stream"
}
return opts, nil
}
func applyBodyOptions(opts *runOptions, raw []byte) error {
var bodyOpts managementBodyOptions
if errUnmarshal := json.Unmarshal(raw, &bodyOpts); errUnmarshal != nil {
return fmt.Errorf("decode JSON request body: %w", errUnmarshal)
}
if strings.TrimSpace(bodyOpts.Model) != "" {
opts.Model = strings.TrimSpace(bodyOpts.Model)
}
if strings.TrimSpace(bodyOpts.Mode) != "" {
applyMode(opts, bodyOpts.Mode)
}
if strings.TrimSpace(bodyOpts.EntryProtocol) != "" {
opts.EntryProtocol = strings.TrimSpace(bodyOpts.EntryProtocol)
}
if strings.TrimSpace(bodyOpts.ExitProtocol) != "" {
opts.ExitProtocol = strings.TrimSpace(bodyOpts.ExitProtocol)
}
if bodyOpts.Prompt != "" {
opts.Prompt = bodyOpts.Prompt
}
if bodyOpts.Stream != nil {
opts.Stream = *bodyOpts.Stream
}
if len(bodyOpts.Body) > 0 && string(bodyOpts.Body) != "null" {
if !json.Valid(bodyOpts.Body) {
return fmt.Errorf("body must be valid JSON")
}
opts.Body = append([]byte(nil), bodyOpts.Body...)
}
if bodyOpts.Headers != nil {
opts.Headers = cloneHeader(bodyOpts.Headers)
}
if bodyOpts.Query != nil {
opts.Query = cloneValues(bodyOpts.Query)
}
if bodyOpts.Alt != "" {
opts.Alt = bodyOpts.Alt
}
if bodyOpts.ImplicitClose != nil {
opts.ImplicitClose = *bodyOpts.ImplicitClose
}
return nil
}
func applyQueryOptions(opts *runOptions, query url.Values) error {
if query == nil {
return nil
}
if raw := strings.TrimSpace(query.Get("model")); raw != "" {
opts.Model = raw
}
if raw := strings.TrimSpace(query.Get("mode")); raw != "" {
applyMode(opts, raw)
}
if raw := strings.TrimSpace(query.Get("entry_protocol")); raw != "" {
opts.EntryProtocol = raw
}
if raw := strings.TrimSpace(query.Get("exit_protocol")); raw != "" {
opts.ExitProtocol = raw
}
if raw := query.Get("prompt"); raw != "" {
opts.Prompt = raw
}
if raw := strings.TrimSpace(query.Get("body")); raw != "" {
body := []byte(raw)
if !json.Valid(body) {
return fmt.Errorf("query body must be valid JSON")
}
opts.Body = append([]byte(nil), body...)
}
if raw := strings.TrimSpace(query.Get("alt")); raw != "" {
opts.Alt = raw
}
if errStream := applyBoolQuery(query, "stream", &opts.Stream); errStream != nil {
return errStream
}
if errImplicitClose := applyBoolQuery(query, "implicit_close", &opts.ImplicitClose); errImplicitClose != nil {
return errImplicitClose
}
return nil
}
func applyMode(opts *runOptions, mode string) {
normalized := strings.ToLower(strings.TrimSpace(mode))
switch normalized {
case "stream", "streaming":
opts.Stream = true
case "non-stream", "non_stream", "nonstream", "sync":
opts.Stream = false
}
}
func applyBoolQuery(query url.Values, key string, target *bool) error {
raw := strings.TrimSpace(query.Get(key))
if raw == "" {
return nil
}
parsed, errParse := strconv.ParseBool(raw)
if errParse != nil {
return fmt.Errorf("%s must be a boolean: %w", key, errParse)
}
*target = parsed
return nil
}
func executeOnce(opts runOptions) (pluginapi.HostModelExecutionResponse, error) {
body, errBody := modelRequestBody(opts)
if errBody != nil {
return pluginapi.HostModelExecutionResponse{}, errBody
}
// Forward HostCallbackID so the host skips this plugin's interceptors on the
// nested model execution. Host model callbacks do not recursively call the
// originating plugin's interceptor chain.
result, errCall := callHost(pluginabi.MethodHostModelExecute, hostModelExecutionRequest{
HostModelExecutionRequest: pluginapi.HostModelExecutionRequest{
EntryProtocol: opts.EntryProtocol,
ExitProtocol: opts.ExitProtocol,
Model: opts.Model,
Stream: false,
Body: body,
Headers: cloneHeader(opts.Headers),
Query: cloneValues(opts.Query),
Alt: opts.Alt,
},
HostCallbackID: opts.HostCallbackID,
})
if errCall != nil {
return pluginapi.HostModelExecutionResponse{}, errCall
}
var resp pluginapi.HostModelExecutionResponse
if errUnmarshal := json.Unmarshal(result, &resp); errUnmarshal != nil {
return pluginapi.HostModelExecutionResponse{}, fmt.Errorf("decode host.model.execute result: %w", errUnmarshal)
}
return resp, nil
}
func executeStream(opts runOptions) (data streamPageData) {
body, errBody := modelRequestBody(opts)
if errBody != nil {
data.Error = errBody.Error()
return data
}
// Forward HostCallbackID so the host skips this plugin's interceptors on the
// nested model execution. Host model callbacks do not recursively call the
// originating plugin's interceptor chain.
result, errCall := callHost(pluginabi.MethodHostModelExecuteStream, hostModelExecutionRequest{
HostModelExecutionRequest: pluginapi.HostModelExecutionRequest{
EntryProtocol: opts.EntryProtocol,
ExitProtocol: opts.ExitProtocol,
Model: opts.Model,
Stream: true,
Body: body,
Headers: cloneHeader(opts.Headers),
Query: cloneValues(opts.Query),
Alt: opts.Alt,
},
HostCallbackID: opts.HostCallbackID,
})
if errCall != nil {
data.Error = errCall.Error()
return data
}
var resp pluginapi.HostModelStreamResponse
if errUnmarshal := json.Unmarshal(result, &resp); errUnmarshal != nil {
data.Error = fmt.Sprintf("decode host.model.execute_stream result: %v", errUnmarshal)
return data
}
data.StatusCode = resp.StatusCode
data.Headers = cloneHeader(resp.Headers)
data.StreamID = resp.StreamID
if resp.StreamID == "" {
data.Error = "host.model.execute_stream returned an empty stream_id"
return data
}
if opts.ImplicitClose {
// When implicit_close=true, the host closes this stream when the management.handle RPC callback scope returns.
data.CloseMode = "implicit close at management.handle return"
} else {
data.CloseMode = "explicit close through host.model.stream_close"
defer func() {
if errClose := closeHostModelStream(resp.StreamID); errClose != nil {
data.CloseError = errClose.Error()
}
}()
}
for {
chunk, errRead := readHostModelStream(resp.StreamID)
if errRead != nil {
data.Error = errRead.Error()
return data
}
if len(chunk.Payload) > 0 {
data.Chunks = append(data.Chunks, string(chunk.Payload))
}
if chunk.Error != "" {
data.Error = chunk.Error
return data
}
if chunk.Done {
return data
}
}
}
func readHostModelStream(streamID string) (pluginapi.HostModelStreamReadResponse, error) {
result, errCall := callHost(pluginabi.MethodHostModelStreamRead, pluginapi.HostModelStreamReadRequest{StreamID: streamID})
if errCall != nil {
return pluginapi.HostModelStreamReadResponse{}, errCall
}
var resp pluginapi.HostModelStreamReadResponse
if errUnmarshal := json.Unmarshal(result, &resp); errUnmarshal != nil {
return pluginapi.HostModelStreamReadResponse{}, fmt.Errorf("decode host.model.stream_read result: %w", errUnmarshal)
}
return resp, nil
}
func closeHostModelStream(streamID string) error {
_, errCall := callHost(pluginabi.MethodHostModelStreamClose, pluginapi.HostModelStreamCloseRequest{StreamID: streamID})
return errCall
}
func modelRequestBody(opts runOptions) ([]byte, error) {
if len(opts.Body) > 0 {
return append([]byte(nil), opts.Body...), nil
}
raw, errMarshal := json.Marshal(chatCompletionRequest{
Model: opts.Model,
Stream: opts.Stream,
Messages: []chatMessage{{
Role: "user",
Content: opts.Prompt,
}},
})
if errMarshal != nil {
return nil, fmt.Errorf("marshal OpenAI-compatible request body: %w", errMarshal)
}
return raw, nil
}
func callHost(method string, payload any) (json.RawMessage, error) {
rawPayload, errMarshal := json.Marshal(payload)
if errMarshal != nil {
return nil, fmt.Errorf("marshal host callback payload %s: %w", method, errMarshal)
}
cMethod := C.CString(method)
defer C.free(unsafe.Pointer(cMethod))
var response C.cliproxy_buffer
var requestPtr *C.uint8_t
if len(rawPayload) > 0 {
cPayload := C.CBytes(rawPayload)
if cPayload == nil {
return nil, fmt.Errorf("allocate host callback payload %s", method)
}
defer C.free(cPayload)
requestPtr = (*C.uint8_t)(cPayload)
}
callCode := C.call_host_api(cMethod, requestPtr, C.size_t(len(rawPayload)), &response)
var rawResponse []byte
if response.ptr != nil && response.len > 0 {
rawResponse = C.GoBytes(response.ptr, C.int(response.len))
}
if response.ptr != nil {
C.free_host_buffer(response.ptr, response.len)
}
if len(rawResponse) == 0 {
return nil, fmt.Errorf("host callback %s returned no response, code=%d", method, int(callCode))
}
var env envelope
if errUnmarshal := json.Unmarshal(rawResponse, &env); errUnmarshal != nil {
return nil, fmt.Errorf("decode host callback envelope %s: %w", method, errUnmarshal)
}
if !env.OK {
if env.Error != nil {
return nil, fmt.Errorf("%s: %s", env.Error.Code, env.Error.Message)
}
return nil, fmt.Errorf("host callback %s failed", method)
}
if callCode != 0 {
return nil, fmt.Errorf("host callback %s returned code=%d", method, int(callCode))
}
return append(json.RawMessage(nil), env.Result...), nil
}
func htmlResponse(statusCode int, body []byte) managementResponse {
return managementResponse{
StatusCode: statusCode,
Headers: http.Header{
"content-type": []string{resourceContentType},
},
Body: body,
}
}
func renderPage(opts runOptions, status int, headers http.Header, body []byte, chunks []string, errText string, closeMode string, closeError string) []byte {
var out bytes.Buffer
out.WriteString("<!doctype html><html><head><meta charset=\"utf-8\"><title>Host Model Callback</title>")
out.WriteString("<style>body{font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif;margin:2rem;line-height:1.45;color:#1f2933}code,pre{background:#f3f4f6;border-radius:6px}code{padding:.1rem .3rem}pre{padding:1rem;overflow:auto;white-space:pre-wrap}dl{display:grid;grid-template-columns:max-content 1fr;gap:.35rem 1rem}dt{font-weight:600}dd{margin:0}.error{color:#b42318}</style>")
out.WriteString("</head><body><main>")
out.WriteString("<h1>Host Model Callback</h1>")
out.WriteString("<dl>")
writeDefinition(&out, "model", opts.Model)
writeDefinition(&out, "mode", opts.Mode)
writeDefinition(&out, "entry_protocol", opts.EntryProtocol)
writeDefinition(&out, "exit_protocol", opts.ExitProtocol)
writeDefinition(&out, "stream", strconv.FormatBool(opts.Stream))
writeDefinition(&out, "implicit_close", strconv.FormatBool(opts.ImplicitClose))
if closeMode != "" {
writeDefinition(&out, "close", closeMode)
}
writeDefinition(&out, "status", strconv.Itoa(status))
out.WriteString("</dl>")
if errText != "" {
out.WriteString("<h2>Error</h2><pre class=\"error\">")
out.WriteString(html.EscapeString(errText))
out.WriteString("</pre>")
}
if closeError != "" {
out.WriteString("<h2>Close Error</h2><pre class=\"error\">")
out.WriteString(html.EscapeString(closeError))
out.WriteString("</pre>")
}
if headers != nil {
out.WriteString("<h2>Headers</h2><pre>")
out.WriteString(html.EscapeString(prettyJSON(headers)))
out.WriteString("</pre>")
}
if len(chunks) > 0 {
out.WriteString("<h2>Stream Chunks</h2><pre>")
out.WriteString(html.EscapeString(strings.Join(chunks, "")))
out.WriteString("</pre>")
}
if len(body) > 0 {
out.WriteString("<h2>Body</h2><pre>")
out.WriteString(html.EscapeString(prettyBody(body)))
out.WriteString("</pre>")
}
out.WriteString("</main></body></html>")
return out.Bytes()
}
func writeDefinition(out *bytes.Buffer, key string, value string) {
out.WriteString("<dt>")
out.WriteString(html.EscapeString(key))
out.WriteString("</dt><dd><code>")
out.WriteString(html.EscapeString(value))
out.WriteString("</code></dd>")
}
func prettyBody(raw []byte) string {
var buf bytes.Buffer
if errIndent := json.Indent(&buf, raw, "", " "); errIndent == nil {
return buf.String()
}
return string(raw)
}
func prettyJSON(v any) string {
raw, errMarshal := json.MarshalIndent(v, "", " ")
if errMarshal != nil {
return fmt.Sprintf("%v", v)
}
return string(raw)
}
func okEnvelope(v any) ([]byte, error) {
raw, errMarshal := json.Marshal(v)
if errMarshal != nil {
return nil, errMarshal
}
return json.Marshal(envelope{OK: true, Result: raw})
}
func errorEnvelope(code, message string) []byte {
raw, _ := json.Marshal(envelope{OK: false, Error: &envelopeError{Code: code, Message: message}})
return raw
}
func writeResponse(response *C.cliproxy_buffer, raw []byte) {
if response == nil || len(raw) == 0 {
return
}
ptr := C.CBytes(raw)
if ptr == nil {
return
}
response.ptr = ptr
response.len = C.size_t(len(raw))
}
func cloneHeader(headers http.Header) http.Header {
if headers == nil {
return nil
}
cloned := make(http.Header, len(headers))
for key, values := range headers {
cloned[key] = append([]string(nil), values...)
}
return cloned
}
func cloneValues(values url.Values) url.Values {
if values == nil {
return nil
}
cloned := make(url.Values, len(values))
for key, items := range values {
cloned[key] = append([]string(nil), items...)
}
return cloned
}