Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/ruby_llm/active_record/chat_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ def prepare_content_for_storage(content)
case content
when RubyLLM::Content::Raw
content_raw = content.value
content_text = nil
content_text = content.respond_to?(:text) ? content.text.presence : nil
when RubyLLM::Content
attachments = content.attachments if content.attachments.any?
content_text = content.text
Expand Down
9 changes: 9 additions & 0 deletions lib/ruby_llm/aliases.json
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,15 @@
"gpt-5.6-terra": {
"bedrock": "openai.gpt-5.6-terra"
},
"gpt-6-luna": {
"bedrock": "openai.gpt-6-luna"
},
"gpt-6-sol": {
"bedrock": "openai.gpt-6-sol"
},
"gpt-6.1-sol": {
"bedrock": "openai.gpt-6.1-sol"
},
"gpt-audio": {
"openai": "gpt-audio",
"openrouter": "openai/gpt-audio"
Expand Down
16 changes: 14 additions & 2 deletions lib/ruby_llm/chat.rb
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,13 @@ def add_completion(response)
response
end

# Compacts the conversation through the provider's standalone compaction endpoint and
# returns the compaction Message without adding it to #messages; the caller decides
# whether to add it. Raises UnsupportedFeatureError for protocols without one.
def compact_context
@provider.compact(messages, model: @model, protocol: @protocol, headers: @headers)
end

# The request this chat would send for its next completion.
def render
@provider.render(
Expand Down Expand Up @@ -361,9 +368,14 @@ def provider_completion(&)
end

def normalize_schema_response(response)
return unless @schema && response.content.is_a?(String) && !response.tool_call?
return unless @schema && !response.tool_call?

response.content = JSON.parse(response.content)
case response.content
when String
response.content = JSON.parse(response.content)
when RubyLLM::Protocols::Responses::OutputItems
response.content = JSON.parse(response.content.text)
end
rescue JSON::ParserError
# If parsing fails, keep content as string.
end
Expand Down
1 change: 1 addition & 0 deletions lib/ruby_llm/error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class PromptNotFoundError < StandardError; end
class InvalidRoleError < StandardError; end
class InvalidToolChoiceError < StandardError; end
class ModelNotFoundError < StandardError; end
class UnsupportedFeatureError < StandardError; end

# Raised when RubyLLM cannot format an attachment for the selected provider.
class UnsupportedAttachmentError < StandardError
Expand Down
205 changes: 204 additions & 1 deletion lib/ruby_llm/models.json
Original file line number Diff line number Diff line change
Expand Up @@ -17978,6 +17978,209 @@
"knowledge": null
}
},
{
"id": "openai.gpt-6-luna",
"name": "GPT-6 Luna",
"provider": "bedrock",
"family": "gpt",
"created_at": "2026-10-05 00:00:00 UTC",
"context_window": 1050000,
"max_output_tokens": 128000,
"knowledge_cutoff": null,
"modalities": {
"input": [
"text",
"image",
"pdf"
],
"output": [
"text"
]
},
"capabilities": [
"function_calling",
"structured_output",
"reasoning",
"vision"
],
"pricing": {
"text_tokens": {
"standard": {
"input_per_million": 0.11,
"output_per_million": 0.55,
"cache_read_input_per_million": 0.011,
"cache_write_input_per_million": 0.1375
}
}
},
"metadata": {
"source": "manual",
"provider_id": "amazon-bedrock",
"open_weights": false,
"attachment": true,
"temperature": false,
"last_updated": "2026-10-05",
"reasoning_options": [
{
"type": "effort",
"values": [
"none",
"low",
"medium",
"high",
"xhigh",
"max"
]
}
],
"cost": {
"input": 0.11,
"output": 0.55,
"cache_read": 0.011,
"cache_write": 0.1375
},
"limit": {
"context": 1050000,
"output": 128000
},
"knowledge": null
}
},
{
"id": "openai.gpt-6-sol",
"name": "GPT-6 Sol",
"provider": "bedrock",
"family": "gpt",
"created_at": "2026-10-05 00:00:00 UTC",
"context_window": 1050000,
"max_output_tokens": 128000,
"knowledge_cutoff": null,
"modalities": {
"input": [
"text",
"image",
"pdf"
],
"output": [
"text"
]
},
"capabilities": [
"function_calling",
"structured_output",
"reasoning",
"vision"
],
"pricing": {
"text_tokens": {
"standard": {
"input_per_million": 2.2,
"output_per_million": 11.0,
"cache_read_input_per_million": 0.22,
"cache_write_input_per_million": 2.75
}
}
},
"metadata": {
"source": "manual",
"provider_id": "amazon-bedrock",
"open_weights": false,
"attachment": true,
"temperature": false,
"last_updated": "2026-10-05",
"reasoning_options": [
{
"type": "effort",
"values": [
"none",
"low",
"medium",
"high",
"xhigh",
"max"
]
}
],
"cost": {
"input": 2.2,
"output": 11.0,
"cache_read": 0.22,
"cache_write": 2.75
},
"limit": {
"context": 1050000,
"output": 128000
},
"knowledge": null
}
},
{
"id": "openai.gpt-6.1-sol",
"name": "GPT-6.1 Sol",
"provider": "bedrock",
"family": "gpt",
"created_at": "2026-10-05 00:00:00 UTC",
"context_window": 1050000,
"max_output_tokens": 128000,
"knowledge_cutoff": null,
"modalities": {
"input": [
"text",
"image",
"pdf"
],
"output": [
"text"
]
},
"capabilities": [
"function_calling",
"structured_output",
"reasoning",
"vision"
],
"pricing": {
"text_tokens": {
"standard": {
"input_per_million": 2.2,
"output_per_million": 11.0,
"cache_read_input_per_million": 0.11,
"cache_write_input_per_million": 2.75
}
}
},
"metadata": {
"source": "manual",
"provider_id": "amazon-bedrock",
"open_weights": false,
"attachment": true,
"temperature": false,
"last_updated": "2026-10-05",
"reasoning_options": [
{
"type": "effort",
"values": [
"low",
"medium",
"high",
"xhigh",
"max"
]
}
],
"cost": {
"input": 2.2,
"output": 11.0,
"cache_read": 0.11,
"cache_write": 2.75
},
"limit": {
"context": 1050000,
"output": 128000
},
"knowledge": null
}
},
{
"id": "openai.gpt-oss-120b",
"name": "gpt-oss-120b",
Expand Down Expand Up @@ -67385,4 +67588,4 @@
"owned_by": "xai"
}
}
]
]
28 changes: 16 additions & 12 deletions lib/ruby_llm/protocols/mantle_responses.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,51 @@ module RubyLLM
module Protocols
# AWS Bedrock's bedrock-mantle endpoint, speaking the OpenAI Responses API.
# Reachable only via bedrock-mantle (not bedrock-runtime), and only for models
# that physically cannot serve Converse (the GPT-5.x frontier
# family). Talks to the provider's mantle connection instead of the default
# that physically cannot serve Converse (the GPT-5.x and GPT-6.x frontier
# families). Talks to the provider's mantle connection instead of the default
# bedrock-runtime connection, and SigV4-signs every request against the
# "bedrock-mantle" service namespace instead of "bedrock".
class MantleResponses < Responses
# Frontier openai.gpt-5.x models are served at /openai/v1/responses; every other
# mantle model (e.g. openai.gpt-oss-*) is served at /v1/responses. See the
# AWS Bedrock model cards for GPT-5.6.
# Frontier openai.gpt-5.x and openai.gpt-6.x models are served at /openai/v1/responses;
# every other mantle model (e.g. openai.gpt-oss-*) is served at /v1/responses. See the
# AWS Bedrock model cards for GPT-5.6 and GPT-6.
#
# Kept in sync with Providers::Bedrock::MANTLE_ONLY_MODEL_PATTERN — that pattern picks
# mantle vs Converse, this one picks the /openai/v1 vs /v1 mantle path. They
# coincide today but are distinct concepts; update both when a new frontier family lands.
FRONTIER_GPT5_PATTERN = /\Aopenai\.gpt-5/
FRONTIER_GPT_PATTERN = /\Aopenai\.gpt-[56]/

