Class: Pikuri::Agent::ChatTransport
- Inherits:
-
Object
- Object
- Pikuri::Agent::ChatTransport
- Defined in:
- lib/pikuri/agent/chat_transport.rb
Overview
Everything that has to travel together for a chat to resolve to the same model *on the same server* on every construction: the model id, the provider hint, the registry-bypass flag, and — when the model lives on a server other than the process-global RubyLLM.config default — that server’s base URL and API key.
Bundling them is structural protection against a recurring bug class — every forwarding site (the synthesizer rescue in #run_loop, the agent tool from pikuri-subagents spawning a sub-agent, a mid-conversation model switch) used to pass the resolution fields individually, and dropping one routed the chat to a different server or raised RubyLLM::ModelNotFoundError on the unknown model id. With a single value object the call site can’t silently miss a field.
Why api_base / api_key live here
RubyLLM::Chat#with_model swaps only the model/provider against the chat’s existing connection config, so switching to a model on a different server (a small local llama.cpp vs a big cloud model) needs the connection to travel with the model — otherwise the new model id is sent to the old server’s URL with the old key. Pikuri::Agent maps these two generic fields onto the provider’s ruby_llm config slots (+##provider_api_base+ / #{provider}_api_key) via a per-chat RubyLLM::Context; both are nil for a transport that rides the process-global config.
Pure data carrier: no RubyLLM references here, so the seam stays in Pikuri::Agent, bin/pikuri-chat, and Tool.
Instance Attribute Summary collapse
-
#api_base ⇒ String?
readonly
Connection base URL for this model’s server (e.g.
http://localhost:8080/v1). -
#api_key ⇒ String?
readonly
API key for this model’s server.
-
#assume_model_exists ⇒ Boolean
readonly
Forwarded to
RubyLLM.chat;trueskips ruby_llm’s registry lookup and trusts the supplied model id. -
#context_window ⇒ Integer?
readonly
Explicit context-window cap for this model on this server, or
nilto defer to ContextWindowDetector‘s probe. -
#model ⇒ String?
readonly
LLM identifier;
nildefers toRubyLLM.config.default_modelat Pikuri::Agent construction time. -
#provider ⇒ Symbol?
readonly
Forwarded to
RubyLLM.chat.
Class Method Summary collapse
-
.from_openai_server(server:, model:, api_key: 'not-needed', context_window: nil) ⇒ ChatTransport
Build an
:openai-provider transport for an OpenAI-compatible server (a local llama.cpp, a cloud endpoint, …), carrying that server’s connection so the agent rides a per-chatRubyLLM::Contextinstead of the process-globalRubyLLM.config.
Instance Method Summary collapse
-
#chat_kwargs ⇒ Hash{Symbol => String, Symbol, Boolean, nil}
The model-resolution kwargs to spread into
RubyLLM.chat/ RubyLLM::Context#chat. -
#connection_overrides? ⇒ Boolean
Whether this transport overrides the process-global connection (and so needs a dedicated
RubyLLM::Context). -
#initialize(model:, provider: nil, assume_model_exists: false, api_base: nil, api_key: nil, context_window: nil) ⇒ ChatTransport
constructor
A new instance of ChatTransport.
-
#inspect ⇒ String
(also: #to_s)
Default Data#inspect would print
api_keyverbatim, leaking the secret into any log line,to_sinterpolation, or backtrace that touches the transport.
Constructor Details
#initialize(model:, provider: nil, assume_model_exists: false, api_base: nil, api_key: nil, context_window: nil) ⇒ ChatTransport
Returns a new instance of ChatTransport.
126 127 128 129 130 131 132 133 |
# File 'lib/pikuri/agent/chat_transport.rb', line 126 def initialize(model:, provider: nil, assume_model_exists: false, api_base: nil, api_key: nil, context_window: nil) if (api_base || api_key) && provider.nil? raise ArgumentError, "api_base/api_key require a provider, got #{provider.inspect}" end super end |
Instance Attribute Details
#api_base ⇒ String? (readonly)
Returns connection base URL for this model’s server (e.g. http://localhost:8080/v1). nil rides the process-global RubyLLM.config base. Mapped to the provider’s #{provider}_api_base slot by Pikuri::Agent.
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/pikuri/agent/chat_transport.rb', line 73 class ChatTransport < Data.define(:model, :provider, :assume_model_exists, :api_base, :api_key, :context_window) # Build an +:openai+-provider transport for an OpenAI-compatible # server (a local llama.cpp, a cloud endpoint, ...), carrying that # server's connection so the agent rides a per-chat # +RubyLLM::Context+ instead of the process-global +RubyLLM.config+. # This is the host-boot factory the +bin/pikuri-*+ demos use in # place of +RubyLLM.configure+ — one isolated connection per agent, # so several agents pointed at different servers (and different # keys) don't stomp a shared global. # # +server+ is the bare server origin; a trailing +/v1+ (the # OpenAI-compatible suffix ruby_llm appends to reach # +/v1/chat/completions+) is stripped and re-appended exactly once, # so +https://api.x.ai+, +https://api.x.ai/v1+, and # +https://api.x.ai/v1/+ all normalize to the same +.../v1+ base. # Without this, a +server+ value that already ended in +/v1+ would # double to +/v1/v1+ and every request would 404. # # @param server [String] server origin, with or without a trailing # +/v1+, e.g. +"http://localhost:8080"+ or +"https://api.x.ai/v1"+ # @param model [String] model id served there, trusted verbatim # (+assume_model_exists+ is +true+, so it need not appear in # ruby_llm's bundled registry) # @param api_key [String] API key for the server; the conventional # +"not-needed"+ placeholder for a keyless local server # @param context_window [Integer, nil] explicit context-window cap # for this model, or +nil+ to defer to {ContextWindowDetector}'s # +/props+ probe (the right default for a local llama.cpp, which # reports its launched +n_ctx+; the right *override* for a cloud # server the probe can't reach, e.g. a 2M-window model on x.ai) # @return [ChatTransport] a transport whose +api_base+ is the # normalized +.../v1+ URL and whose +api_key+ is +api_key+ def self.from_openai_server(server:, model:, api_key: 'not-needed', context_window: nil) base = server.to_s.strip.chomp('/').delete_suffix('/v1') new( model: model, provider: :openai, assume_model_exists: true, api_base: "#{base}/v1", api_key: api_key, context_window: context_window ) end # @param model [String, nil] # @param provider [Symbol, nil] # @param assume_model_exists [Boolean] # @param api_base [String, nil] # @param api_key [String, nil] # @param context_window [Integer, nil] # @raise [ArgumentError] if +api_base+ or +api_key+ is set without # a +provider+ (the provider names the config slots the # connection overrides map onto) def initialize(model:, provider: nil, assume_model_exists: false, api_base: nil, api_key: nil, context_window: nil) if (api_base || api_key) && provider.nil? raise ArgumentError, "api_base/api_key require a provider, got #{provider.inspect}" end super end # The model-resolution kwargs to spread into +RubyLLM.chat+ / # +RubyLLM::Context#chat+. Excludes the connection fields — those # configure the +Context+ the chat is built from, not the +chat+ # call itself. # # @return [Hash{Symbol => String, Symbol, Boolean, nil}] def chat_kwargs { model: model, provider: provider, assume_model_exists: assume_model_exists } end # Whether this transport overrides the process-global connection # (and so needs a dedicated +RubyLLM::Context+). # # @return [Boolean] def connection_overrides? !api_base.nil? || !api_key.nil? end # Default +Data#inspect+ would print +api_key+ verbatim, leaking # the secret into any log line, +to_s+ interpolation, or backtrace # that touches the transport. Redact it. # # @return [String] def inspect "#<#{self.class} model=#{model.inspect} provider=#{provider.inspect} " \ "assume_model_exists=#{assume_model_exists} api_base=#{api_base.inspect} " \ "api_key=#{api_key.nil? ? 'nil' : '[REDACTED]'} context_window=#{context_window.inspect}>" end alias to_s inspect end |
#api_key ⇒ String? (readonly)
Returns API key for this model’s server. nil rides the process-global config key. Mapped to the provider’s #{provider}_api_key slot by Pikuri::Agent. Redacted in #inspect so it never leaks into a log line or backtrace.
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/pikuri/agent/chat_transport.rb', line 73 class ChatTransport < Data.define(:model, :provider, :assume_model_exists, :api_base, :api_key, :context_window) # Build an +:openai+-provider transport for an OpenAI-compatible # server (a local llama.cpp, a cloud endpoint, ...), carrying that # server's connection so the agent rides a per-chat # +RubyLLM::Context+ instead of the process-global +RubyLLM.config+. # This is the host-boot factory the +bin/pikuri-*+ demos use in # place of +RubyLLM.configure+ — one isolated connection per agent, # so several agents pointed at different servers (and different # keys) don't stomp a shared global. # # +server+ is the bare server origin; a trailing +/v1+ (the # OpenAI-compatible suffix ruby_llm appends to reach # +/v1/chat/completions+) is stripped and re-appended exactly once, # so +https://api.x.ai+, +https://api.x.ai/v1+, and # +https://api.x.ai/v1/+ all normalize to the same +.../v1+ base. # Without this, a +server+ value that already ended in +/v1+ would # double to +/v1/v1+ and every request would 404. # # @param server [String] server origin, with or without a trailing # +/v1+, e.g. +"http://localhost:8080"+ or +"https://api.x.ai/v1"+ # @param model [String] model id served there, trusted verbatim # (+assume_model_exists+ is +true+, so it need not appear in # ruby_llm's bundled registry) # @param api_key [String] API key for the server; the conventional # +"not-needed"+ placeholder for a keyless local server # @param context_window [Integer, nil] explicit context-window cap # for this model, or +nil+ to defer to {ContextWindowDetector}'s # +/props+ probe (the right default for a local llama.cpp, which # reports its launched +n_ctx+; the right *override* for a cloud # server the probe can't reach, e.g. a 2M-window model on x.ai) # @return [ChatTransport] a transport whose +api_base+ is the # normalized +.../v1+ URL and whose +api_key+ is +api_key+ def self.from_openai_server(server:, model:, api_key: 'not-needed', context_window: nil) base = server.to_s.strip.chomp('/').delete_suffix('/v1') new( model: model, provider: :openai, assume_model_exists: true, api_base: "#{base}/v1", api_key: api_key, context_window: context_window ) end # @param model [String, nil] # @param provider [Symbol, nil] # @param assume_model_exists [Boolean] # @param api_base [String, nil] # @param api_key [String, nil] # @param context_window [Integer, nil] # @raise [ArgumentError] if +api_base+ or +api_key+ is set without # a +provider+ (the provider names the config slots the # connection overrides map onto) def initialize(model:, provider: nil, assume_model_exists: false, api_base: nil, api_key: nil, context_window: nil) if (api_base || api_key) && provider.nil? raise ArgumentError, "api_base/api_key require a provider, got #{provider.inspect}" end super end # The model-resolution kwargs to spread into +RubyLLM.chat+ / # +RubyLLM::Context#chat+. Excludes the connection fields — those # configure the +Context+ the chat is built from, not the +chat+ # call itself. # # @return [Hash{Symbol => String, Symbol, Boolean, nil}] def chat_kwargs { model: model, provider: provider, assume_model_exists: assume_model_exists } end # Whether this transport overrides the process-global connection # (and so needs a dedicated +RubyLLM::Context+). # # @return [Boolean] def connection_overrides? !api_base.nil? || !api_key.nil? end # Default +Data#inspect+ would print +api_key+ verbatim, leaking # the secret into any log line, +to_s+ interpolation, or backtrace # that touches the transport. Redact it. # # @return [String] def inspect "#<#{self.class} model=#{model.inspect} provider=#{provider.inspect} " \ "assume_model_exists=#{assume_model_exists} api_base=#{api_base.inspect} " \ "api_key=#{api_key.nil? ? 'nil' : '[REDACTED]'} context_window=#{context_window.inspect}>" end alias to_s inspect end |
#assume_model_exists ⇒ Boolean (readonly)
Returns forwarded to RubyLLM.chat; true skips ruby_llm’s registry lookup and trusts the supplied model id. Requires provider.
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/pikuri/agent/chat_transport.rb', line 73 class ChatTransport < Data.define(:model, :provider, :assume_model_exists, :api_base, :api_key, :context_window) # Build an +:openai+-provider transport for an OpenAI-compatible # server (a local llama.cpp, a cloud endpoint, ...), carrying that # server's connection so the agent rides a per-chat # +RubyLLM::Context+ instead of the process-global +RubyLLM.config+. # This is the host-boot factory the +bin/pikuri-*+ demos use in # place of +RubyLLM.configure+ — one isolated connection per agent, # so several agents pointed at different servers (and different # keys) don't stomp a shared global. # # +server+ is the bare server origin; a trailing +/v1+ (the # OpenAI-compatible suffix ruby_llm appends to reach # +/v1/chat/completions+) is stripped and re-appended exactly once, # so +https://api.x.ai+, +https://api.x.ai/v1+, and # +https://api.x.ai/v1/+ all normalize to the same +.../v1+ base. # Without this, a +server+ value that already ended in +/v1+ would # double to +/v1/v1+ and every request would 404. # # @param server [String] server origin, with or without a trailing # +/v1+, e.g. +"http://localhost:8080"+ or +"https://api.x.ai/v1"+ # @param model [String] model id served there, trusted verbatim # (+assume_model_exists+ is +true+, so it need not appear in # ruby_llm's bundled registry) # @param api_key [String] API key for the server; the conventional # +"not-needed"+ placeholder for a keyless local server # @param context_window [Integer, nil] explicit context-window cap # for this model, or +nil+ to defer to {ContextWindowDetector}'s # +/props+ probe (the right default for a local llama.cpp, which # reports its launched +n_ctx+; the right *override* for a cloud # server the probe can't reach, e.g. a 2M-window model on x.ai) # @return [ChatTransport] a transport whose +api_base+ is the # normalized +.../v1+ URL and whose +api_key+ is +api_key+ def self.from_openai_server(server:, model:, api_key: 'not-needed', context_window: nil) base = server.to_s.strip.chomp('/').delete_suffix('/v1') new( model: model, provider: :openai, assume_model_exists: true, api_base: "#{base}/v1", api_key: api_key, context_window: context_window ) end # @param model [String, nil] # @param provider [Symbol, nil] # @param assume_model_exists [Boolean] # @param api_base [String, nil] # @param api_key [String, nil] # @param context_window [Integer, nil] # @raise [ArgumentError] if +api_base+ or +api_key+ is set without # a +provider+ (the provider names the config slots the # connection overrides map onto) def initialize(model:, provider: nil, assume_model_exists: false, api_base: nil, api_key: nil, context_window: nil) if (api_base || api_key) && provider.nil? raise ArgumentError, "api_base/api_key require a provider, got #{provider.inspect}" end super end # The model-resolution kwargs to spread into +RubyLLM.chat+ / # +RubyLLM::Context#chat+. Excludes the connection fields — those # configure the +Context+ the chat is built from, not the +chat+ # call itself. # # @return [Hash{Symbol => String, Symbol, Boolean, nil}] def chat_kwargs { model: model, provider: provider, assume_model_exists: assume_model_exists } end # Whether this transport overrides the process-global connection # (and so needs a dedicated +RubyLLM::Context+). # # @return [Boolean] def connection_overrides? !api_base.nil? || !api_key.nil? end # Default +Data#inspect+ would print +api_key+ verbatim, leaking # the secret into any log line, +to_s+ interpolation, or backtrace # that touches the transport. Redact it. # # @return [String] def inspect "#<#{self.class} model=#{model.inspect} provider=#{provider.inspect} " \ "assume_model_exists=#{assume_model_exists} api_base=#{api_base.inspect} " \ "api_key=#{api_key.nil? ? 'nil' : '[REDACTED]'} context_window=#{context_window.inspect}>" end alias to_s inspect end |
#context_window ⇒ Integer? (readonly)
Returns explicit context-window cap for this model on this server, or nil to defer to Pikuri::Agent::ContextWindowDetector‘s probe. Travels with the model because the cap is a per-model-per-server property: a Ctrl+P-style switch to a different transport must carry its own cap, not inherit the previous model’s. Never sent to ruby_llm (it is neither a #chat_kwargs entry nor a connection slot) — pure pikuri metadata read by Pikuri::Agent#detect_and_emit_context_cap!. The cap-inheritance channel too: the agent tool from pikuri-subagents and the synthesizer hand a spawned agent parent.transport.with( context_window: parent.context_window_cap) so the parent’s resolved cap (explicit or probed) rides along without a re-probe.
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/pikuri/agent/chat_transport.rb', line 73 class ChatTransport < Data.define(:model, :provider, :assume_model_exists, :api_base, :api_key, :context_window) # Build an +:openai+-provider transport for an OpenAI-compatible # server (a local llama.cpp, a cloud endpoint, ...), carrying that # server's connection so the agent rides a per-chat # +RubyLLM::Context+ instead of the process-global +RubyLLM.config+. # This is the host-boot factory the +bin/pikuri-*+ demos use in # place of +RubyLLM.configure+ — one isolated connection per agent, # so several agents pointed at different servers (and different # keys) don't stomp a shared global. # # +server+ is the bare server origin; a trailing +/v1+ (the # OpenAI-compatible suffix ruby_llm appends to reach # +/v1/chat/completions+) is stripped and re-appended exactly once, # so +https://api.x.ai+, +https://api.x.ai/v1+, and # +https://api.x.ai/v1/+ all normalize to the same +.../v1+ base. # Without this, a +server+ value that already ended in +/v1+ would # double to +/v1/v1+ and every request would 404. # # @param server [String] server origin, with or without a trailing # +/v1+, e.g. +"http://localhost:8080"+ or +"https://api.x.ai/v1"+ # @param model [String] model id served there, trusted verbatim # (+assume_model_exists+ is +true+, so it need not appear in # ruby_llm's bundled registry) # @param api_key [String] API key for the server; the conventional # +"not-needed"+ placeholder for a keyless local server # @param context_window [Integer, nil] explicit context-window cap # for this model, or +nil+ to defer to {ContextWindowDetector}'s # +/props+ probe (the right default for a local llama.cpp, which # reports its launched +n_ctx+; the right *override* for a cloud # server the probe can't reach, e.g. a 2M-window model on x.ai) # @return [ChatTransport] a transport whose +api_base+ is the # normalized +.../v1+ URL and whose +api_key+ is +api_key+ def self.from_openai_server(server:, model:, api_key: 'not-needed', context_window: nil) base = server.to_s.strip.chomp('/').delete_suffix('/v1') new( model: model, provider: :openai, assume_model_exists: true, api_base: "#{base}/v1", api_key: api_key, context_window: context_window ) end # @param model [String, nil] # @param provider [Symbol, nil] # @param assume_model_exists [Boolean] # @param api_base [String, nil] # @param api_key [String, nil] # @param context_window [Integer, nil] # @raise [ArgumentError] if +api_base+ or +api_key+ is set without # a +provider+ (the provider names the config slots the # connection overrides map onto) def initialize(model:, provider: nil, assume_model_exists: false, api_base: nil, api_key: nil, context_window: nil) if (api_base || api_key) && provider.nil? raise ArgumentError, "api_base/api_key require a provider, got #{provider.inspect}" end super end # The model-resolution kwargs to spread into +RubyLLM.chat+ / # +RubyLLM::Context#chat+. Excludes the connection fields — those # configure the +Context+ the chat is built from, not the +chat+ # call itself. # # @return [Hash{Symbol => String, Symbol, Boolean, nil}] def chat_kwargs { model: model, provider: provider, assume_model_exists: assume_model_exists } end # Whether this transport overrides the process-global connection # (and so needs a dedicated +RubyLLM::Context+). # # @return [Boolean] def connection_overrides? !api_base.nil? || !api_key.nil? end # Default +Data#inspect+ would print +api_key+ verbatim, leaking # the secret into any log line, +to_s+ interpolation, or backtrace # that touches the transport. Redact it. # # @return [String] def inspect "#<#{self.class} model=#{model.inspect} provider=#{provider.inspect} " \ "assume_model_exists=#{assume_model_exists} api_base=#{api_base.inspect} " \ "api_key=#{api_key.nil? ? 'nil' : '[REDACTED]'} context_window=#{context_window.inspect}>" end alias to_s inspect end |
#model ⇒ String? (readonly)
Returns LLM identifier; nil defers to RubyLLM.config.default_model at Pikuri::Agent construction time.
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/pikuri/agent/chat_transport.rb', line 73 class ChatTransport < Data.define(:model, :provider, :assume_model_exists, :api_base, :api_key, :context_window) # Build an +:openai+-provider transport for an OpenAI-compatible # server (a local llama.cpp, a cloud endpoint, ...), carrying that # server's connection so the agent rides a per-chat # +RubyLLM::Context+ instead of the process-global +RubyLLM.config+. # This is the host-boot factory the +bin/pikuri-*+ demos use in # place of +RubyLLM.configure+ — one isolated connection per agent, # so several agents pointed at different servers (and different # keys) don't stomp a shared global. # # +server+ is the bare server origin; a trailing +/v1+ (the # OpenAI-compatible suffix ruby_llm appends to reach # +/v1/chat/completions+) is stripped and re-appended exactly once, # so +https://api.x.ai+, +https://api.x.ai/v1+, and # +https://api.x.ai/v1/+ all normalize to the same +.../v1+ base. # Without this, a +server+ value that already ended in +/v1+ would # double to +/v1/v1+ and every request would 404. # # @param server [String] server origin, with or without a trailing # +/v1+, e.g. +"http://localhost:8080"+ or +"https://api.x.ai/v1"+ # @param model [String] model id served there, trusted verbatim # (+assume_model_exists+ is +true+, so it need not appear in # ruby_llm's bundled registry) # @param api_key [String] API key for the server; the conventional # +"not-needed"+ placeholder for a keyless local server # @param context_window [Integer, nil] explicit context-window cap # for this model, or +nil+ to defer to {ContextWindowDetector}'s # +/props+ probe (the right default for a local llama.cpp, which # reports its launched +n_ctx+; the right *override* for a cloud # server the probe can't reach, e.g. a 2M-window model on x.ai) # @return [ChatTransport] a transport whose +api_base+ is the # normalized +.../v1+ URL and whose +api_key+ is +api_key+ def self.from_openai_server(server:, model:, api_key: 'not-needed', context_window: nil) base = server.to_s.strip.chomp('/').delete_suffix('/v1') new( model: model, provider: :openai, assume_model_exists: true, api_base: "#{base}/v1", api_key: api_key, context_window: context_window ) end # @param model [String, nil] # @param provider [Symbol, nil] # @param assume_model_exists [Boolean] # @param api_base [String, nil] # @param api_key [String, nil] # @param context_window [Integer, nil] # @raise [ArgumentError] if +api_base+ or +api_key+ is set without # a +provider+ (the provider names the config slots the # connection overrides map onto) def initialize(model:, provider: nil, assume_model_exists: false, api_base: nil, api_key: nil, context_window: nil) if (api_base || api_key) && provider.nil? raise ArgumentError, "api_base/api_key require a provider, got #{provider.inspect}" end super end # The model-resolution kwargs to spread into +RubyLLM.chat+ / # +RubyLLM::Context#chat+. Excludes the connection fields — those # configure the +Context+ the chat is built from, not the +chat+ # call itself. # # @return [Hash{Symbol => String, Symbol, Boolean, nil}] def chat_kwargs { model: model, provider: provider, assume_model_exists: assume_model_exists } end # Whether this transport overrides the process-global connection # (and so needs a dedicated +RubyLLM::Context+). # # @return [Boolean] def connection_overrides? !api_base.nil? || !api_key.nil? end # Default +Data#inspect+ would print +api_key+ verbatim, leaking # the secret into any log line, +to_s+ interpolation, or backtrace # that touches the transport. Redact it. # # @return [String] def inspect "#<#{self.class} model=#{model.inspect} provider=#{provider.inspect} " \ "assume_model_exists=#{assume_model_exists} api_base=#{api_base.inspect} " \ "api_key=#{api_key.nil? ? 'nil' : '[REDACTED]'} context_window=#{context_window.inspect}>" end alias to_s inspect end |
#provider ⇒ Symbol? (readonly)
Returns forwarded to RubyLLM.chat. Required together with assume_model_exists when pointing at a local OpenAI-compatible server (llama.cpp, gpustack, …) whose model ids are not in ruby_llm’s bundled registry; required whenever api_base / api_key is set (it names the config slots).
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/pikuri/agent/chat_transport.rb', line 73 class ChatTransport < Data.define(:model, :provider, :assume_model_exists, :api_base, :api_key, :context_window) # Build an +:openai+-provider transport for an OpenAI-compatible # server (a local llama.cpp, a cloud endpoint, ...), carrying that # server's connection so the agent rides a per-chat # +RubyLLM::Context+ instead of the process-global +RubyLLM.config+. # This is the host-boot factory the +bin/pikuri-*+ demos use in # place of +RubyLLM.configure+ — one isolated connection per agent, # so several agents pointed at different servers (and different # keys) don't stomp a shared global. # # +server+ is the bare server origin; a trailing +/v1+ (the # OpenAI-compatible suffix ruby_llm appends to reach # +/v1/chat/completions+) is stripped and re-appended exactly once, # so +https://api.x.ai+, +https://api.x.ai/v1+, and # +https://api.x.ai/v1/+ all normalize to the same +.../v1+ base. # Without this, a +server+ value that already ended in +/v1+ would # double to +/v1/v1+ and every request would 404. # # @param server [String] server origin, with or without a trailing # +/v1+, e.g. +"http://localhost:8080"+ or +"https://api.x.ai/v1"+ # @param model [String] model id served there, trusted verbatim # (+assume_model_exists+ is +true+, so it need not appear in # ruby_llm's bundled registry) # @param api_key [String] API key for the server; the conventional # +"not-needed"+ placeholder for a keyless local server # @param context_window [Integer, nil] explicit context-window cap # for this model, or +nil+ to defer to {ContextWindowDetector}'s # +/props+ probe (the right default for a local llama.cpp, which # reports its launched +n_ctx+; the right *override* for a cloud # server the probe can't reach, e.g. a 2M-window model on x.ai) # @return [ChatTransport] a transport whose +api_base+ is the # normalized +.../v1+ URL and whose +api_key+ is +api_key+ def self.from_openai_server(server:, model:, api_key: 'not-needed', context_window: nil) base = server.to_s.strip.chomp('/').delete_suffix('/v1') new( model: model, provider: :openai, assume_model_exists: true, api_base: "#{base}/v1", api_key: api_key, context_window: context_window ) end # @param model [String, nil] # @param provider [Symbol, nil] # @param assume_model_exists [Boolean] # @param api_base [String, nil] # @param api_key [String, nil] # @param context_window [Integer, nil] # @raise [ArgumentError] if +api_base+ or +api_key+ is set without # a +provider+ (the provider names the config slots the # connection overrides map onto) def initialize(model:, provider: nil, assume_model_exists: false, api_base: nil, api_key: nil, context_window: nil) if (api_base || api_key) && provider.nil? raise ArgumentError, "api_base/api_key require a provider, got #{provider.inspect}" end super end # The model-resolution kwargs to spread into +RubyLLM.chat+ / # +RubyLLM::Context#chat+. Excludes the connection fields — those # configure the +Context+ the chat is built from, not the +chat+ # call itself. # # @return [Hash{Symbol => String, Symbol, Boolean, nil}] def chat_kwargs { model: model, provider: provider, assume_model_exists: assume_model_exists } end # Whether this transport overrides the process-global connection # (and so needs a dedicated +RubyLLM::Context+). # # @return [Boolean] def connection_overrides? !api_base.nil? || !api_key.nil? end # Default +Data#inspect+ would print +api_key+ verbatim, leaking # the secret into any log line, +to_s+ interpolation, or backtrace # that touches the transport. Redact it. # # @return [String] def inspect "#<#{self.class} model=#{model.inspect} provider=#{provider.inspect} " \ "assume_model_exists=#{assume_model_exists} api_base=#{api_base.inspect} " \ "api_key=#{api_key.nil? ? 'nil' : '[REDACTED]'} context_window=#{context_window.inspect}>" end alias to_s inspect end |
Class Method Details
.from_openai_server(server:, model:, api_key: 'not-needed', context_window: nil) ⇒ ChatTransport
Build an :openai-provider transport for an OpenAI-compatible server (a local llama.cpp, a cloud endpoint, …), carrying that server’s connection so the agent rides a per-chat RubyLLM::Context instead of the process-global RubyLLM.config. This is the host-boot factory the bin/pikuri-* demos use in place of RubyLLM.configure — one isolated connection per agent, so several agents pointed at different servers (and different keys) don’t stomp a shared global.
server is the bare server origin; a trailing /v1 (the OpenAI-compatible suffix ruby_llm appends to reach /v1/chat/completions) is stripped and re-appended exactly once, so https://api.x.ai, https://api.x.ai/v1, and https://api.x.ai/v1/ all normalize to the same .../v1 base. Without this, a server value that already ended in /v1 would double to /v1/v1 and every request would 404.
105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/pikuri/agent/chat_transport.rb', line 105 def self.from_openai_server(server:, model:, api_key: 'not-needed', context_window: nil) base = server.to_s.strip.chomp('/').delete_suffix('/v1') new( model: model, provider: :openai, assume_model_exists: true, api_base: "#{base}/v1", api_key: api_key, context_window: context_window ) end |
Instance Method Details
#chat_kwargs ⇒ Hash{Symbol => String, Symbol, Boolean, nil}
The model-resolution kwargs to spread into RubyLLM.chat / RubyLLM::Context#chat. Excludes the connection fields — those configure the Context the chat is built from, not the chat call itself.
141 142 143 |
# File 'lib/pikuri/agent/chat_transport.rb', line 141 def chat_kwargs { model: model, provider: provider, assume_model_exists: assume_model_exists } end |
#connection_overrides? ⇒ Boolean
Whether this transport overrides the process-global connection (and so needs a dedicated RubyLLM::Context).
149 150 151 |
# File 'lib/pikuri/agent/chat_transport.rb', line 149 def connection_overrides? !api_base.nil? || !api_key.nil? end |
#inspect ⇒ String Also known as: to_s
Default Data#inspect would print api_key verbatim, leaking the secret into any log line, to_s interpolation, or backtrace that touches the transport. Redact it.
158 159 160 161 162 |
# File 'lib/pikuri/agent/chat_transport.rb', line 158 def inspect "#<#{self.class} model=#{model.inspect} provider=#{provider.inspect} " \ "assume_model_exists=#{assume_model_exists} api_base=#{api_base.inspect} " \ "api_key=#{api_key.nil? ? 'nil' : '[REDACTED]'} context_window=#{context_window.inspect}>" end |