Class: Pikuri::Agent::ChatTransport

Inherits:
Object
  • Object
show all
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

Class Method Summary collapse

Instance Method Summary collapse

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.

Parameters:

  • model (String, nil)
  • provider (Symbol, nil) (defaults to: nil)
  • assume_model_exists (Boolean) (defaults to: false)
  • api_base (String, nil) (defaults to: nil)
  • api_key (String, nil) (defaults to: nil)
  • context_window (Integer, nil) (defaults to: nil)

Raises:

  • (ArgumentError)

    if api_base or api_key is set without a provider (the provider names the config slots the connection overrides map onto)



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_baseString? (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.

Returns:

  • (String, nil)

    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_keyString? (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.

Returns:

  • (String, nil)

    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_existsBoolean (readonly)

Returns forwarded to RubyLLM.chat; true skips ruby_llm’s registry lookup and trusts the supplied model id. Requires provider.

Returns:

  • (Boolean)

    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_windowInteger? (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.

Returns:

  • (Integer, nil)

    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

#modelString? (readonly)

Returns LLM identifier; nil defers to RubyLLM.config.default_model at Pikuri::Agent construction time.

Returns:

  • (String, nil)

    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

#providerSymbol? (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).

Returns:

  • (Symbol, nil)

    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.

Parameters:

  • server (String)

    server origin, with or without a trailing /v1, e.g. localhost:8080 or api.x.ai/v1

  • model (String)

    model id served there, trusted verbatim (assume_model_exists is true, so it need not appear in ruby_llm’s bundled registry)

  • api_key (String) (defaults to: 'not-needed')

    API key for the server; the conventional “not-needed” placeholder for a keyless local server

  • context_window (Integer, nil) (defaults to: nil)

    explicit context-window cap for this model, or nil to defer to Pikuri::Agent::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)

Returns:

  • (ChatTransport)

    a transport whose api_base is the normalized .../v1 URL and whose api_key is api_key



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_kwargsHash{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.

Returns:

  • (Hash{Symbol => String, Symbol, Boolean, nil})


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).

Returns:

  • (Boolean)


149
150
151
# File 'lib/pikuri/agent/chat_transport.rb', line 149

def connection_overrides?
  !api_base.nil? || !api_key.nil?
end

#inspectString 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.

Returns:

  • (String)


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