def initialize(provider, model = nil)
super
@connection = provider.mantle_connection
end

# The '/v1/responses' branch is forward-looking: today Providers::Bedrock#protocol_for only
# ever routes /\Aopenai\.gpt-5/ ids here, so this branch is unreachable in production (e.g.
# ever routes /\Aopenai\.gpt-[56]/ ids here, so this branch is unreachable in production (e.g.
# openai.gpt-oss-* still routes to Converse). It exists so this protocol is ready if a
# future mantle-only, non-frontier model needs it, without another round of plumbing.
def completion_url
FRONTIER_GPT5_PATTERN.match?(@model.id) ? '/openai/v1/responses' : '/v1/responses'
FRONTIER_GPT_PATTERN.match?(@model.id) ? '/openai/v1/responses' : '/v1/responses'
end

private

def sync_response(payload, additional_headers = {})
super(payload, additional_headers.merge(mantle_signature_headers(payload)))
super(payload, additional_headers.merge(mantle_signature_headers(completion_url, payload)))
end

def stream_response(payload, additional_headers = {}, &)
super(payload, additional_headers.merge(mantle_signature_headers(payload)), &)
super(payload, additional_headers.merge(mantle_signature_headers(completion_url, payload)), &)
end

def mantle_signature_headers(payload)
def compaction_response(payload, additional_headers = {})
super(payload, additional_headers.merge(mantle_signature_headers(compaction_url, payload)))
end

def mantle_signature_headers(path, payload)
body = JSON.generate(payload)
@provider.sign_headers(
'POST', completion_url, body,
'POST', path, body,
base_url: @provider.mantle_api_base,
service: Providers::Bedrock::MANTLE_SIGNING_SERVICE,
region: @provider.mantle_region
Expand Down
19 changes: 19 additions & 0 deletions lib/ruby_llm/protocols/responses.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ class Responses < ChatCompletions
include Responses::Streaming
include Responses::Tools

# Compacts the conversation through the standalone /compact endpoint and returns the
# response.compaction object as an assistant Message holding OutputItems. GPT-6 only.
def compact(messages, headers: {})
unless gpt6_model?(model.id)
raise UnsupportedFeatureError, "Standalone compaction is only supported for GPT-6 models, not #{model.id}"
end

compaction_response(render_compaction_payload(messages), headers)
end

# Reasoning models reject the temperature parameter on this API.
def maybe_normalize_temperature(temperature, model)
return super unless reasoning_model?(model.id)
Expand All @@ -21,6 +31,15 @@ def maybe_normalize_temperature(temperature, model)
end
nil
end

private

def compaction_response(payload, additional_headers = {})
response = @connection.post compaction_url, payload do |req|
req.headers = additional_headers.merge(req.headers) unless additional_headers.empty?
end
parse_compaction_response response
end
end
end
end
Loading
Loading