My agentic slop goes here. Not intended for anyone else!

Compare changes

Choose any two refs to compare.

+1
claudeio/claude.opam
···
"fmt"
"logs"
"ezjsonm"
+
"jsont" {>= "0.2.0"}
"alcotest" {with-test}
"odoc" {with-doc}
]
+1
claudeio/dune-project
···
fmt
logs
ezjsonm
+
(jsont (>= 0.2.0))
(alcotest :with-test)))
+3
claudeio/lib/claude.ml
···
+
module Model = Model
module Content_block = Content_block
module Message = Message
module Control = Control
module Permissions = Permissions
module Hooks = Hooks
+
module Sdk_control = Sdk_control
+
module Structured_output = Structured_output
module Options = Options
module Transport = Transport
module Client = Client
+9
claudeio/lib/claude.mli
···
module Options = Options
(** Configuration options for Claude sessions. *)
+
module Model = Model
+
(** Claude AI model identifiers with type-safe variants. *)
+
module Content_block = Content_block
(** Content blocks for messages (text, tool use, tool results, thinking). *)
···
module Hooks = Hooks
(** Hooks system for event interception. *)
+
+
module Sdk_control = Sdk_control
+
(** SDK control protocol for dynamic configuration. *)
+
+
module Structured_output = Structured_output
+
(** Structured output support using JSON Schema. *)
module Transport = Transport
(** Low-level transport layer for CLI communication. *)
+80 -1
claudeio/lib/client.ml
···
hook_callbacks : (string, Hooks.callback) Hashtbl.t;
mutable next_callback_id : int;
mutable session_id : string option;
+
control_responses : (string, Ezjsonm.value) Hashtbl.t;
+
control_mutex : Eio.Mutex.t;
+
control_condition : Eio.Condition.t;
}
let handle_control_request t control_msg =
···
(* Handle control responses (e.g., initialize response) *)
let request_id = Json_utils.find_string json ["response"; "request_id"] in
Log.debug (fun m -> m "Received control response for request_id: %s" request_id);
-
(* Don't yield control responses as messages, just loop *)
+
(* Store the response and signal waiting threads *)
+
Eio.Mutex.use_rw ~protect:false t.control_mutex (fun () ->
+
Hashtbl.replace t.control_responses request_id json;
+
Eio.Condition.broadcast t.control_condition
+
);
loop ()
| _ ->
···
hook_callbacks;
next_callback_id = 0;
session_id = None;
+
control_responses = Hashtbl.create 16;
+
control_mutex = Eio.Mutex.create ();
+
control_condition = Eio.Condition.create ();
} in
(* Register hooks and send initialize if hooks are configured *)
···
let with_permission_callback t callback =
{ t with permission_callback = Some callback }
+
+
(* Helper to send a control request and wait for response *)
+
let send_control_request t ~request_id request =
+
let open Ezjsonm in
+
(* Send the control request *)
+
let control_msg = Sdk_control.create_request ~request_id ~request in
+
let json = Sdk_control.to_json control_msg in
+
Log.info (fun m -> m "Sending control request: %s" (value_to_string json));
+
Transport.send t.transport json;
+
+
(* Wait for the response with timeout *)
+
let max_wait = 10.0 in (* 10 seconds timeout *)
+
let start_time = Unix.gettimeofday () in
+
+
let rec wait_for_response () =
+
Eio.Mutex.use_rw ~protect:false t.control_mutex (fun () ->
+
match Hashtbl.find_opt t.control_responses request_id with
+
| Some response_json ->
+
(* Remove it from the table *)
+
Hashtbl.remove t.control_responses request_id;
+
response_json
+
| None ->
+
let elapsed = Unix.gettimeofday () -. start_time in
+
if elapsed > max_wait then
+
raise (Failure (Printf.sprintf "Timeout waiting for control response: %s" request_id))
+
else (
+
(* Release mutex and wait for signal *)
+
Eio.Condition.await_no_mutex t.control_condition;
+
wait_for_response ()
+
)
+
)
+
in
+
+
let response_json = wait_for_response () in
+
Log.debug (fun m -> m "Received control response: %s" (value_to_string response_json));
+
+
(* Parse the response *)
+
let response = find response_json ["response"] |> Sdk_control.Response.of_json in
+
match response with
+
| Sdk_control.Response.Success s -> s.response
+
| Sdk_control.Response.Error e ->
+
raise (Failure (Printf.sprintf "Control request failed: %s" e.error))
+
+
let set_permission_mode t mode =
+
let request_id = Printf.sprintf "set_perm_mode_%f" (Unix.gettimeofday ()) in
+
let request = Sdk_control.Request.set_permission_mode ~mode in
+
let _response = send_control_request t ~request_id request in
+
Log.info (fun m -> m "Permission mode set to: %a" Permissions.Mode.pp mode)
+
+
let set_model t model =
+
let model_str = Model.to_string model in
+
let request_id = Printf.sprintf "set_model_%f" (Unix.gettimeofday ()) in
+
let request = Sdk_control.Request.set_model ~model:model_str in
+
let _response = send_control_request t ~request_id request in
+
Log.info (fun m -> m "Model set to: %a" Model.pp model)
+
+
let set_model_string t model_str =
+
set_model t (Model.of_string model_str)
+
+
let get_server_info t =
+
let request_id = Printf.sprintf "get_server_info_%f" (Unix.gettimeofday ()) in
+
let request = Sdk_control.Request.get_server_info () in
+
match send_control_request t ~request_id request with
+
| Some response_data ->
+
let server_info = Sdk_control.Server_info.of_json response_data in
+
Log.info (fun m -> m "Retrieved server info: %a" Sdk_control.Server_info.pp server_info);
+
server_info
+
| None ->
+
raise (Failure "No response data from get_server_info request")
+193 -3
claudeio/lib/client.mli
···
+
(** Client interface for interacting with Claude.
+
+
This module provides the high-level client API for sending messages to
+
Claude and receiving responses. It handles the bidirectional streaming
+
protocol, permission callbacks, and hooks.
+
+
{2 Basic Usage}
+
+
{[
+
Eio.Switch.run @@ fun sw ->
+
let client = Client.create ~sw ~process_mgr () in
+
Client.query client "What is 2+2?";
+
+
let messages = Client.receive_all client in
+
List.iter (function
+
| Message.Assistant msg ->
+
Printf.printf "Claude: %s\n" (Message.Assistant.text msg)
+
| _ -> ()
+
) messages
+
]}
+
+
{2 Features}
+
+
- {b Message Streaming}: Messages are streamed lazily via {!Seq.t}
+
- {b Permission Control}: Custom permission callbacks for tool usage
+
- {b Hooks}: Intercept and modify tool execution
+
- {b Dynamic Control}: Change settings mid-conversation
+
- {b Resource Management}: Automatic cleanup via Eio switches
+
+
{2 Message Flow}
+
+
1. Create a client with {!create}
+
2. Send messages with {!query} or {!send_message}
+
3. Receive responses with {!receive} or {!receive_all}
+
4. Continue multi-turn conversations by sending more messages
+
5. Client automatically cleans up when the switch exits
+
+
{2 Advanced Features}
+
+
- Permission discovery mode for understanding required permissions
+
- Mid-conversation model switching and permission mode changes
+
- Server capability introspection *)
+
(** The log source for client operations *)
val src : Logs.Src.t
type t
+
(** The type of Claude clients. *)
-
val create :
-
?options:Options.t ->
-
sw:Eio.Switch.t ->
+
val create :
+
?options:Options.t ->
+
sw:Eio.Switch.t ->
process_mgr:_ Eio.Process.mgr ->
unit -> t
+
(** [create ?options ~sw ~process_mgr ()] creates a new Claude client.
+
+
@param options Configuration options (defaults to {!Options.default})
+
@param sw Eio switch for resource management
+
@param process_mgr Eio process manager for spawning the Claude CLI *)
val query : t -> string -> unit
+
(** [query t prompt] sends a text message to Claude.
+
+
This is a convenience function for simple string messages. For more
+
complex messages with tool results or multiple content blocks, use
+
{!send_message} instead. *)
+
val send_message : t -> Message.t -> unit
+
(** [send_message t msg] sends a message to Claude.
+
+
Supports all message types including user messages with tool results. *)
+
val send_user_message : t -> Message.User.t -> unit
+
(** [send_user_message t msg] sends a user message to Claude. *)
val receive : t -> Message.t Seq.t
+
(** [receive t] returns a lazy sequence of messages from Claude.
+
+
The sequence yields messages as they arrive from Claude, including:
+
- {!Message.Assistant} - Claude's responses
+
- {!Message.System} - System notifications
+
- {!Message.Result} - Final result with usage statistics
+
+
Control messages (permission requests, hook callbacks) are handled
+
internally and not yielded to the sequence. *)
+
val receive_all : t -> Message.t list
+
(** [receive_all t] collects all messages into a list.
+
+
This is a convenience function that consumes the {!receive} sequence.
+
Use this when you want to process all messages at once rather than
+
streaming them. *)
val interrupt : t -> unit
+
(** [interrupt t] sends an interrupt signal to stop Claude's execution. *)
val discover_permissions : t -> t
+
(** [discover_permissions t] enables permission discovery mode.
+
+
In discovery mode, all tool usage is logged but allowed. Use
+
{!get_discovered_permissions} to retrieve the list of permissions
+
that were requested during execution.
+
+
This is useful for understanding what permissions your prompt requires. *)
+
val get_discovered_permissions : t -> Permissions.Rule.t list
+
(** [get_discovered_permissions t] returns permissions discovered during execution.
+
+
Only useful after enabling {!discover_permissions}. *)
+
val with_permission_callback : t -> Permissions.callback -> t
+
(** [with_permission_callback t callback] updates the permission callback.
+
+
Allows dynamically changing the permission callback without recreating
+
the client. *)
+
+
(** {1 Dynamic Control Methods}
+
+
These methods allow you to change Claude's behavior mid-conversation
+
without recreating the client. This is useful for:
+
+
- Adjusting permission strictness based on user feedback
+
- Switching to faster/cheaper models for simple tasks
+
- Adapting to changing requirements during long conversations
+
- Introspecting server capabilities
+
+
{2 Example: Adaptive Permission Control}
+
+
{[
+
(* Start with strict permissions *)
+
let client = Client.create ~sw ~process_mgr
+
~options:(Options.default
+
|> Options.with_permission_mode Permissions.Mode.Default) ()
+
in
+
+
Client.query client "Analyze this code";
+
let _ = Client.receive_all client in
+
+
(* User approves, switch to auto-accept edits *)
+
Client.set_permission_mode client Permissions.Mode.Accept_edits;
+
+
Client.query client "Now refactor it";
+
let _ = Client.receive_all client in
+
]}
+
+
{2 Example: Model Switching for Efficiency}
+
+
{[
+
(* Use powerful model for complex analysis *)
+
let client = Client.create ~sw ~process_mgr
+
~options:(Options.default |> Options.with_model "claude-sonnet-4-5") ()
+
in
+
+
Client.query client "Design a new architecture for this system";
+
let _ = Client.receive_all client in
+
+
(* Switch to faster model for simple tasks *)
+
Client.set_model client "claude-haiku-4";
+
+
Client.query client "Now write a README";
+
let _ = Client.receive_all client in
+
]}
+
+
{2 Example: Server Introspection}
+
+
{[
+
let info = Client.get_server_info client in
+
Printf.printf "Claude CLI version: %s\n"
+
(Sdk_control.Server_info.version info);
+
Printf.printf "Capabilities: %s\n"
+
(String.concat ", " (Sdk_control.Server_info.capabilities info));
+
]} *)
+
+
val set_permission_mode : t -> Permissions.Mode.t -> unit
+
(** [set_permission_mode t mode] changes the permission mode mid-conversation.
+
+
This allows switching between permission modes without recreating the client:
+
- {!Permissions.Mode.Default} - Prompt for all permissions
+
- {!Permissions.Mode.Accept_edits} - Auto-accept file edits
+
- {!Permissions.Mode.Plan} - Planning mode with restricted execution
+
- {!Permissions.Mode.Bypass_permissions} - Skip all permission checks
+
+
@raise Failure if the server returns an error *)
+
+
val set_model : t -> Model.t -> unit
+
(** [set_model t model] switches to a different AI model mid-conversation.
+
+
Common models:
+
- [`Sonnet_4_5] - Most capable, balanced performance
+
- [`Opus_4] - Maximum capability for complex tasks
+
- [`Haiku_4] - Fast and cost-effective
+
+
@raise Failure if the model is invalid or unavailable *)
+
+
val set_model_string : t -> string -> unit
+
(** [set_model_string t model] switches to a different AI model using a string.
+
+
This is a convenience function that parses the string using {!Model.of_string}.
+
+
@raise Failure if the model is invalid or unavailable *)
+
+
val get_server_info : t -> Sdk_control.Server_info.t
+
(** [get_server_info t] retrieves server capabilities and metadata.
+
+
Returns information about:
+
- Server version string
+
- Available capabilities
+
- Supported commands
+
- Available output styles
+
+
Useful for feature detection and debugging.
+
+
@raise Failure if the server returns an error *)
+1 -1
claudeio/lib/dune
···
(library
(public_name claude)
(name claude)
-
(libraries eio eio.unix ezjsonm fmt logs))
+
(libraries eio eio.unix ezjsonm fmt logs jsont))
+65 -21
claudeio/lib/message.ml
···
end
module Assistant = struct
+
type error = [
+
| `Authentication_failed
+
| `Billing_error
+
| `Rate_limit
+
| `Invalid_request
+
| `Server_error
+
| `Unknown
+
]
+
+
let error_to_string = function
+
| `Authentication_failed -> "authentication_failed"
+
| `Billing_error -> "billing_error"
+
| `Rate_limit -> "rate_limit"
+
| `Invalid_request -> "invalid_request"
+
| `Server_error -> "server_error"
+
| `Unknown -> "unknown"
+
+
let error_of_string = function
+
| "authentication_failed" -> `Authentication_failed
+
| "billing_error" -> `Billing_error
+
| "rate_limit" -> `Rate_limit
+
| "invalid_request" -> `Invalid_request
+
| "server_error" -> `Server_error
+
| "unknown" | _ -> `Unknown
+
type t = {
content : Content_block.t list;
model : string;
+
error : error option;
}
-
-
let create ~content ~model = { content; model }
+
+
let create ~content ~model ?error () = { content; model; error }
let content t = t.content
let model t = t.model
+
let error t = t.error
let get_text_blocks t =
List.filter_map (function
···
String.concat "\n" (get_text_blocks t)
let to_json t =
+
let msg_fields = [
+
("content", `A (List.map Content_block.to_json t.content));
+
("model", `String t.model);
+
] in
+
let msg_fields = match t.error with
+
| Some err -> ("error", `String (error_to_string err)) :: msg_fields
+
| None -> msg_fields
+
in
`O [
("type", `String "assistant");
-
("message", `O [
-
("content", `A (List.map Content_block.to_json t.content));
-
("model", `String t.model);
-
]);
+
("message", `O msg_fields);
]
let of_json = function
| `O fields ->
let message = List.assoc "message" fields in
-
let content, model = match message with
+
let content, model, error = match message with
| `O msg_fields ->
-
let content =
+
let content =
match List.assoc "content" msg_fields with
| `A blocks -> List.map Content_block.of_json blocks
| _ -> raise (Invalid_argument "Assistant.of_json: invalid content")
in
let model = JU.assoc_string "model" msg_fields in
-
content, model
+
let error =
+
match JU.assoc_string_opt "error" msg_fields with
+
| Some err_str -> Some (error_of_string err_str)
+
| None -> None
+
in
+
content, model, error
| _ -> raise (Invalid_argument "Assistant.of_json: invalid message")
in
-
{ content; model }
+
{ content; model; error }
| _ -> raise (Invalid_argument "Assistant.of_json: expected object")
let pp fmt t =
···
total_cost_usd : float option;
usage : Usage.t option;
result : string option;
+
structured_output : value option;
}
-
let create ~subtype ~duration_ms ~duration_api_ms ~is_error ~num_turns
-
~session_id ?total_cost_usd ?usage ?result () =
+
let create ~subtype ~duration_ms ~duration_api_ms ~is_error ~num_turns
+
~session_id ?total_cost_usd ?usage ?result ?structured_output () =
{ subtype; duration_ms; duration_api_ms; is_error; num_turns;
-
session_id; total_cost_usd; usage; result }
+
session_id; total_cost_usd; usage; result; structured_output }
let subtype t = t.subtype
let duration_ms t = t.duration_ms
···
let total_cost_usd t = t.total_cost_usd
let usage t = t.usage
let result t = t.result
+
let structured_output t = t.structured_output
let to_json t =
let fields = [
···
| Some result -> ("result", `String result) :: fields
| None -> fields
in
+
let fields = match t.structured_output with
+
| Some output -> ("structured_output", output) :: fields
+
| None -> fields
+
in
`O fields
let of_json = function
···
let total_cost_usd = JU.assoc_float_opt "total_cost_usd" fields in
let usage = Option.map Usage.of_json (List.assoc_opt "usage" fields) in
let result = JU.assoc_string_opt "result" fields in
+
let structured_output = List.assoc_opt "structured_output" fields in
{ subtype; duration_ms; duration_api_ms; is_error; num_turns;
-
session_id; total_cost_usd; usage; result }
+
session_id; total_cost_usd; usage; result; structured_output }
| _ -> raise (Invalid_argument "Result.of_json: expected object")
let pp fmt t =
···
let user_with_tool_result ~tool_use_id ~content ?is_error () =
User (User.create_with_tool_result ~tool_use_id ~content ?is_error ())
-
let assistant ~content ~model = Assistant (Assistant.create ~content ~model)
-
let assistant_text ~text ~model =
-
Assistant (Assistant.create ~content:[Content_block.text text] ~model)
+
let assistant ~content ~model ?error () = Assistant (Assistant.create ~content ~model ?error ())
+
let assistant_text ~text ~model ?error () =
+
Assistant (Assistant.create ~content:[Content_block.text text] ~model ?error ())
let system ~subtype ~data = System (System.create ~subtype ~data)
let system_init ~session_id =
···
let data = System.Data.of_assoc [("error", `String error)] in
System (System.create ~subtype:"error" ~data)
-
let result ~subtype ~duration_ms ~duration_api_ms ~is_error ~num_turns
-
~session_id ?total_cost_usd ?usage ?result () =
-
Result (Result.create ~subtype ~duration_ms ~duration_api_ms ~is_error
-
~num_turns ~session_id ?total_cost_usd ?usage ?result ())
+
let result ~subtype ~duration_ms ~duration_api_ms ~is_error ~num_turns
+
~session_id ?total_cost_usd ?usage ?result ?structured_output () =
+
Result (Result.create ~subtype ~duration_ms ~duration_api_ms ~is_error
+
~num_turns ~session_id ?total_cost_usd ?usage ?result ?structured_output ())
let to_json = function
| User t -> User.to_json t
+59 -33
claudeio/lib/message.mli
···
module Assistant : sig
(** Messages from Claude assistant. *)
-
+
+
type error = [
+
| `Authentication_failed (** Authentication with Claude API failed *)
+
| `Billing_error (** Billing or account issue *)
+
| `Rate_limit (** Rate limit exceeded *)
+
| `Invalid_request (** Request was invalid *)
+
| `Server_error (** Internal server error *)
+
| `Unknown (** Unknown error type *)
+
]
+
(** The type of assistant message errors based on Python SDK error types. *)
+
+
val error_to_string : error -> string
+
(** [error_to_string err] converts an error to its string representation. *)
+
+
val error_of_string : string -> error
+
(** [error_of_string s] parses an error string. Unknown strings become [`Unknown]. *)
+
type t
(** The type of assistant messages. *)
-
-
val create : content:Content_block.t list -> model:string -> t
-
(** [create ~content ~model] creates an assistant message.
+
+
val create : content:Content_block.t list -> model:string -> ?error:error -> unit -> t
+
(** [create ~content ~model ?error ()] creates an assistant message.
@param content List of content blocks in the response
-
@param model The model identifier used for the response *)
-
+
@param model The model identifier used for the response
+
@param error Optional error that occurred during message generation *)
+
val content : t -> Content_block.t list
(** [content t] returns the content blocks of the assistant message. *)
-
+
val model : t -> string
(** [model t] returns the model identifier. *)
+
+
val error : t -> error option
+
(** [error t] returns the optional error that occurred during message generation. *)
val get_text_blocks : t -> string list
(** [get_text_blocks t] extracts all text content from the message. *)
···
type t
(** The type of result messages. *)
-
val create :
-
subtype:string ->
-
duration_ms:int ->
-
duration_api_ms:int ->
-
is_error:bool ->
-
num_turns:int ->
-
session_id:string ->
-
?total_cost_usd:float ->
-
?usage:Usage.t ->
-
?result:string ->
+
val create :
+
subtype:string ->
+
duration_ms:int ->
+
duration_api_ms:int ->
+
is_error:bool ->
+
num_turns:int ->
+
session_id:string ->
+
?total_cost_usd:float ->
+
?usage:Usage.t ->
+
?result:string ->
+
?structured_output:Ezjsonm.value ->
unit -> t
(** [create ~subtype ~duration_ms ~duration_api_ms ~is_error ~num_turns
~session_id ?total_cost_usd ?usage ?result ()] creates a result message.
···
@param session_id Unique session identifier
@param total_cost_usd Optional total cost in USD
@param usage Optional usage statistics as JSON
-
@param result Optional result string *)
+
@param result Optional result string
+
@param structured_output Optional structured JSON output from Claude *)
val subtype : t -> string
(** [subtype t] returns the subtype of the result. *)
···
val result : t -> string option
(** [result t] returns the optional result string. *)
-
+
+
val structured_output : t -> Ezjsonm.value option
+
(** [structured_output t] returns the optional structured JSON output. *)
+
val to_json : t -> Ezjsonm.value
(** [to_json t] converts the result message to its JSON representation. *)
···
(** [user_with_tool_result ~tool_use_id ~content ?is_error ()] creates a user message
containing a tool result. *)
-
val assistant : content:Content_block.t list -> model:string -> t
-
(** [assistant ~content ~model] creates an assistant message. *)
+
val assistant : content:Content_block.t list -> model:string -> ?error:Assistant.error -> unit -> t
+
(** [assistant ~content ~model ?error ()] creates an assistant message. *)
-
val assistant_text : text:string -> model:string -> t
-
(** [assistant_text ~text ~model] creates an assistant message with only text content. *)
+
val assistant_text : text:string -> model:string -> ?error:Assistant.error -> unit -> t
+
(** [assistant_text ~text ~model ?error ()] creates an assistant message with only text content. *)
val system : subtype:string -> data:System.Data.t -> t
(** [system ~subtype ~data] creates a system message. *)
···
val system_error : error:string -> t
(** [system_error ~error] creates a system error message. *)
-
val result :
-
subtype:string ->
-
duration_ms:int ->
-
duration_api_ms:int ->
-
is_error:bool ->
-
num_turns:int ->
-
session_id:string ->
-
?total_cost_usd:float ->
-
?usage:Result.Usage.t ->
-
?result:string ->
+
val result :
+
subtype:string ->
+
duration_ms:int ->
+
duration_api_ms:int ->
+
is_error:bool ->
+
num_turns:int ->
+
session_id:string ->
+
?total_cost_usd:float ->
+
?usage:Result.Usage.t ->
+
?result:string ->
+
?structured_output:Ezjsonm.value ->
unit -> t
(** [result ~subtype ~duration_ms ~duration_api_ms ~is_error ~num_turns
~session_id ?total_cost_usd ?usage ?result ()] creates a result message. *)
+50 -7
claudeio/lib/options.ml
···
let src = Logs.Src.create "claude.options" ~doc:"Claude configuration options"
module Log = (val Logs.src_log src : Logs.LOG)
+
type setting_source = User | Project | Local
+
type t = {
allowed_tools : string list;
disallowed_tools : string list;
···
append_system_prompt : string option;
permission_mode : Permissions.Mode.t option;
permission_callback : Permissions.callback option;
-
model : string option;
+
model : Model.t option;
cwd : Eio.Fs.dir_ty Eio.Path.t option;
env : (string * string) list;
continue_conversation : bool;
···
extra_args : (string * string option) list;
debug_stderr : Eio.Flow.sink_ty Eio.Flow.sink option;
hooks : Hooks.config option;
+
max_budget_usd : float option;
+
fallback_model : Model.t option;
+
setting_sources : setting_source list option;
+
max_buffer_size : int option;
+
user : string option;
+
output_format : Structured_output.t option;
}
let default = {
···
extra_args = [];
debug_stderr = None;
hooks = None;
+
max_budget_usd = None;
+
fallback_model = None;
+
setting_sources = None;
+
max_buffer_size = None;
+
user = None;
+
output_format = None;
}
let create
···
?(extra_args = [])
?debug_stderr
?hooks
+
?max_budget_usd
+
?fallback_model
+
?setting_sources
+
?max_buffer_size
+
?user
+
?output_format
() =
{ allowed_tools; disallowed_tools; max_thinking_tokens;
system_prompt; append_system_prompt; permission_mode;
permission_callback; model; cwd; env;
continue_conversation; resume; max_turns;
permission_prompt_tool_name; settings; add_dirs;
-
extra_args; debug_stderr; hooks }
+
extra_args; debug_stderr; hooks;
+
max_budget_usd; fallback_model; setting_sources;
+
max_buffer_size; user; output_format }
let allowed_tools t = t.allowed_tools
let disallowed_tools t = t.disallowed_tools
···
let extra_args t = t.extra_args
let debug_stderr t = t.debug_stderr
let hooks t = t.hooks
+
let max_budget_usd t = t.max_budget_usd
+
let fallback_model t = t.fallback_model
+
let setting_sources t = t.setting_sources
+
let max_buffer_size t = t.max_buffer_size
+
let user t = t.user
+
let output_format t = t.output_format
let with_allowed_tools tools t = { t with allowed_tools = tools }
let with_disallowed_tools tools t = { t with disallowed_tools = tools }
···
let with_permission_mode mode t = { t with permission_mode = Some mode }
let with_permission_callback callback t = { t with permission_callback = Some callback }
let with_model model t = { t with model = Some model }
+
let with_model_string model t = { t with model = Some (Model.of_string model) }
let with_cwd cwd t = { t with cwd = Some cwd }
let with_env env t = { t with env }
let with_continue_conversation continue t = { t with continue_conversation = continue }
···
let with_extra_args args t = { t with extra_args = args }
let with_debug_stderr sink t = { t with debug_stderr = Some sink }
let with_hooks hooks t = { t with hooks = Some hooks }
+
let with_max_budget_usd budget t = { t with max_budget_usd = Some budget }
+
let with_fallback_model model t = { t with fallback_model = Some model }
+
let with_fallback_model_string model t = { t with fallback_model = Some (Model.of_string model) }
+
let with_setting_sources sources t = { t with setting_sources = Some sources }
+
let with_no_settings t = { t with setting_sources = Some [] }
+
let with_max_buffer_size size t = { t with max_buffer_size = Some size }
+
let with_user user t = { t with user = Some user }
+
let with_output_format format t = { t with output_format = Some format }
let to_json t =
let fields = [] in
···
| None -> fields
in
let fields = match t.model with
-
| Some m -> ("model", `String m) :: fields
+
| Some m -> ("model", `String (Model.to_string m)) :: fields
| None -> fields
in
let fields =
···
try Some (Permissions.Mode.of_json (List.assoc "permission_mode" fields))
with Not_found -> None
in
-
let model =
-
try Some (get_string (List.assoc "model" fields))
+
let model =
+
try Some (Model.of_string (get_string (List.assoc "model" fields)))
with Not_found -> None
in
let env =
···
add_dirs = [];
extra_args = [];
debug_stderr = None;
-
hooks = None; }
+
hooks = None;
+
max_budget_usd = None;
+
fallback_model = None;
+
setting_sources = None;
+
max_buffer_size = None;
+
user = None;
+
output_format = None; }
| _ -> raise (Invalid_argument "Options.of_json: expected object")
let pp fmt t =
···
Fmt.(option string) t.system_prompt
Fmt.(option string) t.append_system_prompt
Fmt.(option Permissions.Mode.pp) t.permission_mode
-
Fmt.(option string) t.model
+
Fmt.(option Model.pp) t.model
Fmt.(list (pair string string)) t.env
let log_options t =
+177 -10
claudeio/lib/options.mli
···
(** Configuration options for Claude sessions.
-
-
This module provides configuration options for controlling Claude's
-
behavior, including tool permissions, system prompts, models, and
-
execution environment. *)
+
+
This module provides comprehensive configuration options for controlling
+
Claude's behavior, including tool permissions, system prompts, models,
+
execution environment, cost controls, and structured outputs.
+
+
{2 Overview}
+
+
Options control all aspects of Claude's behavior:
+
- {b Permissions}: Which tools Claude can use and how permission is granted
+
- {b Models}: Which AI model to use and fallback options
+
- {b Environment}: Working directory, environment variables, settings
+
- {b Cost Control}: Budget limits to prevent runaway spending
+
- {b Hooks}: Intercept and modify tool execution
+
- {b Structured Output}: JSON schema validation for responses
+
- {b Session Management}: Continue or resume conversations
+
+
{2 Builder Pattern}
+
+
Options use a functional builder pattern - each [with_*] function returns
+
a new options value with the specified field updated:
+
+
{[
+
let options = Options.default
+
|> Options.with_model "claude-sonnet-4-5"
+
|> Options.with_max_budget_usd 1.0
+
|> Options.with_permission_mode Permissions.Mode.Accept_edits
+
]}
+
+
{2 Common Configuration Scenarios}
+
+
{3 CI/CD: Isolated, Reproducible Builds}
+
+
{[
+
let ci_config = Options.default
+
|> Options.with_no_settings (* Ignore user config *)
+
|> Options.with_max_budget_usd 0.50 (* 50 cent limit *)
+
|> Options.with_permission_mode
+
Permissions.Mode.Bypass_permissions
+
|> Options.with_model "claude-haiku-4"
+
]}
+
+
{3 Production: Cost Control with Fallback}
+
+
{[
+
let prod_config = Options.default
+
|> Options.with_model "claude-sonnet-4-5"
+
|> Options.with_fallback_model "claude-haiku-4"
+
|> Options.with_max_budget_usd 10.0 (* $10 daily limit *)
+
|> Options.with_max_buffer_size 5_000_000
+
]}
+
+
{3 Development: User Settings with Overrides}
+
+
{[
+
let dev_config = Options.default
+
|> Options.with_setting_sources [User; Project]
+
|> Options.with_max_budget_usd 1.0
+
|> Options.with_permission_mode Permissions.Mode.Default
+
]}
+
+
{3 Structured Output: Type-Safe Responses}
+
+
{[
+
let schema = Ezjsonm.(`O [
+
("type", `String "object");
+
("properties", `O [
+
("count", `O [("type", `String "integer")]);
+
("has_tests", `O [("type", `String "boolean")]);
+
]);
+
])
+
let format = Structured_output.of_json_schema schema
+
+
let analysis_config = Options.default
+
|> Options.with_output_format format
+
|> Options.with_allowed_tools ["Read"; "Glob"; "Grep"]
+
]}
+
+
{2 Advanced Options}
+
+
{3 Budget Control}
+
+
Use {!with_max_budget_usd} to set hard spending limits. Claude will
+
terminate the session if the budget is exceeded, preventing runaway costs.
+
+
{3 Settings Isolation}
+
+
Use {!with_setting_sources} or {!with_no_settings} to control which
+
configuration files are loaded:
+
- [User] - ~/.claude/config
+
- [Project] - .claude/ in project root
+
- [Local] - Current directory settings
+
- [Some \[\]] (via {!with_no_settings}) - No settings, fully isolated
+
+
This is critical for reproducible builds in CI/CD environments.
+
+
{3 Model Fallback}
+
+
Use {!with_fallback_model} to specify an alternative model when the
+
primary model is unavailable or overloaded. This improves reliability. *)
open Ezjsonm
(** The log source for options operations *)
val src : Logs.Src.t
+
(** {1 Types} *)
+
+
type setting_source = User | Project | Local
+
(** Setting source determines which configuration files to load.
+
- [User]: Load user-level settings from ~/.claude/config
+
- [Project]: Load project-level settings from .claude/ in project root
+
- [Local]: Load local settings from current directory *)
+
type t
(** The type of configuration options. *)
···
?append_system_prompt:string ->
?permission_mode:Permissions.Mode.t ->
?permission_callback:Permissions.callback ->
-
?model:string ->
+
?model:Model.t ->
?cwd:Eio.Fs.dir_ty Eio.Path.t ->
?env:(string * string) list ->
?continue_conversation:bool ->
···
?extra_args:(string * string option) list ->
?debug_stderr:Eio.Flow.sink_ty Eio.Flow.sink ->
?hooks:Hooks.config ->
+
?max_budget_usd:float ->
+
?fallback_model:Model.t ->
+
?setting_sources:setting_source list ->
+
?max_buffer_size:int ->
+
?user:string ->
+
?output_format:Structured_output.t ->
unit -> t
(** [create ?allowed_tools ?disallowed_tools ?max_thinking_tokens ?system_prompt
?append_system_prompt ?permission_mode ?permission_callback ?model ?cwd ?env
?continue_conversation ?resume ?max_turns ?permission_prompt_tool_name ?settings
-
?add_dirs ?extra_args ?debug_stderr ()]
+
?add_dirs ?extra_args ?debug_stderr ?hooks ?max_budget_usd ?fallback_model
+
?setting_sources ?max_buffer_size ?user ()]
creates a new configuration.
@param allowed_tools List of explicitly allowed tool names
@param disallowed_tools List of explicitly disallowed tool names
···
@param settings Path to settings file
@param add_dirs Additional directories to allow access to
@param extra_args Additional CLI flags to pass through
-
@param debug_stderr Sink for debug output when debug-to-stderr is set *)
+
@param debug_stderr Sink for debug output when debug-to-stderr is set
+
@param hooks Hooks configuration for event interception
+
@param max_budget_usd Hard spending limit in USD (terminates on exceed)
+
@param fallback_model Automatic fallback on primary model unavailability
+
@param setting_sources Control which settings load (user/project/local)
+
@param max_buffer_size Control for stdout buffer size in bytes
+
@param user Unix user for subprocess execution
+
@param output_format Optional structured output format specification *)
(** {1 Accessors} *)
···
val permission_callback : t -> Permissions.callback option
(** [permission_callback t] returns the optional permission callback. *)
-
val model : t -> string option
+
val model : t -> Model.t option
(** [model t] returns the optional model override. *)
val cwd : t -> Eio.Fs.dir_ty Eio.Path.t option
···
val hooks : t -> Hooks.config option
(** [hooks t] returns the optional hooks configuration. *)
+
val max_budget_usd : t -> float option
+
(** [max_budget_usd t] returns the optional spending limit in USD. *)
+
+
val fallback_model : t -> Model.t option
+
(** [fallback_model t] returns the optional fallback model. *)
+
+
val setting_sources : t -> setting_source list option
+
(** [setting_sources t] returns the optional list of setting sources to load. *)
+
+
val max_buffer_size : t -> int option
+
(** [max_buffer_size t] returns the optional stdout buffer size in bytes. *)
+
+
val user : t -> string option
+
(** [user t] returns the optional Unix user for subprocess execution. *)
+
+
val output_format : t -> Structured_output.t option
+
(** [output_format t] returns the optional structured output format. *)
+
(** {1 Builders} *)
val with_allowed_tools : string list -> t -> t
···
val with_permission_callback : Permissions.callback -> t -> t
(** [with_permission_callback callback t] sets the permission callback. *)
-
val with_model : string -> t -> t
-
(** [with_model model t] sets the model override. *)
+
val with_model : Model.t -> t -> t
+
(** [with_model model t] sets the model override using a typed Model.t. *)
+
+
val with_model_string : string -> t -> t
+
(** [with_model_string model t] sets the model override from a string.
+
The string is parsed using {!Model.of_string}. *)
val with_cwd : Eio.Fs.dir_ty Eio.Path.t -> t -> t
(** [with_cwd cwd t] sets the working directory. *)
···
val with_hooks : Hooks.config -> t -> t
(** [with_hooks hooks t] sets the hooks configuration. *)
+
+
val with_max_budget_usd : float -> t -> t
+
(** [with_max_budget_usd budget t] sets the maximum spending limit in USD.
+
The session will terminate if this limit is exceeded. *)
+
+
val with_fallback_model : Model.t -> t -> t
+
(** [with_fallback_model model t] sets the fallback model using a typed Model.t. *)
+
+
val with_fallback_model_string : string -> t -> t
+
(** [with_fallback_model_string model t] sets the fallback model from a string.
+
The string is parsed using {!Model.of_string}. *)
+
+
val with_setting_sources : setting_source list -> t -> t
+
(** [with_setting_sources sources t] sets which configuration sources to load.
+
Use empty list for isolated environments (e.g., CI/CD). *)
+
+
val with_no_settings : t -> t
+
(** [with_no_settings t] disables all settings loading (user, project, local).
+
Useful for CI/CD environments where you want isolated, reproducible behavior. *)
+
+
val with_max_buffer_size : int -> t -> t
+
(** [with_max_buffer_size size t] sets the maximum stdout buffer size in bytes. *)
+
+
val with_user : string -> t -> t
+
(** [with_user user t] sets the Unix user for subprocess execution. *)
+
+
val with_output_format : Structured_output.t -> t -> t
+
(** [with_output_format format t] sets the structured output format. *)
(** {1 Serialization} *)
+91 -4
claudeio/lib/sdk_control.ml
···
server_name : string;
message : value;
}
-
+
+
type set_model = {
+
subtype : [`Set_model];
+
model : string;
+
}
+
+
type get_server_info = {
+
subtype : [`Get_server_info];
+
}
+
type t =
| Interrupt of interrupt
| Permission of permission
···
| Set_permission_mode of set_permission_mode
| Hook_callback of hook_callback
| Mcp_message of mcp_message
+
| Set_model of set_model
+
| Get_server_info of get_server_info
let interrupt () = Interrupt { subtype = `Interrupt }
···
server_name;
message;
}
-
+
+
let set_model ~model =
+
Set_model { subtype = `Set_model; model }
+
+
let get_server_info () =
+
Get_server_info { subtype = `Get_server_info }
+
let to_json = function
| Interrupt _ ->
`O [("subtype", `String "interrupt")]
···
("server_name", `String m.server_name);
("message", m.message);
]
-
+
| Set_model s ->
+
`O [
+
("subtype", `String "set_model");
+
("model", `String s.model);
+
]
+
| Get_server_info _ ->
+
`O [("subtype", `String "get_server_info")]
+
let of_json = function
| `O fields ->
let subtype = JU.assoc_string "subtype" fields in
···
server_name;
message;
}
+
| "set_model" ->
+
let model = JU.assoc_string "model" fields in
+
Set_model { subtype = `Set_model; model }
+
| "get_server_info" ->
+
Get_server_info { subtype = `Get_server_info }
| _ -> raise (Invalid_argument ("Unknown request subtype: " ^ subtype)))
| _ -> raise (Invalid_argument "Request.of_json: expected object")
···
| Mcp_message m ->
Fmt.pf fmt "@[<2>McpMessage@ { server = %S }@]"
m.server_name
+
| Set_model s ->
+
Fmt.pf fmt "@[<2>SetModel@ { model = %S }@]" s.model
+
| Get_server_info _ ->
+
Fmt.pf fmt "@[<2>GetServerInfo@]"
end
module Response = struct
···
Log.debug (fun m -> m "SDK control request: %a" Request.pp req)
let log_response resp =
-
Log.debug (fun m -> m "SDK control response: %a" Response.pp resp)
+
Log.debug (fun m -> m "SDK control response: %a" Response.pp resp)
+
+
(** Server information *)
+
module Server_info = struct
+
type t = {
+
version : string;
+
capabilities : string list;
+
commands : string list;
+
output_styles : string list;
+
}
+
+
let create ~version ~capabilities ~commands ~output_styles =
+
{ version; capabilities; commands; output_styles }
+
+
let version t = t.version
+
let capabilities t = t.capabilities
+
let commands t = t.commands
+
let output_styles t = t.output_styles
+
+
let of_json = function
+
| `O fields ->
+
let version = JU.assoc_string "version" fields in
+
let capabilities =
+
match List.assoc_opt "capabilities" fields with
+
| Some (`A lst) -> List.map Ezjsonm.get_string lst
+
| _ -> []
+
in
+
let commands =
+
match List.assoc_opt "commands" fields with
+
| Some (`A lst) -> List.map Ezjsonm.get_string lst
+
| _ -> []
+
in
+
let output_styles =
+
match List.assoc_opt "outputStyles" fields with
+
| Some (`A lst) -> List.map Ezjsonm.get_string lst
+
| _ -> []
+
in
+
{ version; capabilities; commands; output_styles }
+
| _ -> raise (Invalid_argument "Server_info.of_json: expected object")
+
+
let to_json t =
+
`O [
+
("version", `String t.version);
+
("capabilities", `A (List.map (fun s -> `String s) t.capabilities));
+
("commands", `A (List.map (fun s -> `String s) t.commands));
+
("outputStyles", `A (List.map (fun s -> `String s) t.output_styles));
+
]
+
+
let pp fmt t =
+
Fmt.pf fmt "@[<2>ServerInfo@ { version = %S;@ capabilities = [%a];@ commands = [%a];@ output_styles = [%a] }@]"
+
t.version
+
Fmt.(list ~sep:(any ", ") (quote string)) t.capabilities
+
Fmt.(list ~sep:(any ", ") (quote string)) t.commands
+
Fmt.(list ~sep:(any ", ") (quote string)) t.output_styles
+
end
+149 -6
claudeio/lib/sdk_control.mli
···
(** SDK Control Protocol for Claude.
-
-
This module defines the typed SDK control protocol for communication
-
between the SDK and Claude, including request and response types. *)
+
+
This module defines the typed SDK control protocol for bidirectional
+
communication between the SDK and the Claude CLI. It handles:
+
+
- Permission requests (tool usage authorization)
+
- Hook callbacks (intercepting and modifying tool execution)
+
- Dynamic control (changing settings mid-conversation)
+
- Server introspection (querying capabilities)
+
+
{2 Protocol Overview}
+
+
The SDK control protocol is a JSON-based request/response protocol that
+
runs alongside the main message stream. It enables:
+
+
1. {b Callbacks}: Claude asks the SDK for permission or hook execution
+
2. {b Control}: SDK changes Claude's behavior dynamically
+
3. {b Introspection}: SDK queries server metadata
+
+
{2 Request/Response Flow}
+
+
{v
+
SDK Claude CLI
+
| |
+
|-- Initialize (with hooks) --> |
+
|<-- Permission Request --------| (for tool usage)
+
|-- Allow/Deny Response ------> |
+
| |
+
|<-- Hook Callback -------------| (pre/post tool)
+
|-- Hook Result -------------> |
+
| |
+
|-- Set Model ---------------> | (dynamic control)
+
|<-- Success Response ----------|
+
| |
+
|-- Get Server Info ----------> |
+
|<-- Server Info Response ------|
+
v}
+
+
{2 Usage}
+
+
Most users won't interact with this module directly. The {!Client} module
+
handles the protocol automatically. However, this module is exposed for:
+
+
- Understanding the control protocol
+
- Implementing custom control logic
+
- Debugging control message flow
+
- Advanced SDK extensions
+
+
{2 Dynamic Control Examples}
+
+
See {!Client.set_permission_mode}, {!Client.set_model}, and
+
{!Client.get_server_info} for high-level APIs that use this protocol. *)
open Ezjsonm
···
message : value;
}
(** MCP server message request. *)
-
+
+
type set_model = {
+
subtype : [`Set_model];
+
model : string;
+
}
+
(** Request to change the AI model. *)
+
+
type get_server_info = {
+
subtype : [`Get_server_info];
+
}
+
(** Request to get server information. *)
+
type t =
| Interrupt of interrupt
| Permission of permission
···
| Set_permission_mode of set_permission_mode
| Hook_callback of hook_callback
| Mcp_message of mcp_message
+
| Set_model of set_model
+
| Get_server_info of get_server_info
(** The type of SDK control requests. *)
val interrupt : unit -> t
···
val mcp_message : server_name:string -> message:value -> t
(** [mcp_message ~server_name ~message] creates an MCP message request. *)
-
+
+
val set_model : model:string -> t
+
(** [set_model ~model] creates a model change request. *)
+
+
val get_server_info : unit -> t
+
(** [get_server_info ()] creates a server info request. *)
+
val to_json : t -> value
(** [to_json t] converts a request to JSON. *)
···
(** [log_request req] logs an SDK control request. *)
val log_response : Response.t -> unit
-
(** [log_response resp] logs an SDK control response. *)
+
(** [log_response resp] logs an SDK control response. *)
+
+
(** {1 Server Information}
+
+
Server information provides metadata about the Claude CLI server,
+
including version, capabilities, available commands, and output styles.
+
+
{2 Use Cases}
+
+
- Feature detection: Check if specific capabilities are available
+
- Version compatibility: Ensure minimum version requirements
+
- Debugging: Log server information for troubleshooting
+
- Dynamic adaptation: Adjust SDK behavior based on capabilities
+
+
{2 Example}
+
+
{[
+
let info = Client.get_server_info client in
+
Printf.printf "Claude CLI version: %s\n"
+
(Server_info.version info);
+
+
if List.mem "structured-output" (Server_info.capabilities info) then
+
Printf.printf "Structured output is supported\n"
+
else
+
Printf.printf "Structured output not available\n";
+
]} *)
+
+
module Server_info : sig
+
(** Server information and capabilities. *)
+
+
type t = {
+
version : string;
+
(** Server version string (e.g., "2.0.0") *)
+
+
capabilities : string list;
+
(** Available server capabilities (e.g., "hooks", "structured-output") *)
+
+
commands : string list;
+
(** Available CLI commands *)
+
+
output_styles : string list;
+
(** Supported output formats (e.g., "json", "stream-json") *)
+
}
+
(** Server metadata and capabilities.
+
+
This information is useful for feature detection and debugging. *)
+
+
val create :
+
version:string ->
+
capabilities:string list ->
+
commands:string list ->
+
output_styles:string list ->
+
t
+
(** [create ~version ~capabilities ~commands ~output_styles] creates server info. *)
+
+
val version : t -> string
+
(** [version t] returns the server version. *)
+
+
val capabilities : t -> string list
+
(** [capabilities t] returns the server capabilities. *)
+
+
val commands : t -> string list
+
(** [commands t] returns available commands. *)
+
+
val output_styles : t -> string list
+
(** [output_styles t] returns available output styles. *)
+
+
val of_json : value -> t
+
(** [of_json json] parses server info from JSON.
+
@raise Invalid_argument if the JSON is not valid server info. *)
+
+
val to_json : t -> value
+
(** [to_json t] converts server info to JSON. *)
+
+
val pp : Format.formatter -> t -> unit
+
(** [pp fmt t] pretty-prints the server info. *)
+
end
+45 -9
claudeio/lib/transport.ml
···
sw : Switch.t;
}
+
let setting_source_to_string = function
+
| Options.User -> "user"
+
| Options.Project -> "project"
+
| Options.Local -> "local"
+
let build_command ~claude_path ~options =
let cmd = [claude_path; "--output-format"; "stream-json"; "--verbose"] in
-
+
let cmd = match Options.system_prompt options with
| Some prompt -> cmd @ ["--system-prompt"; prompt]
| None -> cmd
in
-
+
let cmd = match Options.append_system_prompt options with
| Some prompt -> cmd @ ["--append-system-prompt"; prompt]
| None -> cmd
in
-
+
let cmd = match Options.allowed_tools options with
| [] -> cmd
| tools -> cmd @ ["--allowedTools"; String.concat "," tools]
in
-
+
let cmd = match Options.disallowed_tools options with
| [] -> cmd
| tools -> cmd @ ["--disallowedTools"; String.concat "," tools]
in
-
+
let cmd = match Options.model options with
-
| Some model -> cmd @ ["--model"; model]
+
| Some model -> cmd @ ["--model"; Model.to_string model]
| None -> cmd
in
-
+
let cmd = match Options.permission_mode options with
| Some mode ->
let mode_str = Permissions.Mode.to_string mode in
···
| None -> cmd
in
+
(* Advanced configuration options *)
+
let cmd = match Options.max_budget_usd options with
+
| Some budget -> cmd @ ["--max-budget-usd"; Float.to_string budget]
+
| None -> cmd
+
in
+
+
let cmd = match Options.fallback_model options with
+
| Some model -> cmd @ ["--fallback-model"; Model.to_string model]
+
| None -> cmd
+
in
+
+
let cmd = match Options.setting_sources options with
+
| Some sources ->
+
let sources_str = String.concat "," (List.map setting_source_to_string sources) in
+
cmd @ ["--setting-sources"; sources_str]
+
| None -> cmd
+
in
+
+
(* Add JSON Schema if specified *)
+
let cmd = match Options.output_format options with
+
| Some format ->
+
let schema = Structured_output.json_schema format in
+
let schema_str = Ezjsonm.value_to_string schema in
+
cmd @ ["--json-schema"; schema_str]
+
| None -> cmd
+
in
+
(* Use streaming input mode *)
cmd @ ["--input-format"; "stream-json"]
···
let stdin = (stdin_w :> Eio.Flow.sink_ty r) in
let stdin_close = (stdin_w :> [`Close | `Flow] r) in
-
let stdout = Eio.Buf_read.of_flow ~max_size:1_000_000 (stdout_r :> Eio.Flow.source_ty r) in
-
+
let max_size = match Options.max_buffer_size options with
+
| Some size -> size
+
| None -> 1_000_000 (* Default 1MB *)
+
in
+
let stdout = Eio.Buf_read.of_flow ~max_size (stdout_r :> Eio.Flow.source_ty r) in
+
{ process = P process; stdin; stdin_close; stdout; sw }
let send t json =
+1 -1
claudeio/test/camel_jokes.ml
···
let run_claude ~sw ~env name prompt =
Log.info (fun m -> m "๐Ÿช Starting %s..." name);
-
let options = Claude.Options.create ~model:"sonnet" ~allowed_tools:[] () in
+
let options = Claude.Options.create ~model:(Claude.Model.of_string "sonnet") ~allowed_tools:[] () in
let client = Claude.Client.create ~options ~sw
~process_mgr:env#process_mgr
+1 -1
claudeio/test/discovery_demo.ml
···
Log.app (fun m -> m "This will discover what permissions Claude needs.\n");
(* Create client with discovery mode *)
-
let options = Claude.Options.create ~model:"sonnet" () in
+
let options = Claude.Options.create ~model:(Claude.Model.of_string "sonnet") () in
let client = Claude.Client.discover_permissions
(Claude.Client.create ~options ~sw ~process_mgr:env#process_mgr ()) in
+27 -1
claudeio/test/dune
···
(public_name hooks_example)
(name hooks_example)
(modules hooks_example)
-
(libraries claude eio_main cmdliner logs logs.fmt fmt.tty fmt.cli logs.cli))
+
(libraries claude eio_main cmdliner logs logs.fmt fmt.tty fmt.cli logs.cli))
+
+
(executable
+
(public_name dynamic_control_demo)
+
(name dynamic_control_demo)
+
(modules dynamic_control_demo)
+
(libraries claude eio_main cmdliner logs logs.fmt fmt.tty fmt.cli logs.cli))
+
+
(executable
+
(public_name advanced_config_demo)
+
(name advanced_config_demo)
+
(modules advanced_config_demo)
+
(libraries claude eio_main logs logs.fmt fmt.tty))
+
+
(executable
+
(public_name structured_output_demo)
+
(name structured_output_demo)
+
(modules structured_output_demo)
+
(flags (:standard -w -33))
+
(libraries claude eio_main logs logs.fmt fmt.tty))
+
+
(executable
+
(public_name structured_output_simple)
+
(name structured_output_simple)
+
(modules structured_output_simple)
+
(flags (:standard -w -33))
+
(libraries claude eio_main logs logs.fmt fmt.tty))
+1 -1
claudeio/test/hooks_example.ml
···
in
let options = Claude.Options.create
-
~model:"sonnet"
+
~model:(Claude.Model.of_string "sonnet")
~hooks
() in
+1 -1
claudeio/test/permission_demo.ml
···
(* DON'T specify allowed_tools - let the permission callback handle everything.
The Default permission mode with a callback should send requests for all tools. *)
let options = Claude.Options.create
-
~model:"sonnet"
+
~model:(Claude.Model.of_string "sonnet")
~permission_mode:Claude.Permissions.Mode.Default
~permission_callback:interactive_permission_callback
() in
+1 -1
claudeio/test/simple_permission_test.ml
···
(* Create options with permission callback *)
let options = Claude.Options.create
-
~model:"sonnet"
+
~model:(Claude.Model.of_string "sonnet")
~permission_callback:auto_allow_callback
() in
+1 -1
claudeio/test/test_permissions.ml
···
(* Create options with custom permission callback *)
let options = Claude.Options.create
-
~model:"sonnet"
+
~model:(Claude.Model.of_string "sonnet")
~permission_callback:auto_allow_callback
() in
+2 -4
stack/immich/immich.ml
···
(** {1 Types} *)
-
type 'net t_internal = {
+
type t = {
base_url: string;
api_key: string;
-
requests_session: (float Eio.Time.clock_ty Eio.Resource.t, 'net Eio.Net.ty Eio.Resource.t) Requests.t;
+
requests_session: Requests.t;
}
-
-
type t = [`Generic | `Unix] t_internal
type person = {
id: string;
+1 -1
stack/immich/immich.mli
···
@return An Immich client configured for the specified instance
*)
val create :
-
requests_session:(float Eio.Time.clock_ty Eio.Resource.t, [`Generic | `Unix] Eio.Net.ty Eio.Resource.t) Requests.t ->
+
requests_session:Requests.t ->
base_url:string ->
api_key:string ->
t
+2 -4
stack/karakeep/karakeep.ml
···
let json_mems_empty = Jsont.Object ([], Jsont.Meta.none)
(** Type representing a Karakeep client session *)
-
type 'net t_internal = {
+
type t = {
api_key: string;
base_url: string;
-
http_client: (float Eio.Time.clock_ty Eio.Resource.t, 'net Eio.Net.ty Eio.Resource.t) Requests.t;
+
http_client: Requests.t;
}
-
-
type t = [`Generic | `Unix] t_internal
(** Create a new Karakeep client *)
let create ~sw ~env ~api_key ~base_url : t =
+2 -4
stack/peertubee/peertubee.ml
···
(** PeerTube API client implementation (Eio version) *)
(** Type representing a PeerTube client *)
-
type 'net t_internal = {
+
type t = {
base_url: string;
-
requests_session: (float Eio.Time.clock_ty Eio.Resource.t, 'net Eio.Net.ty Eio.Resource.t) Requests.t;
+
requests_session: Requests.t;
}
-
-
type t = [`Generic | `Unix] t_internal
(** Create a new PeerTube client *)
let create ~requests_session ~base_url : t =
+1 -1
stack/peertubee/peertubee.mli
···
@param base_url Base URL of the PeerTube instance
@return A PeerTube client configured for the specified instance *)
val create :
-
requests_session:(float Eio.Time.clock_ty Eio.Resource.t, [`Generic | `Unix] Eio.Net.ty Eio.Resource.t) Requests.t ->
+
requests_session:Requests.t ->
base_url:string ->
t
+5 -13
stack/requests/bin/ocurl.ml
···
let doc = "Show progress bar for downloads" in
Arg.(value & flag & info ["progress-bar"] ~doc)
-
let enable_cache =
-
let doc = "Enable HTTP response caching for GET and HEAD requests" in
-
let env_info = Cmdliner.Cmd.Env.info "OCURL_ENABLE_CACHE" in
-
Arg.(value & flag & info ["enable-cache"] ~env:env_info ~doc)
-
(* Logging setup *)
(* Setup logging using Logs_cli for standard logging options *)
let setup_log =
···
Error (url_str, exn)
(* Main function using Requests with concurrent fetching *)
-
let run_request env sw persist_cookies verify_tls enable_cache timeout follow_redirects max_redirects
+
let run_request env sw persist_cookies verify_tls timeout follow_redirects max_redirects
method_ urls headers data json_data output include_headers
auth _show_progress () =
···
(* Create requests instance with configuration *)
let timeout_obj = Option.map (fun t -> Requests.Timeout.create ~total:t ()) timeout in
-
let req = Requests.create ~sw ~xdg ~persist_cookies ~verify_tls ~enable_cache
+
let req = Requests.create ~sw ~xdg ~persist_cookies ~verify_tls
~follow_redirects ~max_redirects ?timeout:timeout_obj env in
(* Set authentication if provided *)
···
(* Main entry point *)
let main method_ urls headers data json_data output include_headers
-
auth show_progress persist_cookies verify_tls enable_cache
+
auth show_progress persist_cookies verify_tls
timeout follow_redirects max_redirects () =
Eio_main.run @@ fun env ->
Mirage_crypto_rng_unix.use_default ();
Switch.run @@ fun sw ->
-
run_request env sw persist_cookies verify_tls enable_cache timeout follow_redirects max_redirects
+
run_request env sw persist_cookies verify_tls timeout follow_redirects max_redirects
method_ urls headers data json_data output include_headers auth
show_progress ()
···
`P "Custom headers:";
`Pre " $(tname) -H 'Accept: application/json' -H 'X-Api-Key: secret' https://api.example.com";
`P "With persistent cookies:";
-
`Pre " $(tname) --persist-cookies --cache-dir ~/.ocurl https://example.com";
-
`P "Enable response caching:";
-
`Pre " $(tname) --enable-cache https://api.example.com";
+
`Pre " $(tname) --persist-cookies https://example.com";
`P "Disable TLS verification (insecure):";
`Pre " $(tname) --no-verify-tls https://self-signed.example.com";
`S "LOGGING OPTIONS";
···
show_progress $
Requests.Cmd.persist_cookies_term app_name $
Requests.Cmd.verify_tls_term app_name $
-
enable_cache $
Requests.Cmd.timeout_term app_name $
Requests.Cmd.follow_redirects_term app_name $
Requests.Cmd.max_redirects_term app_name $
-488
stack/requests/lib/cache.ml
···
-
let src = Logs.Src.create "requests.cache" ~doc:"HTTP cache with cacheio"
-
module Log = (val Logs.src_log src : Logs.LOG)
-
-
type cached_response = {
-
status : Cohttp.Code.status_code;
-
headers : Cohttp.Header.t;
-
body : string;
-
}
-
-
type t = {
-
sw : Eio.Switch.t;
-
enabled : bool;
-
cache_get_requests : bool;
-
cache_range_requests : bool;
-
cacheio : Cacheio.t option;
-
memory_cache : (string, cached_response * float) Hashtbl.t;
-
}
-
-
let create ~sw ~enabled ?(cache_get_requests=true) ?(cache_range_requests=true) ~cache_dir () =
-
let cacheio =
-
match cache_dir with
-
| Some dir when enabled ->
-
(try
-
Some (Cacheio.create ~base_dir:dir)
-
with e ->
-
Log.warn (fun m -> m "Failed to create cacheio backend: %s. Using memory cache only."
-
(Printexc.to_string e));
-
None)
-
| _ -> None
-
in
-
{ sw; enabled; cache_get_requests; cache_range_requests; cacheio;
-
memory_cache = Hashtbl.create 100 }
-
-
let make_cache_key ~method_ ~url ~headers =
-
let method_str = match method_ with
-
| `GET -> "GET" | `HEAD -> "HEAD"
-
| _ -> "OTHER"
-
in
-
let url_str = Uri.to_string url in
-
let range_str = match Cohttp.Header.get headers "range" with
-
| Some r -> "_range:" ^ r
-
| None -> ""
-
in
-
Printf.sprintf "%s_%s%s" method_str url_str range_str
-
-
let is_cacheable ~method_ ~status ~headers =
-
match method_ with
-
| `GET | `HEAD ->
-
let code = Cohttp.Code.code_of_status status in
-
if code >= 200 && code < 300 then
-
match Cohttp.Header.get headers "cache-control" with
-
| Some cc ->
-
let cc_lower = String.lowercase_ascii cc in
-
let rec contains s sub pos =
-
if pos + String.length sub > String.length s then false
-
else if String.sub s pos (String.length sub) = sub then true
-
else contains s sub (pos + 1)
-
in
-
not (contains cc_lower "no-store" 0 ||
-
contains cc_lower "no-cache" 0 ||
-
contains cc_lower "private" 0)
-
| None -> true
-
else
-
code = 301 || code = 308
-
| _ -> false
-
-
let parse_max_age headers =
-
match Cohttp.Header.get headers "cache-control" with
-
| Some cc ->
-
let parts = String.split_on_char ',' cc |> List.map String.trim in
-
List.find_map (fun part ->
-
let prefix = "max-age=" in
-
if String.starts_with ~prefix part then
-
let value = String.sub part (String.length prefix)
-
(String.length part - String.length prefix) in
-
try Some (float_of_string value) with _ -> None
-
else None
-
) parts
-
| None -> None
-
-
(* JSON codec for cache metadata *)
-
module Metadata = struct
-
type t = {
-
status_code : int;
-
headers : (string * string) list;
-
}
-
-
let make status_code headers = { status_code; headers }
-
let status_code t = t.status_code
-
let headers t = t.headers
-
-
let t_jsont =
-
let header_pair_jsont =
-
let dec x y = (x, y) in
-
let enc (x, y) i = if i = 0 then x else y in
-
Jsont.t2 ~dec ~enc Jsont.string
-
in
-
Jsont.Object.map ~kind:"CacheMetadata" make
-
|> Jsont.Object.mem "status_code" Jsont.int ~enc:status_code
-
|> Jsont.Object.mem "headers" (Jsont.list header_pair_jsont) ~enc:headers
-
|> Jsont.Object.finish
-
end
-
-
let serialize_metadata ~status ~headers =
-
let status_code = Cohttp.Code.code_of_status status in
-
let headers_assoc = Cohttp.Header.to_list headers in
-
let metadata = Metadata.make status_code headers_assoc in
-
match Jsont_bytesrw.encode_string' Metadata.t_jsont metadata with
-
| Ok s -> s
-
| Error e -> failwith (Fmt.str "Failed to serialize metadata: %s" (Jsont.Error.to_string e))
-
-
let deserialize_metadata json_str =
-
try
-
match Jsont_bytesrw.decode_string' Metadata.t_jsont json_str with
-
| Ok metadata ->
-
let status = Cohttp.Code.status_of_code (Metadata.status_code metadata) in
-
let headers = Cohttp.Header.of_list (Metadata.headers metadata) in
-
Some (status, headers)
-
| Error _ -> None
-
with _ -> None
-
-
let get t ~method_ ~url ~headers =
-
if not t.enabled then None
-
else if method_ = `GET && not t.cache_get_requests then None
-
else
-
let key = make_cache_key ~method_ ~url ~headers in
-
-
(* Try cacheio first *)
-
match t.cacheio with
-
| Some cache ->
-
(* Check for metadata entry *)
-
let metadata_key = key ^ ".meta" in
-
let body_key = key ^ ".body" in
-
-
if Cacheio.exists cache ~key:metadata_key && Cacheio.exists cache ~key:body_key then
-
Eio.Switch.run @@ fun sw ->
-
(* Read metadata *)
-
let metadata_opt = match Cacheio.get cache ~key:metadata_key ~sw with
-
| Some source ->
-
let buf = Buffer.create 256 in
-
Eio.Flow.copy source (Eio.Flow.buffer_sink buf);
-
deserialize_metadata (Buffer.contents buf)
-
| None -> None
-
in
-
-
(match metadata_opt with
-
| Some (status, resp_headers) ->
-
(* Read body *)
-
(match Cacheio.get cache ~key:body_key ~sw with
-
| Some source ->
-
let buf = Buffer.create 4096 in
-
Eio.Flow.copy source (Eio.Flow.buffer_sink buf);
-
let body = Buffer.contents buf in
-
Log.debug (fun m -> m "Cache hit for %s" (Uri.to_string url));
-
Some { status; headers = resp_headers; body }
-
| None ->
-
Log.debug (fun m -> m "Cache body missing for %s" (Uri.to_string url));
-
None)
-
| None ->
-
Log.debug (fun m -> m "Cache metadata missing for %s" (Uri.to_string url));
-
None)
-
else
-
(Log.debug (fun m -> m "Cache miss for %s" (Uri.to_string url));
-
None)
-
| None ->
-
(* Fall back to memory cache *)
-
match Hashtbl.find_opt t.memory_cache key with
-
| Some (response, expiry) when expiry > Unix.gettimeofday () ->
-
Log.debug (fun m -> m "Memory cache hit for %s" (Uri.to_string url));
-
Some response
-
| _ ->
-
Log.debug (fun m -> m "Cache miss for %s" (Uri.to_string url));
-
None
-
-
let get_stream t ~method_ ~url ~headers ~sw =
-
if not t.enabled then None
-
else if method_ = `GET && not t.cache_get_requests then None
-
else
-
let key = make_cache_key ~method_ ~url ~headers in
-
-
match t.cacheio with
-
| Some cache ->
-
let metadata_key = key ^ ".meta" in
-
let body_key = key ^ ".body" in
-
-
if Cacheio.exists cache ~key:metadata_key && Cacheio.exists cache ~key:body_key then
-
(* Read metadata first *)
-
let metadata_opt =
-
match Cacheio.get cache ~key:metadata_key ~sw with
-
| Some source ->
-
let buf = Buffer.create 256 in
-
Eio.Flow.copy source (Eio.Flow.buffer_sink buf);
-
deserialize_metadata (Buffer.contents buf)
-
| None -> None
-
in
-
-
(match metadata_opt with
-
| Some (status, resp_headers) ->
-
(* Return body stream directly *)
-
(match Cacheio.get cache ~key:body_key ~sw with
-
| Some source ->
-
Log.debug (fun m -> m "Streaming cache hit for %s" (Uri.to_string url));
-
Some (status, resp_headers, source)
-
| None -> None)
-
| None -> None)
-
else None
-
| None -> None
-
-
let put t ~method_ ~url ~request_headers ~status ~headers ~body =
-
if not t.enabled then ()
-
else if is_cacheable ~method_ ~status ~headers then
-
let key = make_cache_key ~method_ ~url ~headers:request_headers in
-
let ttl = parse_max_age headers in
-
-
Log.debug (fun m -> m "Caching response for %s (ttl: %s)"
-
(Uri.to_string url)
-
(match ttl with Some t -> Printf.sprintf "%.0fs" t | None -> "3600s"));
-
-
(match t.cacheio with
-
| Some cache ->
-
Eio.Switch.run @@ fun _sw ->
-
let metadata_key = key ^ ".meta" in
-
let metadata = serialize_metadata ~status ~headers in
-
let metadata_source = Eio.Flow.string_source metadata in
-
Cacheio.put cache ~key:metadata_key ~source:metadata_source ~ttl ();
-
-
let body_key = key ^ ".body" in
-
let body_source = Eio.Flow.string_source body in
-
Cacheio.put cache ~key:body_key ~source:body_source ~ttl ()
-
| None -> ());
-
-
let cached_resp = { status; headers; body } in
-
let expiry = Unix.gettimeofday () +. Option.value ttl ~default:3600.0 in
-
Hashtbl.replace t.memory_cache key (cached_resp, expiry)
-
-
let put_stream t ~method_ ~url ~request_headers ~status ~headers ~body_source ~ttl =
-
if not t.enabled then ()
-
else if is_cacheable ~method_ ~status ~headers then
-
let key = make_cache_key ~method_ ~url ~headers:request_headers in
-
-
Log.debug (fun m -> m "Caching streamed response for %s (ttl: %s)"
-
(Uri.to_string url)
-
(match ttl with Some t -> Printf.sprintf "%.0fs" t | None -> "3600s"));
-
-
match t.cacheio with
-
| Some cache ->
-
Eio.Switch.run @@ fun _sw ->
-
-
(* Store metadata *)
-
let metadata_key = key ^ ".meta" in
-
let metadata = serialize_metadata ~status ~headers in
-
let metadata_source = Eio.Flow.string_source metadata in
-
Cacheio.put cache ~key:metadata_key ~source:metadata_source ~ttl ();
-
-
(* Store body directly from source *)
-
let body_key = key ^ ".body" in
-
Cacheio.put cache ~key:body_key ~source:body_source ~ttl ()
-
| None -> ()
-
-
module Range = struct
-
type t = {
-
start : int64;
-
end_ : int64 option; (* None means to end of file *)
-
}
-
-
let of_header header =
-
(* Parse Range: bytes=start-end *)
-
let prefix = "bytes=" in
-
let prefix_len = String.length prefix in
-
if String.length header >= prefix_len &&
-
String.sub header 0 prefix_len = prefix then
-
let range_str = String.sub header prefix_len (String.length header - prefix_len) in
-
match String.split_on_char '-' range_str with
-
| [start; ""] ->
-
(* bytes=N- means from N to end *)
-
(try Some { start = Int64.of_string start; end_ = None }
-
with _ -> None)
-
| [start; end_] ->
-
(* bytes=N-M *)
-
(try Some {
-
start = Int64.of_string start;
-
end_ = Some (Int64.of_string end_)
-
}
-
with _ -> None)
-
| _ -> None
-
else None
-
-
let to_header t =
-
match t.end_ with
-
| None -> Printf.sprintf "bytes=%Ld-" t.start
-
| Some e -> Printf.sprintf "bytes=%Ld-%Ld" t.start e
-
-
let to_cacheio_range t ~total_size =
-
let end_ = match t.end_ with
-
| None -> Int64.pred total_size
-
| Some e -> min e (Int64.pred total_size)
-
in
-
(* Convert to Cacheio.Range.t *)
-
Cacheio.Range.create ~start:t.start ~end_
-
end
-
-
let download_range t ~sw ~url ~range ~on_chunk =
-
let range_header = Range.to_header range in
-
Log.debug (fun m -> m "Range request for %s: %s"
-
(Uri.to_string url) range_header);
-
-
match t.cacheio with
-
| Some cache ->
-
let key = Uri.to_string url in
-
let cacheio_range = Range.to_cacheio_range range ~total_size:Int64.max_int in
-
-
(match Cacheio.get_range cache ~key ~range:cacheio_range ~sw with
-
| `Complete source ->
-
let rec read_chunks () =
-
let chunk = Cstruct.create 8192 in
-
try
-
let n = Eio.Flow.single_read source chunk in
-
if n > 0 then begin
-
on_chunk (Cstruct.to_string ~off:0 ~len:n chunk);
-
read_chunks ()
-
end
-
with End_of_file -> ()
-
in
-
read_chunks ();
-
Some true
-
| `Chunks chunk_sources ->
-
List.iter (fun (_range, source) ->
-
let rec read_chunk () =
-
let chunk = Cstruct.create 8192 in
-
try
-
let n = Eio.Flow.single_read source chunk in
-
if n > 0 then begin
-
on_chunk (Cstruct.to_string ~off:0 ~len:n chunk);
-
read_chunk ()
-
end
-
with End_of_file -> ()
-
in
-
read_chunk ()
-
) chunk_sources;
-
Some true
-
| `Not_found -> None)
-
| None -> None
-
-
let put_chunk t ~url ~range ~data =
-
if not t.enabled || not t.cache_range_requests then ()
-
else
-
match t.cacheio with
-
| Some cache ->
-
let key = Uri.to_string url in
-
let cacheio_range = Range.to_cacheio_range range ~total_size:Int64.max_int in
-
Eio.Switch.run @@ fun _sw ->
-
let source = Eio.Flow.string_source data in
-
Cacheio.put_chunk cache ~key ~range:cacheio_range ~source ()
-
| None ->
-
Log.debug (fun m -> m "Cannot cache chunk for %s: no cacheio backend"
-
(Uri.to_string url))
-
-
let has_complete t ~url ~total_size =
-
if not t.enabled then false
-
else
-
match t.cacheio with
-
| Some cache ->
-
let key = Uri.to_string url in
-
Cacheio.has_complete_chunks cache ~key ~total_size
-
| None -> false
-
-
let missing_ranges t ~url ~total_size =
-
if not t.enabled then
-
[{ Range.start = 0L; end_ = Some (Int64.pred total_size) }]
-
else
-
match t.cacheio with
-
| Some cache ->
-
let key = Uri.to_string url in
-
let cacheio_ranges = Cacheio.missing_ranges cache ~key ~total_size in
-
List.map (fun r ->
-
{ Range.start = Cacheio.Range.start r;
-
end_ = Some (Cacheio.Range.end_ r) }
-
) cacheio_ranges
-
| None ->
-
[{ Range.start = 0L; end_ = Some (Int64.pred total_size) }]
-
-
let coalesce_chunks t ~url =
-
if not t.enabled then false
-
else
-
match t.cacheio with
-
| Some cache ->
-
let key = Uri.to_string url in
-
let promise = Cacheio.coalesce_chunks cache ~key ~verify:true () in
-
(match Eio.Promise.await promise with
-
| Ok () ->
-
Log.info (fun m -> m "Successfully coalesced chunks for %s" key);
-
true
-
| Error exn ->
-
Log.warn (fun m -> m "Failed to coalesce chunks for %s: %s"
-
key (Printexc.to_string exn));
-
false)
-
| None -> false
-
-
let evict t ~url =
-
if not t.enabled then ()
-
else
-
let key = make_cache_key ~method_:`GET ~url ~headers:(Cohttp.Header.init ()) in
-
(match t.cacheio with
-
| Some cache ->
-
Cacheio.delete cache ~key:(key ^ ".meta");
-
Cacheio.delete cache ~key:(key ^ ".body")
-
| None -> ());
-
Log.debug (fun m -> m "Evicting cache for %s" (Uri.to_string url));
-
Hashtbl.remove t.memory_cache key
-
-
let clear t =
-
Log.info (fun m -> m "Clearing entire cache");
-
(match t.cacheio with
-
| Some cache -> Cacheio.clear cache
-
| None -> ());
-
Hashtbl.clear t.memory_cache
-
-
module Stats = struct
-
type cacheio_stats = {
-
total_entries : int;
-
total_bytes : int;
-
expired_entries : int;
-
pinned_entries : int;
-
temporary_entries : int;
-
}
-
-
type t = {
-
memory_cache_entries : int;
-
cache_backend : string;
-
enabled : bool;
-
cache_get_requests : bool;
-
cache_range_requests : bool;
-
cacheio_stats : cacheio_stats option;
-
}
-
-
let make_cacheio_stats total_entries total_bytes expired_entries pinned_entries temporary_entries =
-
{ total_entries; total_bytes; expired_entries; pinned_entries; temporary_entries }
-
-
let make memory_cache_entries cache_backend enabled cache_get_requests cache_range_requests cacheio_stats =
-
{ memory_cache_entries; cache_backend; enabled; cache_get_requests; cache_range_requests; cacheio_stats }
-
-
let cacheio_stats_jsont =
-
Jsont.Object.map ~kind:"CacheioStats" make_cacheio_stats
-
|> Jsont.Object.mem "total_entries" Jsont.int ~enc:(fun t -> t.total_entries)
-
|> Jsont.Object.mem "total_bytes" Jsont.int ~enc:(fun t -> t.total_bytes)
-
|> Jsont.Object.mem "expired_entries" Jsont.int ~enc:(fun t -> t.expired_entries)
-
|> Jsont.Object.mem "pinned_entries" Jsont.int ~enc:(fun t -> t.pinned_entries)
-
|> Jsont.Object.mem "temporary_entries" Jsont.int ~enc:(fun t -> t.temporary_entries)
-
|> Jsont.Object.finish
-
-
let t_jsont =
-
Jsont.Object.map ~kind:"CacheStats" make
-
|> Jsont.Object.mem "memory_cache_entries" Jsont.int ~enc:(fun t -> t.memory_cache_entries)
-
|> Jsont.Object.mem "cache_backend" Jsont.string ~enc:(fun t -> t.cache_backend)
-
|> Jsont.Object.mem "enabled" Jsont.bool ~enc:(fun t -> t.enabled)
-
|> Jsont.Object.mem "cache_get_requests" Jsont.bool ~enc:(fun t -> t.cache_get_requests)
-
|> Jsont.Object.mem "cache_range_requests" Jsont.bool ~enc:(fun t -> t.cache_range_requests)
-
|> Jsont.Object.opt_mem "cacheio_stats" cacheio_stats_jsont ~enc:(fun t -> t.cacheio_stats)
-
|> Jsont.Object.finish
-
-
let to_string t =
-
match Jsont_bytesrw.encode_string' ~format:Jsont.Indent t_jsont t with
-
| Ok s -> s
-
| Error e ->
-
let msg = Jsont.Error.to_string e in
-
failwith (Printf.sprintf "Failed to encode stats: %s" msg)
-
end
-
-
let stats t =
-
let cacheio_stats =
-
match t.cacheio with
-
| Some cache ->
-
let stats = Cacheio.stats cache in
-
Some (Stats.make_cacheio_stats
-
(Cacheio.Stats.entry_count stats)
-
(Int64.to_int (Cacheio.Stats.total_size stats))
-
(Cacheio.Stats.expired_count stats)
-
(Cacheio.Stats.pinned_count stats)
-
(Cacheio.Stats.temporary_count stats))
-
| None -> None
-
in
-
Stats.make
-
(Hashtbl.length t.memory_cache)
-
(if Option.is_some t.cacheio then "cacheio" else "memory")
-
t.enabled
-
t.cache_get_requests
-
t.cache_range_requests
-
cacheio_stats
-1
stack/requests/lib/dune
···
jsont
jsont.bytesrw
base64
-
cacheio
cookeio
xdge
logs
+41 -76
stack/requests/lib/requests.ml
···
module Status = Status
module Error = Error
module Retry = Retry
-
module Cache = Cache
(* Note: RNG initialization should be done by the application using
Mirage_crypto_rng_unix.initialize before calling Eio_main.run.
···
(* Main API - Session functionality with connection pooling *)
-
type ('clock, 'net) t = {
+
(* Internal session type with existential type parameters *)
+
type ('clock, 'net) session = {
sw : Eio.Switch.t;
clock : 'clock;
net : 'net;
···
retry : Retry.config option;
persist_cookies : bool;
xdg : Xdge.t option;
-
cache : Cache.t option;
(* Statistics - mutable for tracking across all derived sessions *)
mutable requests_made : int;
···
mutable retries_count : int;
}
+
(* Public type that hides the existential type parameters.
+
We constrain the existentials to ensure they satisfy the requirements
+
of the internal functions. *)
+
type t = T : ([> float Eio.Time.clock_ty] Eio.Resource.t,
+
[> [> `Generic] Eio.Net.ty] Eio.Resource.t) session -> t
+
let create
~sw
?http_pool
···
?(connection_lifetime = 300.0)
?retry
?(persist_cookies = false)
-
?(enable_cache = false)
?xdg
env =
let clock = env#clock in
let net = env#net in
-
let xdg = match xdg, persist_cookies || enable_cache with
+
let xdg = match xdg, persist_cookies with
| Some x, _ -> Some x
| None, true -> Some (Xdge.create env#fs "requests")
| None, false -> None
···
Cookeio.create ()
in
-
let cache = match enable_cache, xdg with
-
| true, Some xdg_ctx ->
-
let cache_dir = Xdge.cache_dir xdg_ctx in
-
Some (Cache.create ~sw ~enabled:true ~cache_dir:(Some cache_dir) ())
-
| true, None ->
-
(* Memory-only cache when no XDG available *)
-
Some (Cache.create ~sw ~enabled:true ~cache_dir:None ())
-
| false, _ -> None
-
in
-
-
{
+
T {
sw;
clock;
net;
···
retry;
persist_cookies;
xdg;
-
cache;
requests_made = 0;
total_time = 0.0;
retries_count = 0;
}
-
let set_default_header t key value =
-
{ t with default_headers = Headers.set key value t.default_headers }
+
let set_default_header (T t) key value =
+
T { t with default_headers = Headers.set key value t.default_headers }
-
let remove_default_header t key =
-
{ t with default_headers = Headers.remove key t.default_headers }
+
let remove_default_header (T t) key =
+
T { t with default_headers = Headers.remove key t.default_headers }
-
let set_auth t auth =
+
let set_auth (T t) auth =
Log.debug (fun m -> m "Setting authentication method");
-
{ t with auth = Some auth }
+
T { t with auth = Some auth }
-
let clear_auth t =
+
let clear_auth (T t) =
Log.debug (fun m -> m "Clearing authentication");
-
{ t with auth = None }
+
T { t with auth = None }
-
let set_timeout t timeout =
+
let set_timeout (T t) timeout =
Log.debug (fun m -> m "Setting timeout: %a" Timeout.pp timeout);
-
{ t with timeout }
+
T { t with timeout }
-
let set_retry t config =
+
let set_retry (T t) config =
Log.debug (fun m -> m "Setting retry config: max_retries=%d" config.Retry.max_retries);
-
{ t with retry = Some config }
+
T { t with retry = Some config }
-
let cookies t = t.cookie_jar
-
let clear_cookies t = Cookeio.clear t.cookie_jar
+
let cookies (T t) = t.cookie_jar
+
let clear_cookies (T t) = Cookeio.clear t.cookie_jar
(* Internal request function using connection pools *)
let make_request_internal t ?headers ?body ?auth ?timeout ?follow_redirects ?max_redirects ~method_ url =
···
| Some b -> Body.Private.to_string b
in
-
(* Check cache for GET and HEAD requests when body is not present *)
-
let cached_response = match t.cache, method_, body with
-
| Some cache, (`GET | `HEAD), None ->
-
Log.debug (fun m -> m "Checking cache for %s request to %s" method_str url);
-
let headers_cohttp = Cohttp.Header.of_list (Headers.to_list headers) in
-
Cache.get cache ~method_ ~url:uri ~headers:headers_cohttp
-
| _ -> None
-
in
-
-
let response = match cached_response with
-
| Some cached ->
-
Log.info (fun m -> m "Cache HIT for %s request to %s" method_str url);
-
(* Convert cached response to Response.t *)
-
let status = Cohttp.Code.code_of_status cached.Cache.status in
-
let resp_headers = Headers.of_list (Cohttp.Header.to_list cached.Cache.headers) in
-
let body_flow = Eio.Flow.string_source cached.Cache.body in
-
Response.Private.make ~sw:t.sw ~status ~headers:resp_headers ~body:body_flow ~url ~elapsed:0.0
-
| None ->
-
Log.info (fun m -> m "Cache MISS or not applicable for %s request to %s" method_str url);
+
let response =
(* Execute request with redirect handling *)
let rec make_with_redirects url_to_fetch redirects_left =
···
let elapsed = Unix.gettimeofday () -. start_time in
Log.info (fun m -> m "Request completed in %.3f seconds" elapsed);
-
(* Store in cache if successful and caching enabled *)
-
(match t.cache with
-
| Some cache when final_status >= 200 && final_status < 300 ->
-
Log.debug (fun m -> m "Storing response in cache for %s" url);
-
let status = Cohttp.Code.status_of_code final_status in
-
let resp_headers_cohttp = Cohttp.Header.of_list (Headers.to_list final_headers) in
-
let headers_cohttp = Cohttp.Header.of_list (Headers.to_list headers) in
-
Cache.put cache ~method_ ~url:uri ~request_headers:headers_cohttp
-
~status ~headers:resp_headers_cohttp ~body:final_body_str
-
| _ -> ());
-
(* Create a flow from the body string *)
let body_flow = Eio.Flow.string_source final_body_str in
···
response
(* Public request function - executes synchronously *)
-
let request t ?headers ?body ?auth ?timeout ?follow_redirects ?max_redirects ~method_ url =
+
let request (T t) ?headers ?body ?auth ?timeout ?follow_redirects ?max_redirects ~method_ url =
+
(* Keep t in scope to preserve existential types *)
make_request_internal t ?headers ?body ?auth ?timeout
?follow_redirects ?max_redirects ~method_ url
(* Convenience methods *)
-
let get t ?headers ?auth ?timeout ?params url =
+
let get (T t) ?headers ?auth ?timeout ?params url =
let url = match params with
| Some p ->
let uri = Uri.of_string url in
···
Uri.to_string uri
| None -> url
in
-
request t ?headers ?auth ?timeout ~method_:`GET url
+
make_request_internal t ?headers ?auth ?timeout ~method_:`GET url
-
let post t ?headers ?body ?auth ?timeout url =
-
request t ?headers ?body ?auth ?timeout ~method_:`POST url
+
let post (T t) ?headers ?body ?auth ?timeout url =
+
make_request_internal t ?headers ?body ?auth ?timeout ~method_:`POST url
-
let put t ?headers ?body ?auth ?timeout url =
-
request t ?headers ?body ?auth ?timeout ~method_:`PUT url
+
let put (T t) ?headers ?body ?auth ?timeout url =
+
make_request_internal t ?headers ?body ?auth ?timeout ~method_:`PUT url
-
let patch t ?headers ?body ?auth ?timeout url =
-
request t ?headers ?body ?auth ?timeout ~method_:`PATCH url
+
let patch (T t) ?headers ?body ?auth ?timeout url =
+
make_request_internal t ?headers ?body ?auth ?timeout ~method_:`PATCH url
-
let delete t ?headers ?auth ?timeout url =
-
request t ?headers ?auth ?timeout ~method_:`DELETE url
+
let delete (T t) ?headers ?auth ?timeout url =
+
make_request_internal t ?headers ?auth ?timeout ~method_:`DELETE url
-
let head t ?headers ?auth ?timeout url =
-
request t ?headers ?auth ?timeout ~method_:`HEAD url
+
let head (T t) ?headers ?auth ?timeout url =
+
make_request_internal t ?headers ?auth ?timeout ~method_:`HEAD url
-
let options t ?headers ?auth ?timeout url =
-
request t ?headers ?auth ?timeout ~method_:`OPTIONS url
+
let options (T t) ?headers ?auth ?timeout url =
+
make_request_internal t ?headers ?auth ?timeout ~method_:`OPTIONS url
(* Cmdliner integration module *)
module Cmd = struct
+37 -29
stack/requests/lib/requests.mli
···
Use Eio.Fiber.both or Eio.Fiber.all for concurrent execution.
*)
-
type ('clock, 'net) t
+
type t
(** A stateful HTTP client that maintains cookies, auth, configuration, and
-
connection pools across requests. *)
+
connection pools across requests. The internal clock and network types are
+
hidden from external users. *)
(** {2 Creation and Configuration} *)
val create :
sw:Eio.Switch.t ->
-
?http_pool:('clock Eio.Time.clock, 'net Eio.Net.t) Conpool.t ->
-
?https_pool:('clock Eio.Time.clock, 'net Eio.Net.t) Conpool.t ->
+
?http_pool:(([> float Eio.Time.clock_ty] as 'clock) Eio.Resource.t,
+
([> [> `Generic] Eio.Net.ty] as 'net) Eio.Resource.t) Conpool.t ->
+
?https_pool:('clock Eio.Resource.t, 'net Eio.Resource.t) Conpool.t ->
?cookie_jar:Cookeio.jar ->
?default_headers:Headers.t ->
?auth:Auth.t ->
···
?connection_lifetime:float ->
?retry:Retry.config ->
?persist_cookies:bool ->
-
?enable_cache:bool ->
?xdg:Xdge.t ->
< clock: 'clock Eio.Resource.t; net: 'net Eio.Resource.t; fs: Eio.Fs.dir_ty Eio.Path.t; .. > ->
-
('clock Eio.Resource.t, 'net Eio.Resource.t) t
+
t
(** Create a new requests instance with persistent state and connection pooling.
All resources are bound to the provided switch and will be cleaned up automatically.
···
@param connection_lifetime Max lifetime of any pooled connection (default: 300s)
@param retry Retry configuration for failed requests
@param persist_cookies Whether to persist cookies to disk (default: false)
-
@param enable_cache Whether to enable HTTP caching (default: false)
-
@param xdg XDG directory context for cookies/cache (required if persist_cookies or enable_cache)
+
@param xdg XDG directory context for cookies (required if persist_cookies=true)
+
+
{b Note:} HTTP caching has been disabled for simplicity. See CACHEIO.md for integration notes
+
if you need to restore caching functionality in the future.
*)
(** {2 Configuration Management} *)
-
val set_default_header : ('clock, 'net) t -> string -> string -> ('clock, 'net) t
+
val set_default_header : t -> string -> string -> t
(** Add or update a default header. Returns a new session with the updated header.
The original session's connection pools are shared. *)
-
val remove_default_header : ('clock, 'net) t -> string -> ('clock, 'net) t
+
val remove_default_header : t -> string -> t
(** Remove a default header. Returns a new session without the header. *)
-
val set_auth : ('clock, 'net) t -> Auth.t -> ('clock, 'net) t
+
val set_auth : t -> Auth.t -> t
(** Set default authentication. Returns a new session with auth configured. *)
-
val clear_auth : ('clock, 'net) t -> ('clock, 'net) t
+
val clear_auth : t -> t
(** Clear authentication. Returns a new session without auth. *)
-
val set_timeout : ('clock, 'net) t -> Timeout.t -> ('clock, 'net) t
+
val set_timeout : t -> Timeout.t -> t
(** Set default timeout. Returns a new session with the timeout configured. *)
-
val set_retry : ('clock, 'net) t -> Retry.config -> ('clock, 'net) t
+
val set_retry : t -> Retry.config -> t
(** Set retry configuration. Returns a new session with retry configured. *)
(** {2 Request Methods}
···
*)
val request :
-
(_ Eio.Time.clock, _ Eio.Net.t) t ->
+
t ->
?headers:Headers.t ->
?body:Body.t ->
?auth:Auth.t ->
···
(** Make a concurrent HTTP request *)
val get :
-
(_ Eio.Time.clock, _ Eio.Net.t) t ->
+
t ->
?headers:Headers.t ->
?auth:Auth.t ->
?timeout:Timeout.t ->
···
(** Concurrent GET request *)
val post :
-
(_ Eio.Time.clock, _ Eio.Net.t) t ->
+
t ->
?headers:Headers.t ->
?body:Body.t ->
?auth:Auth.t ->
···
(** Concurrent POST request *)
val put :
-
(_ Eio.Time.clock, _ Eio.Net.t) t ->
+
t ->
?headers:Headers.t ->
?body:Body.t ->
?auth:Auth.t ->
···
(** Concurrent PUT request *)
val patch :
-
(_ Eio.Time.clock, _ Eio.Net.t) t ->
+
t ->
?headers:Headers.t ->
?body:Body.t ->
?auth:Auth.t ->
···
(** Concurrent PATCH request *)
val delete :
-
(_ Eio.Time.clock, _ Eio.Net.t) t ->
+
t ->
?headers:Headers.t ->
?auth:Auth.t ->
?timeout:Timeout.t ->
···
(** Concurrent DELETE request *)
val head :
-
(_ Eio.Time.clock, _ Eio.Net.t) t ->
+
t ->
?headers:Headers.t ->
?auth:Auth.t ->
?timeout:Timeout.t ->
···
(** Concurrent HEAD request *)
val options :
-
(_ Eio.Time.clock, _ Eio.Net.t) t ->
+
t ->
?headers:Headers.t ->
?auth:Auth.t ->
?timeout:Timeout.t ->
···
(** {2 Cookie Management} *)
-
val cookies : ('clock, 'net) t -> Cookeio.jar
+
val cookies : t -> Cookeio.jar
(** Get the cookie jar for direct manipulation *)
-
val clear_cookies : ('clock, 'net) t -> unit
+
val clear_cookies : t -> unit
(** Clear all cookies *)
(** {1 Cmdliner Integration} *)
···
user_agent : string option; (** User-Agent header *)
}
-
val create : config -> < clock: ([> float Eio.Time.clock_ty ] as 'clock) Eio.Resource.t; net: ([> [>`Generic] Eio.Net.ty ] as 'net) Eio.Resource.t; fs: Eio.Fs.dir_ty Eio.Path.t; .. > -> Eio.Switch.t -> ('clock Eio.Resource.t, 'net Eio.Resource.t) t
+
val create : config ->
+
< clock: [> float Eio.Time.clock_ty] Eio.Resource.t;
+
net: [> [> `Generic] Eio.Net.ty] Eio.Resource.t;
+
fs: Eio.Fs.dir_ty Eio.Path.t; .. > ->
+
Eio.Switch.t -> t
(** [create config env sw] creates a requests instance from command-line configuration *)
(** {2 Individual Terms} *)
···
Cmd.eval cmd
]} *)
-
val requests_term : string -> < clock: ([> float Eio.Time.clock_ty ] as 'clock) Eio.Resource.t; net: ([> [>`Generic] Eio.Net.ty ] as 'net) Eio.Resource.t; fs: Eio.Fs.dir_ty Eio.Path.t; .. > -> Eio.Switch.t -> ('clock Eio.Resource.t, 'net Eio.Resource.t) t Cmdliner.Term.t
+
val requests_term : string ->
+
< clock: [> float Eio.Time.clock_ty] Eio.Resource.t;
+
net: [> [> `Generic] Eio.Net.ty] Eio.Resource.t;
+
fs: Eio.Fs.dir_ty Eio.Path.t; .. > ->
+
Eio.Switch.t -> t Cmdliner.Term.t
(** [requests_term app_name env sw] creates a term that directly produces a requests instance.
This is a convenience function that combines configuration parsing
···
(** Timeout configuration for requests *)
module Timeout = Timeout
-
-
(** HTTP caching with cache control and range request support *)
-
module Cache = Cache
(** {2 Logging} *)
+12 -12
stack/requests_json_api/lib/requests_json_api.mli
···
(** {1 JSON Request Helpers} *)
-
val get_json_exn : (_ Eio.Time.clock, _ Eio.Net.t) Requests.t -> string -> 'a Jsont.t -> 'a
+
val get_json_exn : Requests.t -> string -> 'a Jsont.t -> 'a
(** [get_json_exn session url decoder] makes a GET request, checks status is 2xx,
reads and parses JSON body using the provided Jsont decoder.
Raises [Failure] on any error (HTTP, network, or JSON parse). *)
-
val get_json : (_ Eio.Time.clock, _ Eio.Net.t) Requests.t -> string -> 'a Jsont.t ->
+
val get_json : Requests.t -> string -> 'a Jsont.t ->
('a, [> `Http of int * string | `Json_error of string]) result
(** Like [get_json_exn] but returns [Result] instead of raising exceptions.
Returns [Ok parsed_value] on success, or [Error] with details on failure. *)
-
val post_json : (_ Eio.Time.clock, _ Eio.Net.t) Requests.t -> string -> 'a Jsont.t -> 'a -> Requests.Response.t
+
val post_json : Requests.t -> string -> 'a Jsont.t -> 'a -> Requests.Response.t
(** [post_json session url codec value] encodes [value] using the Jsont codec and POSTs it to the URL.
Returns the raw response for custom handling. *)
-
val post_json_exn : (_ Eio.Time.clock, _ Eio.Net.t) Requests.t -> string -> 'a Jsont.t -> 'a -> string
+
val post_json_exn : Requests.t -> string -> 'a Jsont.t -> 'a -> string
(** Like [post_json] but checks status is 2xx and returns the response body as a string.
Raises [Failure] on non-2xx status. *)
-
val post_json_result : (_ Eio.Time.clock, _ Eio.Net.t) Requests.t -> string -> 'a Jsont.t -> 'a ->
+
val post_json_result : Requests.t -> string -> 'a Jsont.t -> 'a ->
(string, int * string) result
(** Like [post_json_exn] but returns [Result] instead of raising.
[Ok body] on 2xx status, [Error (status, body)] otherwise. *)
-
val post_json_decode_exn : (_ Eio.Time.clock, _ Eio.Net.t) Requests.t -> string ->
+
val post_json_decode_exn : Requests.t -> string ->
req:'a Jsont.t -> 'a -> resp:'b Jsont.t -> 'b
(** [post_json_decode_exn session url ~req req_value ~resp] encodes [req_value] using the [req] codec,
POSTs it to the URL, checks status is 2xx, and decodes the response using the [resp] codec.
Raises [Failure] on any error. *)
-
val post_json_decode : (_ Eio.Time.clock, _ Eio.Net.t) Requests.t -> string ->
+
val post_json_decode : Requests.t -> string ->
req:'a Jsont.t -> 'a -> resp:'b Jsont.t ->
('b, [> `Http of int * string | `Json_error of string]) result
(** Like [post_json_decode_exn] but returns [Result] instead of raising. *)
-
val put_json_exn : (_ Eio.Time.clock, _ Eio.Net.t) Requests.t -> string -> 'a Jsont.t -> 'a -> string
+
val put_json_exn : Requests.t -> string -> 'a Jsont.t -> 'a -> string
(** [put_json_exn session url codec value] encodes [value] and PUTs it to the URL.
Returns response body. Raises [Failure] on non-2xx status. *)
-
val put_json_decode_exn : (_ Eio.Time.clock, _ Eio.Net.t) Requests.t -> string ->
+
val put_json_decode_exn : Requests.t -> string ->
req:'a Jsont.t -> 'a -> resp:'b Jsont.t -> 'b
(** Like [post_json_decode_exn] but uses PUT method. *)
-
val patch_json_exn : (_ Eio.Time.clock, _ Eio.Net.t) Requests.t -> string -> 'a Jsont.t -> 'a -> string
+
val patch_json_exn : Requests.t -> string -> 'a Jsont.t -> 'a -> string
(** [patch_json_exn session url codec value] encodes [value] and PATCHes it to the URL.
Returns response body. Raises [Failure] on non-2xx status. *)
-
val delete_json_exn : (_ Eio.Time.clock, _ Eio.Net.t) Requests.t -> string -> string
+
val delete_json_exn : Requests.t -> string -> string
(** [delete_json_exn session url] makes a DELETE request.
Returns response body. Raises [Failure] on non-2xx status. *)
···
(** [read_body response] reads the entire response body as a string.
Equivalent to [Requests.Response.body response |> Eio.Flow.read_all] *)
-
val get_result : (_ Eio.Time.clock, _ Eio.Net.t) Requests.t -> string -> (string, int * string) result
+
val get_result : Requests.t -> string -> (string, int * string) result
(** [get_result session url] makes a GET request and returns the result.
Returns [Ok body] on 2xx status, [Error (status, body)] otherwise. *)
+1 -2
stack/river/lib/client.ml
···
module Log = (val Logs.src_log src : Logs.LOG)
type t = {
-
session : (float Eio.Time.clock_ty Eio.Resource.t,
-
[`Generic | `Unix] Eio.Net.ty Eio.Resource.t) Requests.t;
+
session : Requests.t;
}
let create ~sw (env : _ ) =
+1 -2
stack/river/lib/client.mli
···
(** [session t] returns the underlying Requests session.
This is used internally by River's HTTP functions. *)
-
val session : t -> (float Eio.Time.clock_ty Eio.Resource.t,
-
[`Generic | `Unix] Eio.Net.ty Eio.Resource.t) Requests.t
+
val session : t -> Requests.t
+3 -6
stack/typesense-client/typesense_client.ml
···
api_key : string;
}
-
(** Internal polymorphic type for the client *)
-
type 'net t_internal = {
+
(** Client type *)
+
type t = {
config: config;
-
requests_session: (float Eio.Time.clock_ty Eio.Resource.t, 'net Eio.Net.ty Eio.Resource.t) Requests.t;
+
requests_session: Requests.t;
}
-
-
(** Public client type *)
-
type t = [`Generic | `Unix] t_internal
(** Create a new Typesense client *)
let create ~requests_session ~config : t =
+1 -1
stack/typesense-client/typesense_client.mli
···
@param config Configuration with endpoint and API key
@return A client instance *)
val create :
-
requests_session:(float Eio.Time.clock_ty Eio.Resource.t, [`Generic | `Unix] Eio.Net.ty Eio.Resource.t) Requests.t ->
+
requests_session:Requests.t ->
config:config ->
t
+2 -2
stack/zotero-translation/zotero_translation.ml
···
| true -> Uri.of_string (base_uri ^ "import")
| false -> Uri.of_string (base_uri ^ "/import")
-
type ('clock, 'net) t = {
+
type t = {
base_uri: string;
-
requests_session: ('clock, 'net) Requests.t;
+
requests_session: Requests.t;
}
let create ~requests_session base_uri =
+8 -8
stack/zotero-translation/zotero_translation.mli
···
(** {1 Interface to the Zotero Translation Server} *)
-
type ('clock, 'net) t
+
type t
type format =
| Bibtex
···
@param requests_session Shared Requests session for connection pooling.
@param base_uri Base URI of the Zotero translation server (e.g., "http://localhost:1969"). *)
val create :
-
requests_session:('clock Eio.Resource.t, 'net Eio.Resource.t) Requests.t ->
-
string -> ('clock Eio.Resource.t, 'net Eio.Resource.t) t
+
requests_session:Requests.t ->
+
string -> t
-
val resolve_doi: ([> float Eio.Time.clock_ty ] Eio.Resource.t, [> [> `Generic ] Eio.Net.ty ] Eio.Resource.t) t ->
+
val resolve_doi: t ->
string -> (Jsont.json, [>`Msg of string]) result
-
val resolve_url: ([> float Eio.Time.clock_ty ] Eio.Resource.t, [> [> `Generic ] Eio.Net.ty ] Eio.Resource.t) t ->
+
val resolve_url: t ->
string -> (Jsont.json, [>`Msg of string]) result
-
val search_id: ([> float Eio.Time.clock_ty ] Eio.Resource.t, [> [> `Generic ] Eio.Net.ty ] Eio.Resource.t) t ->
+
val search_id: t ->
string -> (Jsont.json, [>`Msg of string]) result
-
val export: ([> float Eio.Time.clock_ty ] Eio.Resource.t, [> [> `Generic ] Eio.Net.ty ] Eio.Resource.t) t ->
+
val export: t ->
format -> Jsont.json -> (string, [>`Msg of string]) result
-
val json_of_doi : ([> float Eio.Time.clock_ty ] Eio.Resource.t, [> [> `Generic ] Eio.Net.ty ] Eio.Resource.t) t ->
+
val json_of_doi : t ->
slug:string -> string -> Jsont.object'
+1 -2
stack/zulip/lib/zulip/lib/client.ml
···
type t = {
auth : Auth.t;
-
session : (float Eio.Time.clock_ty Eio.Resource.t,
-
[`Generic | `Unix] Eio.Net.ty Eio.Resource.t) Requests.t;
+
session : Requests.t;
}
let create ~sw env auth =
+16 -19
stack/zulip/lib/zulip/lib/event_queue.ml
···
module Events_response = struct
type t = { events : Event.t list }
-
(* Custom codec that handles Event.t which has its own of_json *)
let codec =
-
let kind = "EventsResponse" in
-
let of_string s =
-
match Jsont_bytesrw.decode_string' Jsont.json s with
-
| Error e -> Error (Jsont.Error.to_string e)
-
| Ok (Jsont.Object (fields, _)) ->
+
(* Use keep_unknown pattern to handle the whole object and extract events manually *)
+
let make raw_json =
+
match raw_json with
+
| Jsont.Object (fields, _) ->
let assoc = List.map (fun ((k, _), v) -> (k, v)) fields in
(match List.assoc_opt "events" assoc with
-
| Some (Jsont.Array (event_list, _)) ->
-
let events = List.fold_left (fun acc event_json ->
-
match Event.of_json event_json with
+
| Some (Jsont.Array (items, _)) ->
+
(* Parse each event, skipping failures *)
+
let events = List.fold_left (fun acc item ->
+
match Event.of_json item with
| Ok event -> event :: acc
| Error _ -> acc
-
) [] event_list in
-
Ok { events = List.rev events }
-
| None -> Ok { events = [] }
-
| _ -> Error "events field is not an array")
-
| Ok _ -> Error "Expected JSON object"
+
) [] items |> List.rev in
+
{ events }
+
| Some _ -> { events = [] }
+
| None -> { events = [] })
+
| _ -> { events = [] }
in
-
let enc _t =
-
(* Not used for responses, but required by codec *)
-
Fmt.str "{\"events\": []}"
-
in
-
Jsont.of_of_string ~kind of_string ~enc
+
Jsont.Object.map ~kind:"EventsResponse" make
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun _ -> Jsont.Object ([], Jsont.Meta.none))
+
|> Jsont.Object.finish
end
let get_events t client ?last_event_id () =
+18 -46
stack/zulip/lib/zulip_bot/lib/bot_storage.ml
···
(** {1 JSON Codecs for Bot Storage} *)
+
(* String map for storage values *)
+
module String_map = Map.Make (String)
+
(* Storage response type - {"storage": {...}} *)
type storage_response = {
-
storage : (string * string) list;
+
storage : string String_map.t;
unknown : Jsont.json; (** Unknown/extra JSON fields preserved during parsing *)
}
-
(* Custom codec for storage_response that handles the dictionary *)
+
(* Codec for storage response using Jsont.Object with keep_unknown *)
let storage_response_jsont : storage_response Jsont.t =
-
let of_string s =
-
match Jsont_bytesrw.decode_string' Jsont.json s with
-
| Error _ -> Error "Failed to decode JSON"
-
| Ok json ->
-
match json with
-
| Jsont.Object (fields, _) ->
-
let assoc = List.map (fun ((k, _), v) -> (k, v)) fields in
-
(match List.assoc_opt "storage" assoc with
-
| Some (Jsont.Object (storage_fields, _)) ->
-
let storage = List.filter_map (fun ((k, _), v) ->
-
match v with
-
| Jsont.String (s, _) -> Some (k, s)
-
| _ -> None
-
) storage_fields in
-
(* Keep unknown fields *)
-
let unknown_fields = List.filter (fun (k, _) -> k <> "storage") assoc in
-
let unknown_mems = List.map (fun (k, v) -> ((k, Jsont.Meta.none), v)) unknown_fields in
-
let unknown = Jsont.Object (unknown_mems, Jsont.Meta.none) in
-
Ok { storage; unknown }
-
| Some _ -> Error "Expected 'storage' field to be an object"
-
| None -> Ok { storage = []; unknown = Jsont.Object ([], Jsont.Meta.none) })
-
| _ -> Error "Expected JSON object for storage response"
-
in
-
let to_string { storage; unknown } =
-
(* Create storage object *)
-
let storage_fields = List.map (fun (k, v) ->
-
((k, Jsont.Meta.none), Jsont.String (v, Jsont.Meta.none))
-
) storage in
-
let storage_obj = Jsont.Object (storage_fields, Jsont.Meta.none) in
-
-
(* Merge with unknown fields *)
-
let storage_mem = (("storage", Jsont.Meta.none), storage_obj) in
-
let unknown_mems = match unknown with
-
| Jsont.Object (fields, _) -> fields
-
| _ -> []
-
in
-
let json = Jsont.Object (storage_mem :: unknown_mems, Jsont.Meta.none) in
-
match Jsont_bytesrw.encode_string' Jsont.json json with
-
| Ok s -> s
-
| Error e -> failwith ("Failed to encode storage response: " ^ Jsont.Error.to_string e)
+
let make storage unknown = { storage; unknown } in
+
let storage_map_jsont =
+
Jsont.Object.map ~kind:"StorageMap" Fun.id
+
|> Jsont.Object.keep_unknown (Jsont.Object.Mems.string_map Jsont.string) ~enc:Fun.id
+
|> Jsont.Object.finish
in
-
Jsont.of_of_string ~kind:"StorageResponse" of_string ~enc:to_string
+
Jsont.Object.map ~kind:"StorageResponse" make
+
|> Jsont.Object.mem "storage" storage_map_jsont ~enc:(fun r -> r.storage)
+
~dec_absent:String_map.empty
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun r -> r.unknown)
+
|> Jsont.Object.finish
let create client ~bot_email =
Log.info (fun m -> m "Creating bot storage for %s" bot_email);
···
| Ok json ->
(match Zulip.Encode.from_json storage_response_jsont json with
| Ok response ->
-
List.iter (fun (k, v) ->
+
String_map.iter (fun k v ->
Log.debug (fun m -> m "Loaded key from server: %s" k);
Hashtbl.add cache k v
) response.storage
···
| Ok json ->
(match Zulip.Encode.from_json storage_response_jsont json with
| Ok response ->
-
(match List.assoc_opt key response.storage with
+
(match String_map.find_opt key response.storage with
| Some value ->
(* Cache the value *)
Log.debug (fun m -> m "Retrieved key from API: %s" key);
···
| Ok json ->
(match Zulip.Encode.from_json storage_response_jsont json with
| Ok response ->
-
let api_keys = List.map fst response.storage in
+
let api_keys = String_map.fold (fun k _ acc -> k :: acc) response.storage [] in
(* Merge with cache keys *)
let cache_keys = Hashtbl.fold (fun k _ acc -> k :: acc) t.cache [] in
let all_keys = List.sort_uniq String.compare (api_keys @ cache_keys) in
+38 -123
stack/zulip/lib/zulip_bot/lib/message.ml
···
(* Jsont codec for User - handles both user_id and id fields *)
let jsont : t Jsont.t =
-
let of_string s =
-
match Jsont_bytesrw.decode_string' Jsont.json s with
-
| Error _ -> Error "Failed to decode JSON"
-
| Ok json ->
-
match json with
-
| Jsont.Object (fields, _) ->
-
let assoc = List.map (fun ((k, _), v) -> (k, v)) fields in
-
let user_id =
-
match List.assoc_opt "user_id" assoc with
-
| Some (Jsont.Number (f, _)) -> Some (int_of_float f)
-
| _ ->
-
match List.assoc_opt "id" assoc with
-
| Some (Jsont.Number (f, _)) -> Some (int_of_float f)
-
| _ -> None
-
in
-
let email =
-
match List.assoc_opt "email" assoc with
-
| Some (Jsont.String (s, _)) -> Some s
-
| _ -> None
-
in
-
let full_name =
-
match List.assoc_opt "full_name" assoc with
-
| Some (Jsont.String (s, _)) -> Some s
-
| _ -> None
-
in
-
let short_name =
-
match List.assoc_opt "short_name" assoc with
-
| Some (Jsont.String (s, _)) -> Some s
-
| _ -> None
-
in
-
(match (user_id, email, full_name) with
-
| (Some user_id, Some email, Some full_name) ->
-
(* Keep unknown fields *)
-
let unknown_fields = List.filter (fun (k, _) ->
-
k <> "user_id" && k <> "id" && k <> "email" && k <> "full_name" && k <> "short_name"
-
) assoc in
-
let unknown_mems = List.map (fun (k, v) -> ((k, Jsont.Meta.none), v)) unknown_fields in
-
let unknown = Jsont.Object (unknown_mems, Jsont.Meta.none) in
-
Ok { user_id; email; full_name; short_name; unknown }
-
| _ -> Error "Missing required user fields")
-
| _ -> Error "Expected JSON object for user"
+
let make email full_name short_name unknown =
+
(* user_id will be extracted in a custom way from the object *)
+
fun user_id_opt id_opt ->
+
let user_id = match user_id_opt, id_opt with
+
| Some uid, _ -> uid
+
| None, Some id -> id
+
| None, None -> Jsont.Error.msgf Jsont.Meta.none "Missing user_id or id field"
+
in
+
{ user_id; email; full_name; short_name; unknown }
in
-
let to_string { user_id; email; full_name; short_name; unknown } =
-
let fields = [
-
(("user_id", Jsont.Meta.none), Jsont.Number (float_of_int user_id, Jsont.Meta.none));
-
(("email", Jsont.Meta.none), Jsont.String (email, Jsont.Meta.none));
-
(("full_name", Jsont.Meta.none), Jsont.String (full_name, Jsont.Meta.none));
-
] in
-
let fields = match short_name with
-
| Some sn -> (("short_name", Jsont.Meta.none), Jsont.String (sn, Jsont.Meta.none)) :: fields
-
| None -> fields
-
in
-
let unknown_mems = match unknown with
-
| Jsont.Object (mems, _) -> mems
-
| _ -> []
-
in
-
let json = Jsont.Object (fields @ unknown_mems, Jsont.Meta.none) in
-
match Jsont_bytesrw.encode_string' Jsont.json json with
-
| Ok s -> s
-
| Error e -> failwith ("Failed to encode user: " ^ Jsont.Error.to_string e)
-
in
-
Jsont.of_of_string ~kind:"User" of_string ~enc:to_string
+
Jsont.Object.map ~kind:"User" make
+
|> Jsont.Object.mem "email" Jsont.string ~enc:email
+
|> Jsont.Object.mem "full_name" Jsont.string ~enc:full_name
+
|> Jsont.Object.opt_mem "short_name" Jsont.string ~enc:short_name
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun t -> t.unknown)
+
|> Jsont.Object.opt_mem "user_id" Jsont.int ~enc:(fun t -> Some t.user_id)
+
|> Jsont.Object.opt_mem "id" Jsont.int ~enc:(fun _ -> None)
+
|> Jsont.Object.finish
let of_json (json : Zulip.json) : (t, Zulip.zerror) result =
match Zulip.Encode.from_json jsont json with
···
(* Jsont codec for Reaction - handles user_id in different locations *)
let jsont : t Jsont.t =
-
let of_string s =
-
match Jsont_bytesrw.decode_string' Jsont.json s with
-
| Error _ -> Error "Failed to decode JSON"
-
| Ok json ->
-
match json with
-
| Jsont.Object (fields, _) ->
-
let assoc = List.map (fun ((k, _), v) -> (k, v)) fields in
-
let emoji_name =
-
match List.assoc_opt "emoji_name" assoc with
-
| Some (Jsont.String (s, _)) -> Some s
-
| _ -> None
-
in
-
let emoji_code =
-
match List.assoc_opt "emoji_code" assoc with
-
| Some (Jsont.String (s, _)) -> Some s
-
| _ -> None
-
in
-
let reaction_type =
-
match List.assoc_opt "reaction_type" assoc with
-
| Some (Jsont.String (s, _)) -> Some s
-
| _ -> None
-
in
-
(* user_id can be either directly in the object or nested in a "user" field *)
-
let user_id =
-
match List.assoc_opt "user_id" assoc with
-
| Some (Jsont.Number (f, _)) -> Some (int_of_float f)
-
| _ ->
-
match List.assoc_opt "user" assoc with
-
| Some (Jsont.Object (user_fields, _)) ->
-
let user_assoc = List.map (fun ((k, _), v) -> (k, v)) user_fields in
-
(match List.assoc_opt "user_id" user_assoc with
-
| Some (Jsont.Number (f, _)) -> Some (int_of_float f)
-
| _ -> None)
-
| _ -> None
-
in
-
(match (emoji_name, emoji_code, reaction_type, user_id) with
-
| (Some emoji_name, Some emoji_code, Some reaction_type, Some user_id) ->
-
(* Keep unknown fields *)
-
let unknown_fields = List.filter (fun (k, _) ->
-
k <> "emoji_name" && k <> "emoji_code" && k <> "reaction_type" && k <> "user_id" && k <> "user"
-
) assoc in
-
let unknown_mems = List.map (fun (k, v) -> ((k, Jsont.Meta.none), v)) unknown_fields in
-
let unknown = Jsont.Object (unknown_mems, Jsont.Meta.none) in
-
Ok { emoji_name; emoji_code; reaction_type; user_id; unknown }
-
| _ -> Error "Missing required reaction fields")
-
| _ -> Error "Expected JSON object for reaction"
+
(* Helper codec for nested user object - extracts just the user_id *)
+
let user_obj_codec =
+
Jsont.Object.map ~kind:"ReactionUser" Fun.id
+
|> Jsont.Object.mem "user_id" Jsont.int ~enc:Fun.id
+
|> Jsont.Object.finish
in
-
let to_string { emoji_name; emoji_code; reaction_type; user_id; unknown } =
-
let fields = [
-
(("emoji_name", Jsont.Meta.none), Jsont.String (emoji_name, Jsont.Meta.none));
-
(("emoji_code", Jsont.Meta.none), Jsont.String (emoji_code, Jsont.Meta.none));
-
(("reaction_type", Jsont.Meta.none), Jsont.String (reaction_type, Jsont.Meta.none));
-
(("user_id", Jsont.Meta.none), Jsont.Number (float_of_int user_id, Jsont.Meta.none));
-
] in
-
let unknown_mems = match unknown with
-
| Jsont.Object (mems, _) -> mems
-
| _ -> []
-
in
-
let json = Jsont.Object (fields @ unknown_mems, Jsont.Meta.none) in
-
match Jsont_bytesrw.encode_string' Jsont.json json with
-
| Ok s -> s
-
| Error e -> failwith ("Failed to encode reaction: " ^ Jsont.Error.to_string e)
+
let make emoji_name emoji_code reaction_type unknown =
+
fun user_id_direct user_obj_nested ->
+
let user_id = match user_id_direct, user_obj_nested with
+
| Some uid, _ -> uid
+
| None, Some uid -> uid
+
| None, None -> Jsont.Error.msgf Jsont.Meta.none "Missing user_id field"
+
in
+
{ emoji_name; emoji_code; reaction_type; user_id; unknown }
in
-
Jsont.of_of_string ~kind:"Reaction" of_string ~enc:to_string
+
Jsont.Object.map ~kind:"Reaction" make
+
|> Jsont.Object.mem "emoji_name" Jsont.string ~enc:emoji_name
+
|> Jsont.Object.mem "emoji_code" Jsont.string ~enc:emoji_code
+
|> Jsont.Object.mem "reaction_type" Jsont.string ~enc:reaction_type
+
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun t -> t.unknown)
+
|> Jsont.Object.opt_mem "user_id" Jsont.int ~enc:(fun t -> Some t.user_id)
+
|> Jsont.Object.opt_mem "user" user_obj_codec ~enc:(fun _ -> None)
+
|> Jsont.Object.finish
let of_json (json : Zulip.json) : (t, Zulip.zerror) result =
match Zulip.Encode.from_json jsont json with