little_ghost 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +122 -0
- data/docs/guides/Core Concepts.md +203 -0
- data/docs/guides/Getting Started.md +187 -0
- data/lib/little_ghost/ag_ui/adapter.rb +194 -0
- data/lib/little_ghost/ag_ui.rb +5 -0
- data/lib/little_ghost/agent/context_management.rb +285 -0
- data/lib/little_ghost/agent/delegation.rb +128 -0
- data/lib/little_ghost/agent/skills.rb +96 -0
- data/lib/little_ghost/agent/tool_loop.rb +239 -0
- data/lib/little_ghost/agent.rb +2111 -0
- data/lib/little_ghost/agent_builder.rb +191 -0
- data/lib/little_ghost/agent_interruptions.rb +197 -0
- data/lib/little_ghost/configuration.rb +337 -0
- data/lib/little_ghost/content.rb +324 -0
- data/lib/little_ghost/default_model_registry.rb +71 -0
- data/lib/little_ghost/errors.rb +48 -0
- data/lib/little_ghost/events.rb +264 -0
- data/lib/little_ghost/execution_state.rb +58 -0
- data/lib/little_ghost/instrumentation.rb +475 -0
- data/lib/little_ghost/invocation.rb +285 -0
- data/lib/little_ghost/lookup.rb +37 -0
- data/lib/little_ghost/mcp/client.rb +396 -0
- data/lib/little_ghost/mcp.rb +5 -0
- data/lib/little_ghost/message.rb +75 -0
- data/lib/little_ghost/model.rb +88 -0
- data/lib/little_ghost/model_capabilities.rb +126 -0
- data/lib/little_ghost/model_registry.rb +173 -0
- data/lib/little_ghost/model_request.rb +107 -0
- data/lib/little_ghost/model_response.rb +48 -0
- data/lib/little_ghost/path_set.rb +32 -0
- data/lib/little_ghost/prompt_resolver.rb +251 -0
- data/lib/little_ghost/providers/bedrock.rb +506 -0
- data/lib/little_ghost/providers/http_transport.rb +149 -0
- data/lib/little_ghost/providers/open_router.rb +171 -0
- data/lib/little_ghost/providers/openai.rb +27 -0
- data/lib/little_ghost/providers/openai_compatible.rb +745 -0
- data/lib/little_ghost/providers/sse_parser.rb +35 -0
- data/lib/little_ghost/run.rb +607 -0
- data/lib/little_ghost/run_context.rb +129 -0
- data/lib/little_ghost/run_result.rb +111 -0
- data/lib/little_ghost/runtime/hook.rb +31 -0
- data/lib/little_ghost/runtime.rb +392 -0
- data/lib/little_ghost/sandbox.rb +138 -0
- data/lib/little_ghost/session.rb +229 -0
- data/lib/little_ghost/session_store.rb +96 -0
- data/lib/little_ghost/session_stores/agent_core_memory.rb +1086 -0
- data/lib/little_ghost/session_stores/memory.rb +86 -0
- data/lib/little_ghost/skills/catalog.rb +283 -0
- data/lib/little_ghost/skills/skill.rb +60 -0
- data/lib/little_ghost/skills.rb +4 -0
- data/lib/little_ghost/stream_event.rb +49 -0
- data/lib/little_ghost/structured_output.rb +126 -0
- data/lib/little_ghost/subagents/agent_path.rb +63 -0
- data/lib/little_ghost/subagents/definition.rb +42 -0
- data/lib/little_ghost/subagents/manager.rb +1615 -0
- data/lib/little_ghost/support/callbacks.rb +151 -0
- data/lib/little_ghost/support/cancellation_token.rb +86 -0
- data/lib/little_ghost/support/class_attributes.rb +40 -0
- data/lib/little_ghost/support/content_capture.rb +150 -0
- data/lib/little_ghost/support/executor.rb +75 -0
- data/lib/little_ghost/support/interruptible_stream.rb +103 -0
- data/lib/little_ghost/support/loader.rb +263 -0
- data/lib/little_ghost/support/output_truncation.rb +71 -0
- data/lib/little_ghost/support/redactor.rb +66 -0
- data/lib/little_ghost/support.rb +34 -0
- data/lib/little_ghost/tool.rb +448 -0
- data/lib/little_ghost/tool_execution.rb +59 -0
- data/lib/little_ghost/tool_registry.rb +156 -0
- data/lib/little_ghost/tools/filesystem.rb +119 -0
- data/lib/little_ghost/tools/shell.rb +45 -0
- data/lib/little_ghost/tools/write_todos.rb +91 -0
- data/lib/little_ghost/tools.rb +6 -0
- data/lib/little_ghost/tracing/open_telemetry.rb +517 -0
- data/lib/little_ghost/unrestricted_sandbox.rb +306 -0
- data/lib/little_ghost/usage.rb +47 -0
- data/lib/little_ghost/version.rb +6 -0
- data/lib/little_ghost/workflow.rb +351 -0
- data/lib/little_ghost/workspace.rb +31 -0
- data/lib/little_ghost.rb +120 -0
- metadata +225 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "openai_compatible"
|
|
4
|
+
|
|
5
|
+
module LittleGhost
|
|
6
|
+
module Providers
|
|
7
|
+
# OpenRouter gives one LittleGhost provider access to models routed through
|
|
8
|
+
# OpenRouter. Agents keep their model roles while the registry selects an
|
|
9
|
+
# OpenRouter model for each role.
|
|
10
|
+
#
|
|
11
|
+
# provider = LittleGhost::Providers::OpenRouter.new(
|
|
12
|
+
# api_key: ENV.fetch("OPENROUTER_API_KEY"),
|
|
13
|
+
# model: ENV.fetch("OPENROUTER_MODEL"),
|
|
14
|
+
# app_name: "Support Console"
|
|
15
|
+
# )
|
|
16
|
+
#
|
|
17
|
+
# +site_url+ and +app_name+ populate attribution headers. Capability metadata
|
|
18
|
+
# controls structured output, tools, tool choice, and parameter filtering.
|
|
19
|
+
# Requests that need a capability ask OpenRouter to route only to providers
|
|
20
|
+
# that advertise it.
|
|
21
|
+
class OpenRouter < OpenAICompatible
|
|
22
|
+
# The OpenRouter API endpoint used when +base_url+ is omitted.
|
|
23
|
+
DEFAULT_BASE_URL = "https://openrouter.ai/api/v1/"
|
|
24
|
+
TOP_LEVEL_CACHE_MODELS = ["anthropic/", "~anthropic/"].freeze # :nodoc:
|
|
25
|
+
MESSAGE_CACHE_MODELS = [
|
|
26
|
+
"google/gemini",
|
|
27
|
+
"qwen/qwen3-max",
|
|
28
|
+
"qwen/qwen-plus",
|
|
29
|
+
"qwen/qwen3.6-plus",
|
|
30
|
+
"qwen/qwen3-coder-plus",
|
|
31
|
+
"qwen/qwen3-coder-flash",
|
|
32
|
+
"deepseek/deepseek-v3.2"
|
|
33
|
+
].freeze # :nodoc:
|
|
34
|
+
|
|
35
|
+
# Configures an OpenRouter client. All OpenAICompatible options, including
|
|
36
|
+
# +api_key+, +model+, retry limits, timeouts, and custom transport, apply.
|
|
37
|
+
def initialize(site_url: nil, app_name: nil, base_url: DEFAULT_BASE_URL, **arguments)
|
|
38
|
+
@site_url = site_url
|
|
39
|
+
@app_name = app_name
|
|
40
|
+
super(base_url:, api: :chat_completions, **arguments)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Reads model capabilities from OpenRouter +supported_parameters+ metadata.
|
|
44
|
+
# Missing metadata produces ModelCapabilities.unknown.
|
|
45
|
+
def capabilities(metadata: {})
|
|
46
|
+
parameters = metadata[:supported_parameters] || metadata["supported_parameters"]
|
|
47
|
+
return ModelCapabilities.unknown unless parameters.is_a?(Array)
|
|
48
|
+
|
|
49
|
+
values = parameters.map(&:to_s)
|
|
50
|
+
ModelCapabilities.new(
|
|
51
|
+
native_structured_output: values.any? { |value| %w[structured_outputs response_format].include?(value) },
|
|
52
|
+
tools: values.include?("tools"),
|
|
53
|
+
tool_choice: values.include?("tool_choice"),
|
|
54
|
+
supported_parameters: values
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Filters unsupported model settings when the request requires advertised
|
|
59
|
+
# capabilities.
|
|
60
|
+
def prepare_request(request, capabilities:)
|
|
61
|
+
return request if request.required_capabilities.empty?
|
|
62
|
+
return request unless capabilities.supported_parameters
|
|
63
|
+
|
|
64
|
+
settings = filter_supported_settings(request.settings, capabilities)
|
|
65
|
+
ModelRequest.new(
|
|
66
|
+
messages: request.messages,
|
|
67
|
+
tools: request.tools,
|
|
68
|
+
settings:,
|
|
69
|
+
output_schema: request.output_schema,
|
|
70
|
+
tool_choice: request.tool_choice,
|
|
71
|
+
required_capabilities: request.required_capabilities,
|
|
72
|
+
cancellation_token: request.cancellation_token,
|
|
73
|
+
deadline: request.deadline
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
MODEL_SETTING_PARAMETERS = { # :nodoc:
|
|
80
|
+
temperature: %w[temperature],
|
|
81
|
+
top_p: %w[top_p],
|
|
82
|
+
max_tokens: %w[max_tokens max_completion_tokens],
|
|
83
|
+
max_completion_tokens: %w[max_completion_tokens max_tokens],
|
|
84
|
+
stop: %w[stop],
|
|
85
|
+
seed: %w[seed],
|
|
86
|
+
reasoning: %w[reasoning reasoning_effort],
|
|
87
|
+
reasoning_effort: %w[reasoning reasoning_effort]
|
|
88
|
+
}.freeze
|
|
89
|
+
|
|
90
|
+
def filter_supported_settings(settings, capabilities)
|
|
91
|
+
result = settings.to_h.dup
|
|
92
|
+
MODEL_SETTING_PARAMETERS.each do |setting, parameters|
|
|
93
|
+
key = result.key?(setting) ? setting : setting.to_s
|
|
94
|
+
next unless result.key?(key)
|
|
95
|
+
|
|
96
|
+
result.delete(key) unless capabilities.supports_parameter?(*parameters)
|
|
97
|
+
end
|
|
98
|
+
max_tokens_key = result.key?(:max_tokens) ? :max_tokens : "max_tokens"
|
|
99
|
+
if result.key?(max_tokens_key) &&
|
|
100
|
+
!capabilities.supports_parameter?("max_tokens") &&
|
|
101
|
+
capabilities.supports_parameter?("max_completion_tokens")
|
|
102
|
+
result[result.key?(:max_tokens) ? :max_completion_tokens : "max_completion_tokens"] =
|
|
103
|
+
result.delete(max_tokens_key)
|
|
104
|
+
end
|
|
105
|
+
result.freeze
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def dynamic_headers(request)
|
|
109
|
+
session_id = request.settings[:session_id] || request.settings["session_id"]
|
|
110
|
+
compact_hash(
|
|
111
|
+
"HTTP-Referer" => @site_url,
|
|
112
|
+
"X-Title" => @app_name,
|
|
113
|
+
"x-session-id" => session_id&.to_s&.slice(0, 256)
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def provider_settings(settings)
|
|
118
|
+
result = super
|
|
119
|
+
reasoning = settings[:reasoning] || settings["reasoning"]
|
|
120
|
+
effort = settings[:reasoning_effort] || settings["reasoning_effort"]
|
|
121
|
+
result[:reasoning] = reasoning || {effort:} if reasoning || effort
|
|
122
|
+
session_id = settings[:session_id] || settings["session_id"]
|
|
123
|
+
result[:session_id] = session_id.to_s.slice(0, 256) if session_id
|
|
124
|
+
result[:cache_control] = {type: "ephemeral"} if model_prefix?(TOP_LEVEL_CACHE_MODELS)
|
|
125
|
+
prompt_cache_key = settings[:prompt_cache_key] || settings["prompt_cache_key"]
|
|
126
|
+
result[:prompt_cache_key] = prompt_cache_key if prompt_cache_key
|
|
127
|
+
result
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def chat_reasoning_fields(message)
|
|
131
|
+
return {} unless message.role == :assistant
|
|
132
|
+
|
|
133
|
+
blocks = message.content.grep(Content::Reasoning)
|
|
134
|
+
text = blocks.map(&:text).reject(&:empty?).join
|
|
135
|
+
details = blocks.flat_map { |block| Array(block.details) }
|
|
136
|
+
compact_hash(
|
|
137
|
+
reasoning: text.empty? ? nil : text,
|
|
138
|
+
reasoning_details: details.empty? ? nil : details
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def request_body(request)
|
|
143
|
+
body = super
|
|
144
|
+
if request.output_schema || !request.required_capabilities.empty?
|
|
145
|
+
body[:provider] = {require_parameters: true}
|
|
146
|
+
end
|
|
147
|
+
add_message_cache_control(body[:messages]) if model_prefix?(MESSAGE_CACHE_MODELS)
|
|
148
|
+
body
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def model_prefix?(prefixes)
|
|
152
|
+
normalized = model.downcase
|
|
153
|
+
prefixes.any? { |prefix| normalized.start_with?(prefix) }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def add_message_cache_control(messages)
|
|
157
|
+
message = messages.find { |candidate| %w[system developer].include?(candidate[:role]) }
|
|
158
|
+
return unless message
|
|
159
|
+
|
|
160
|
+
content = message[:content]
|
|
161
|
+
if content.is_a?(String)
|
|
162
|
+
message[:content] = [{type: "text", text: content, cache_control: {type: "ephemeral"}}]
|
|
163
|
+
return
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
block = Array(content).reverse.find { |candidate| candidate[:type] == "text" && !candidate.key?(:cache_control) }
|
|
167
|
+
block[:cache_control] = {type: "ephemeral"} if block
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "openai_compatible"
|
|
4
|
+
|
|
5
|
+
module LittleGhost
|
|
6
|
+
module Providers
|
|
7
|
+
# OpenAI connects LittleGhost agents to OpenAI models with streaming, tools,
|
|
8
|
+
# and structured results. It uses the Responses API by default.
|
|
9
|
+
#
|
|
10
|
+
# provider = LittleGhost::Providers::OpenAI.new(
|
|
11
|
+
# api_key: ENV.fetch("OPENAI_API_KEY"),
|
|
12
|
+
# model: ENV.fetch("OPENAI_MODEL")
|
|
13
|
+
# )
|
|
14
|
+
#
|
|
15
|
+
# Supply <tt>api: :chat_completions</tt> only when a model or integration requires
|
|
16
|
+
# the Chat Completions wire API.
|
|
17
|
+
class OpenAI < OpenAICompatible
|
|
18
|
+
# The OpenAI API endpoint used when +base_url+ is omitted.
|
|
19
|
+
DEFAULT_BASE_URL = "https://api.openai.com/v1/"
|
|
20
|
+
|
|
21
|
+
# Uses the official OpenAI API base URL by default.
|
|
22
|
+
def initialize(base_url: DEFAULT_BASE_URL, **arguments)
|
|
23
|
+
super
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|