riffer 0.48.0 → 0.50.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 +4 -4
- data/.claude/rules/comments.md +2 -4
- data/.claude/rules/rbs-inline.md +2 -12
- data/.release-please-manifest.json +1 -1
- data/.rubocop.yml +5 -0
- data/CHANGELOG.md +32 -0
- data/docs/AGENTS.md +3 -5
- data/docs/AGENT_LIFECYCLE.md +13 -16
- data/docs/CONFIGURATION.md +22 -33
- data/docs/STREAM_EVENTS.md +3 -3
- data/docs/TOOL_ADVANCED.md +1 -3
- data/docs/TRACING.md +1 -1
- data/docs/providers/AMAZON_BEDROCK.md +31 -0
- data/docs/providers/ANTHROPIC.md +10 -0
- data/docs/providers/AZURE_OPENAI.md +2 -0
- data/docs/providers/OPENAI.md +22 -0
- data/docs/providers/OPENROUTER.md +18 -1
- data/docs-site/build.rb +0 -6
- data/docs-site/check.rb +1 -5
- data/lib/riffer/agent/config.rb +4 -45
- data/lib/riffer/agent/context.rb +2 -26
- data/lib/riffer/agent/outcome.rb +0 -20
- data/lib/riffer/agent/response.rb +0 -38
- data/lib/riffer/agent/run.rb +11 -43
- data/lib/riffer/agent/serializer.rb +10 -39
- data/lib/riffer/agent/session/repair.rb +3 -15
- data/lib/riffer/agent/session.rb +25 -38
- data/lib/riffer/agent/structured_output/result.rb +0 -8
- data/lib/riffer/agent/structured_output.rb +0 -7
- data/lib/riffer/agent.rb +10 -130
- data/lib/riffer/config/amazon_bedrock.rb +30 -0
- data/lib/riffer/config/anthropic.rb +21 -0
- data/lib/riffer/config/azure_open_ai.rb +30 -0
- data/lib/riffer/config/evals.rb +18 -0
- data/lib/riffer/config/files.rb +61 -0
- data/lib/riffer/config/gemini.rb +21 -0
- data/lib/riffer/config/mcp.rb +31 -0
- data/lib/riffer/config/open_ai.rb +30 -0
- data/lib/riffer/config/open_router.rb +21 -0
- data/lib/riffer/config/pricing/rates.rb +36 -0
- data/lib/riffer/config/pricing.rb +64 -0
- data/lib/riffer/config/skills.rb +38 -0
- data/lib/riffer/config/tracing.rb +43 -0
- data/lib/riffer/config.rb +3 -362
- data/lib/riffer/evals/evaluator.rb +1 -29
- data/lib/riffer/evals/evaluator_runner.rb +0 -15
- data/lib/riffer/evals/judge.rb +0 -8
- data/lib/riffer/evals/result.rb +1 -11
- data/lib/riffer/evals/run_result.rb +0 -12
- data/lib/riffer/evals/scenario_result.rb +0 -19
- data/lib/riffer/files/downloader.rb +2 -3
- data/lib/riffer/files/resolver.rb +2 -7
- data/lib/riffer/guardrail.rb +2 -22
- data/lib/riffer/guardrails/modification.rb +0 -8
- data/lib/riffer/guardrails/result.rb +0 -17
- data/lib/riffer/guardrails/runner.rb +2 -11
- data/lib/riffer/guardrails/tripwire.rb +0 -8
- data/lib/riffer/guardrails.rb +0 -2
- data/lib/riffer/helpers/boolean.rb +0 -4
- data/lib/riffer/helpers/call_or_value.rb +0 -3
- data/lib/riffer/helpers/deep_dup.rb +3 -8
- data/lib/riffer/helpers/dependencies.rb +0 -4
- data/lib/riffer/helpers/identifier.rb +3 -12
- data/lib/riffer/helpers/validate.rb +42 -0
- data/lib/riffer/mcp/authenticated_tool.rb +4 -12
- data/lib/riffer/mcp/client.rb +0 -7
- data/lib/riffer/mcp/manifest.rb +3 -7
- data/lib/riffer/mcp/registration.rb +0 -10
- data/lib/riffer/mcp/registry.rb +0 -9
- data/lib/riffer/mcp/search_tool.rb +0 -4
- data/lib/riffer/mcp/tool.rb +1 -4
- data/lib/riffer/mcp/tool_factory.rb +3 -6
- data/lib/riffer/mcp.rb +2 -23
- data/lib/riffer/messages/assistant/reasoning_part.rb +4 -21
- data/lib/riffer/messages/assistant/tool_call.rb +1 -9
- data/lib/riffer/messages/assistant.rb +1 -21
- data/lib/riffer/messages/base.rb +0 -12
- data/lib/riffer/messages/system.rb +0 -3
- data/lib/riffer/messages/tool.rb +0 -15
- data/lib/riffer/messages/user/file_part.rb +2 -30
- data/lib/riffer/messages/user.rb +0 -4
- data/lib/riffer/params/boolean.rb +1 -5
- data/lib/riffer/params/param.rb +3 -31
- data/lib/riffer/params.rb +8 -41
- data/lib/riffer/providers/amazon_bedrock.rb +98 -46
- data/lib/riffer/providers/anthropic.rb +65 -65
- data/lib/riffer/providers/azure_open_ai.rb +10 -8
- data/lib/riffer/providers/base.rb +7 -28
- data/lib/riffer/providers/finish_reason.rb +0 -6
- data/lib/riffer/providers/gemini/client.rb +4 -17
- data/lib/riffer/providers/gemini.rb +4 -10
- data/lib/riffer/providers/mock.rb +1 -26
- data/lib/riffer/providers/open_ai.rb +94 -46
- data/lib/riffer/providers/open_router.rb +105 -30
- data/lib/riffer/providers/repository.rb +2 -14
- data/lib/riffer/providers/token_usage.rb +5 -14
- data/lib/riffer/registrable.rb +11 -45
- data/lib/riffer/runner/fibers.rb +1 -6
- data/lib/riffer/runner/sequential.rb +0 -1
- data/lib/riffer/runner/threaded.rb +0 -3
- data/lib/riffer/runner.rb +0 -3
- data/lib/riffer/skills/activate_tool.rb +0 -3
- data/lib/riffer/skills/adapter.rb +0 -8
- data/lib/riffer/skills/backend.rb +2 -7
- data/lib/riffer/skills/config.rb +4 -20
- data/lib/riffer/skills/context.rb +0 -29
- data/lib/riffer/skills/filesystem_backend.rb +0 -7
- data/lib/riffer/skills/frontmatter.rb +2 -16
- data/lib/riffer/skills/markdown_adapter.rb +3 -6
- data/lib/riffer/skills/xml_adapter.rb +0 -3
- data/lib/riffer/stream_events/base.rb +0 -3
- data/lib/riffer/stream_events/finish_reason_done.rb +1 -6
- data/lib/riffer/stream_events/guardrail_modification.rb +0 -10
- data/lib/riffer/stream_events/guardrail_tripwire.rb +0 -10
- data/lib/riffer/stream_events/interrupt.rb +2 -12
- data/lib/riffer/stream_events/reasoning_delta.rb +0 -3
- data/lib/riffer/stream_events/reasoning_done.rb +1 -5
- data/lib/riffer/stream_events/skill_activation.rb +0 -3
- data/lib/riffer/stream_events/text_delta.rb +0 -2
- data/lib/riffer/stream_events/text_done.rb +0 -2
- data/lib/riffer/stream_events/token_usage_done.rb +0 -2
- data/lib/riffer/stream_events/tool_call_delta.rb +1 -5
- data/lib/riffer/stream_events/tool_call_done.rb +0 -5
- data/lib/riffer/stream_events/web_search_done.rb +0 -3
- data/lib/riffer/stream_events/web_search_status.rb +1 -5
- data/lib/riffer/testing/minitest.rb +4 -5
- data/lib/riffer/testing.rb +5 -38
- data/lib/riffer/tool.rb +2 -28
- data/lib/riffer/tools/response.rb +3 -32
- data/lib/riffer/tools/runtime/fibers.rb +0 -6
- data/lib/riffer/tools/runtime/inline.rb +0 -1
- data/lib/riffer/tools/runtime/threaded.rb +0 -6
- data/lib/riffer/tools/runtime.rb +5 -26
- data/lib/riffer/tools/toolable.rb +0 -37
- data/lib/riffer/tracing/capture.rb +3 -5
- data/lib/riffer/tracing/no_op.rb +0 -7
- data/lib/riffer/tracing/otel.rb +7 -16
- data/lib/riffer/tracing/stream_recorder.rb +0 -7
- data/lib/riffer/tracing.rb +4 -27
- data/lib/riffer/version.rb +1 -1
- data/lib/riffer.rb +2 -30
- data/sig/generated/riffer/agent/config.rbs +12 -48
- data/sig/generated/riffer/agent/context.rbs +2 -26
- data/sig/generated/riffer/agent/outcome.rbs +0 -20
- data/sig/generated/riffer/agent/response.rbs +1 -29
- data/sig/generated/riffer/agent/run.rbs +1 -27
- data/sig/generated/riffer/agent/serializer.rbs +2 -27
- data/sig/generated/riffer/agent/session/repair.rbs +2 -10
- data/sig/generated/riffer/agent/session.rbs +10 -36
- data/sig/generated/riffer/agent/structured_output/result.rbs +0 -7
- data/sig/generated/riffer/agent/structured_output.rbs +0 -7
- data/sig/generated/riffer/agent.rbs +5 -121
- data/sig/generated/riffer/config/amazon_bedrock.rbs +21 -0
- data/sig/generated/riffer/config/anthropic.rbs +15 -0
- data/sig/generated/riffer/config/azure_open_ai.rbs +21 -0
- data/sig/generated/riffer/config/evals.rbs +13 -0
- data/sig/generated/riffer/config/files.rbs +43 -0
- data/sig/generated/riffer/config/gemini.rbs +15 -0
- data/sig/generated/riffer/config/mcp.rbs +19 -0
- data/sig/generated/riffer/config/open_ai.rbs +21 -0
- data/sig/generated/riffer/config/open_router.rbs +15 -0
- data/sig/generated/riffer/config/pricing/rates.rbs +19 -0
- data/sig/generated/riffer/config/pricing.rbs +31 -0
- data/sig/generated/riffer/config/skills.rbs +19 -0
- data/sig/generated/riffer/config/tracing.rbs +25 -0
- data/sig/generated/riffer/config.rbs +2 -307
- data/sig/generated/riffer/evals/evaluator.rbs +0 -28
- data/sig/generated/riffer/evals/evaluator_runner.rbs +0 -14
- data/sig/generated/riffer/evals/judge.rbs +0 -7
- data/sig/generated/riffer/evals/result.rbs +1 -11
- data/sig/generated/riffer/evals/run_result.rbs +0 -12
- data/sig/generated/riffer/evals/scenario_result.rbs +0 -14
- data/sig/generated/riffer/files/resolver.rbs +0 -7
- data/sig/generated/riffer/guardrail.rbs +2 -22
- data/sig/generated/riffer/guardrails/modification.rbs +0 -6
- data/sig/generated/riffer/guardrails/result.rbs +0 -15
- data/sig/generated/riffer/guardrails/runner.rbs +0 -11
- data/sig/generated/riffer/guardrails/tripwire.rbs +0 -8
- data/sig/generated/riffer/guardrails.rbs +0 -2
- data/sig/generated/riffer/helpers/boolean.rbs +0 -4
- data/sig/generated/riffer/helpers/call_or_value.rbs +0 -3
- data/sig/generated/riffer/helpers/deep_dup.rbs +0 -8
- data/sig/generated/riffer/helpers/dependencies.rbs +0 -4
- data/sig/generated/riffer/helpers/identifier.rbs +0 -12
- data/sig/generated/riffer/helpers/validate.rbs +21 -0
- data/sig/generated/riffer/mcp/authenticated_tool.rbs +0 -5
- data/sig/generated/riffer/mcp/client.rbs +0 -7
- data/sig/generated/riffer/mcp/manifest.rbs +3 -7
- data/sig/generated/riffer/mcp/registration.rbs +0 -10
- data/sig/generated/riffer/mcp/registry.rbs +0 -9
- data/sig/generated/riffer/mcp/search_tool.rbs +0 -4
- data/sig/generated/riffer/mcp/tool.rbs +0 -4
- data/sig/generated/riffer/mcp/tool_factory.rbs +0 -6
- data/sig/generated/riffer/mcp.rbs +2 -22
- data/sig/generated/riffer/messages/assistant/reasoning_part.rbs +4 -17
- data/sig/generated/riffer/messages/assistant/tool_call.rbs +1 -9
- data/sig/generated/riffer/messages/assistant.rbs +10 -30
- data/sig/generated/riffer/messages/base.rbs +0 -12
- data/sig/generated/riffer/messages/system.rbs +0 -3
- data/sig/generated/riffer/messages/tool.rbs +0 -12
- data/sig/generated/riffer/messages/user/file_part.rbs +0 -30
- data/sig/generated/riffer/messages/user.rbs +0 -4
- data/sig/generated/riffer/params/boolean.rbs +1 -4
- data/sig/generated/riffer/params/param.rbs +0 -31
- data/sig/generated/riffer/params.rbs +4 -40
- data/sig/generated/riffer/providers/amazon_bedrock.rbs +26 -26
- data/sig/generated/riffer/providers/anthropic.rbs +23 -22
- data/sig/generated/riffer/providers/azure_open_ai.rbs +5 -8
- data/sig/generated/riffer/providers/base.rbs +0 -28
- data/sig/generated/riffer/providers/finish_reason.rbs +0 -6
- data/sig/generated/riffer/providers/gemini/client.rbs +4 -17
- data/sig/generated/riffer/providers/gemini.rbs +0 -6
- data/sig/generated/riffer/providers/mock.rbs +0 -26
- data/sig/generated/riffer/providers/open_ai.rbs +36 -19
- data/sig/generated/riffer/providers/open_router.rbs +33 -14
- data/sig/generated/riffer/providers/repository.rbs +2 -14
- data/sig/generated/riffer/providers/token_usage.rbs +5 -14
- data/sig/generated/riffer/registrable.rbs +0 -45
- data/sig/generated/riffer/runner/fibers.rbs +0 -6
- data/sig/generated/riffer/runner/sequential.rbs +0 -1
- data/sig/generated/riffer/runner/threaded.rbs +0 -3
- data/sig/generated/riffer/runner.rbs +0 -3
- data/sig/generated/riffer/skills/activate_tool.rbs +0 -3
- data/sig/generated/riffer/skills/adapter.rbs +0 -8
- data/sig/generated/riffer/skills/backend.rbs +2 -7
- data/sig/generated/riffer/skills/config.rbs +0 -20
- data/sig/generated/riffer/skills/context.rbs +0 -29
- data/sig/generated/riffer/skills/filesystem_backend.rbs +0 -7
- data/sig/generated/riffer/skills/frontmatter.rbs +2 -16
- data/sig/generated/riffer/skills/markdown_adapter.rbs +0 -6
- data/sig/generated/riffer/skills/xml_adapter.rbs +0 -3
- data/sig/generated/riffer/stream_events/base.rbs +0 -3
- data/sig/generated/riffer/stream_events/finish_reason_done.rbs +1 -6
- data/sig/generated/riffer/stream_events/guardrail_modification.rbs +0 -10
- data/sig/generated/riffer/stream_events/guardrail_tripwire.rbs +0 -10
- data/sig/generated/riffer/stream_events/interrupt.rbs +2 -10
- data/sig/generated/riffer/stream_events/reasoning_delta.rbs +0 -3
- data/sig/generated/riffer/stream_events/reasoning_done.rbs +1 -5
- data/sig/generated/riffer/stream_events/skill_activation.rbs +0 -3
- data/sig/generated/riffer/stream_events/text_delta.rbs +0 -2
- data/sig/generated/riffer/stream_events/text_done.rbs +0 -2
- data/sig/generated/riffer/stream_events/token_usage_done.rbs +0 -2
- data/sig/generated/riffer/stream_events/tool_call_delta.rbs +1 -5
- data/sig/generated/riffer/stream_events/tool_call_done.rbs +0 -5
- data/sig/generated/riffer/stream_events/web_search_done.rbs +0 -3
- data/sig/generated/riffer/stream_events/web_search_status.rbs +1 -5
- data/sig/generated/riffer/testing.rbs +0 -38
- data/sig/generated/riffer/tool.rbs +0 -27
- data/sig/generated/riffer/tools/response.rbs +3 -29
- data/sig/generated/riffer/tools/runtime/fibers.rbs +0 -5
- data/sig/generated/riffer/tools/runtime/inline.rbs +0 -1
- data/sig/generated/riffer/tools/runtime/threaded.rbs +0 -5
- data/sig/generated/riffer/tools/runtime.rbs +2 -24
- data/sig/generated/riffer/tools/toolable.rbs +0 -37
- data/sig/generated/riffer/tracing/capture.rbs +2 -5
- data/sig/generated/riffer/tracing/no_op.rbs +0 -7
- data/sig/generated/riffer/tracing/otel.rbs +7 -16
- data/sig/generated/riffer/tracing/stream_recorder.rbs +0 -2
- data/sig/generated/riffer/tracing.rbs +4 -23
- data/sig/generated/riffer.rbs +2 -29
- data/sig/manual/riffer/helpers/validate.rbs +5 -0
- metadata +30 -1
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
# rbs_inline: enabled
|
|
3
3
|
|
|
4
|
-
# Azure OpenAI provider for GPT models hosted on Azure. Requires the +openai+
|
|
5
|
-
# gem. Credentials resolve from config, then +AZURE_OPENAI_API_KEY+ /
|
|
6
|
-
# +AZURE_OPENAI_ENDPOINT+.
|
|
7
4
|
class Riffer::Providers::AzureOpenAI < Riffer::Providers::OpenAI
|
|
8
|
-
# The GenAI semconv well-known provider name.
|
|
9
5
|
#--
|
|
10
6
|
#: () -> String
|
|
11
7
|
def self.semconv_provider_name
|
|
@@ -20,15 +16,21 @@ class Riffer::Providers::AzureOpenAI < Riffer::Providers::OpenAI
|
|
|
20
16
|
Riffer.config.azure_openai.client
|
|
21
17
|
end
|
|
22
18
|
|
|
23
|
-
# Deliberately not compacted: this borrows the OpenAI SDK to talk to Azure, so
|
|
24
|
-
# omitting an unset argument would let the SDK fall back to +OPENAI_API_KEY+
|
|
25
|
-
# and +OPENAI_BASE_URL+ — sending Azure traffic, and an OpenAI credential, to
|
|
26
|
-
# whatever those name. Passing nil raises in the SDK instead.
|
|
27
19
|
#--
|
|
28
20
|
#: () -> untyped
|
|
29
21
|
def build_client
|
|
30
22
|
api_key = Riffer.config.azure_openai.api_key || ENV.fetch("AZURE_OPENAI_API_KEY", nil)
|
|
31
23
|
base_url = Riffer.config.azure_openai.endpoint || ENV.fetch("AZURE_OPENAI_ENDPOINT", nil)
|
|
24
|
+
# Pass nils rather than omitting them: an omitted argument lets the SDK fall
|
|
25
|
+
# back to OPENAI_API_KEY / OPENAI_BASE_URL, sending Azure traffic and an
|
|
26
|
+
# OpenAI credential elsewhere. A nil raises in the SDK instead.
|
|
32
27
|
::OpenAI::Client.new(api_key: api_key, base_url: base_url)
|
|
33
28
|
end
|
|
29
|
+
|
|
30
|
+
# Azure resolves and decrypts reasoning items only within the resource that produced them.
|
|
31
|
+
#--
|
|
32
|
+
#: () -> String
|
|
33
|
+
def reasoning_format
|
|
34
|
+
"azure-openai-v1"
|
|
35
|
+
end
|
|
34
36
|
end
|
|
@@ -3,10 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
require "json"
|
|
5
5
|
|
|
6
|
-
# Base class for all LLM providers. A template-method flow: subclasses implement
|
|
7
|
-
# the hooks (+build_request_params+, +execute_generate+, +execute_stream+,
|
|
8
|
-
# +extract_token_usage+, +extract_content+, +extract_tool_calls+) and the base
|
|
9
|
-
# class orchestrates them.
|
|
10
6
|
class Riffer::Providers::Base
|
|
11
7
|
# @rbs @current_tools: Array[singleton(Riffer::Tool)]
|
|
12
8
|
# @rbs @current_model: String?
|
|
@@ -15,18 +11,12 @@ class Riffer::Providers::Base
|
|
|
15
11
|
|
|
16
12
|
WIRE_SEPARATOR = "__" #: String
|
|
17
13
|
|
|
18
|
-
# Returns the preferred skill adapter for this provider; override in
|
|
19
|
-
# subclasses (optionally introspecting +model+) for provider-specific formats.
|
|
20
14
|
#--
|
|
21
15
|
#: (?String?) -> singleton(Riffer::Skills::Adapter)
|
|
22
16
|
def self.skills_adapter(_model = nil)
|
|
23
17
|
Riffer::Skills::MarkdownAdapter
|
|
24
18
|
end
|
|
25
19
|
|
|
26
|
-
# Returns the provider name stamped as <tt>gen_ai.provider.name</tt> on trace
|
|
27
|
-
# spans, ideally a GenAI semconv well-known value. Defaults to the snake_cased
|
|
28
|
-
# class name rather than raising like the abstract provider methods, so
|
|
29
|
-
# enabling tracing never breaks an otherwise-working custom provider.
|
|
30
20
|
#--
|
|
31
21
|
#: () -> String
|
|
32
22
|
def self.semconv_provider_name
|
|
@@ -34,11 +24,11 @@ class Riffer::Providers::Base
|
|
|
34
24
|
# later must pick up its real name, not a frozen "unknown".
|
|
35
25
|
class_name = name or return "unknown"
|
|
36
26
|
|
|
27
|
+
# A default rather than NotImplementedError, so enabling tracing never
|
|
28
|
+
# breaks an otherwise-working custom provider.
|
|
37
29
|
@semconv_provider_name ||= Riffer::Helpers::Identifier.derive(class_name.split("::").last)
|
|
38
30
|
end
|
|
39
31
|
|
|
40
|
-
# Generates text using the provider.
|
|
41
|
-
#
|
|
42
32
|
#--
|
|
43
33
|
#: (?prompt: String?, ?system: String?, ?messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?model: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::User::FilePart]?, **untyped) -> Riffer::Messages::Assistant
|
|
44
34
|
def generate_text(prompt: nil, system: nil, messages: nil, model: nil, files: nil, **options)
|
|
@@ -77,8 +67,6 @@ class Riffer::Providers::Base
|
|
|
77
67
|
end
|
|
78
68
|
end
|
|
79
69
|
|
|
80
|
-
# Streams text from the provider.
|
|
81
|
-
#
|
|
82
70
|
#--
|
|
83
71
|
#: (?prompt: String?, ?system: String?, ?messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?model: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::User::FilePart]?, **untyped) -> Enumerator[Riffer::StreamEvents::Base, void]
|
|
84
72
|
def stream_text(prompt: nil, system: nil, messages: nil, model: nil, files: nil, **options)
|
|
@@ -119,21 +107,17 @@ class Riffer::Providers::Base
|
|
|
119
107
|
Riffer::Helpers::Dependencies.depends_on(gem_name)
|
|
120
108
|
end
|
|
121
109
|
|
|
122
|
-
# Returns the client for the current LLM call. A configured client wins,
|
|
123
|
-
# resolved on every call so a Proc can vary the client by process or
|
|
124
|
-
# credential lifetime; otherwise the provider builds one from the configured
|
|
125
|
-
# credentials, memoized for the life of the provider.
|
|
126
110
|
#--
|
|
127
111
|
#: () -> untyped
|
|
128
112
|
def client
|
|
129
113
|
configured = global_client
|
|
114
|
+
# Resolved on every call, never memoized, so a Proc can vary the client by
|
|
115
|
+
# process or credential lifetime.
|
|
130
116
|
return Riffer::Helpers::CallOrValue.resolve(configured) if configured
|
|
131
117
|
|
|
132
118
|
@client ||= build_client
|
|
133
119
|
end
|
|
134
120
|
|
|
135
|
-
# Returns the consumer-configured client for this provider; nil when none is
|
|
136
|
-
# configured, and for providers that take no configuration at all.
|
|
137
121
|
#--
|
|
138
122
|
#: () -> untyped
|
|
139
123
|
def global_client
|
|
@@ -218,19 +202,17 @@ class Riffer::Providers::Base
|
|
|
218
202
|
pricing.rates_for("#{key}/#{model}")
|
|
219
203
|
end
|
|
220
204
|
|
|
221
|
-
# Defaults to nil rather than raising — finish reasons are optional, so
|
|
222
|
-
# providers that don't report one stay valid.
|
|
223
205
|
#--
|
|
224
206
|
#: (untyped) -> Riffer::Providers::FinishReason?
|
|
225
207
|
def extract_finish_reason(_response)
|
|
208
|
+
# Optional hook, so providers that don't report a finish reason stay valid.
|
|
226
209
|
nil
|
|
227
210
|
end
|
|
228
211
|
|
|
229
|
-
# Defaults to no parts rather than raising — reasoning parts are optional, so
|
|
230
|
-
# providers that don't expose replayable reasoning stay valid.
|
|
231
212
|
#--
|
|
232
213
|
#: (untyped) -> Array[Riffer::Messages::Assistant::ReasoningPart]
|
|
233
214
|
def extract_reasoning(_response)
|
|
215
|
+
# Optional hook, so providers that don't expose replayable reasoning stay valid.
|
|
234
216
|
[]
|
|
235
217
|
end
|
|
236
218
|
|
|
@@ -295,8 +277,6 @@ class Riffer::Providers::Base
|
|
|
295
277
|
attributes.merge(tag_attributes(options[:tags] || {}))
|
|
296
278
|
end
|
|
297
279
|
|
|
298
|
-
# Maps normalized tags to their namespaced span attribute form. An empty map
|
|
299
|
-
# yields an empty hash, so merging it is a no-op.
|
|
300
280
|
#--
|
|
301
281
|
#: (Hash[String, String]) -> Hash[String, String]
|
|
302
282
|
def tag_attributes(tags)
|
|
@@ -358,11 +338,10 @@ class Riffer::Providers::Base
|
|
|
358
338
|
Riffer.config.tracing.capture_messages && span.recording?
|
|
359
339
|
end
|
|
360
340
|
|
|
361
|
-
# Wraps reasoning text that an adapter cannot yet replay in a +:text+ part
|
|
362
|
-
# with no +format+, so it persists for display but is never sent back.
|
|
363
341
|
#--
|
|
364
342
|
#: (Riffer::Providers::_EventSink, String) -> void
|
|
365
343
|
def yield_reasoning_done(yielder, text)
|
|
344
|
+
# No +format+, so the reasoning persists for display but is never replayed.
|
|
366
345
|
part = Riffer::Messages::Assistant::ReasoningPart.new(type: :text, text: text)
|
|
367
346
|
yielder << Riffer::StreamEvents::ReasoningDone.new(part)
|
|
368
347
|
end
|
|
@@ -1,19 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
# rbs_inline: enabled
|
|
3
3
|
|
|
4
|
-
# Normalized reason an LLM call finished, paired with the provider's raw
|
|
5
|
-
# wire value. +reason+ carries the same meaning for every provider.
|
|
6
4
|
class Riffer::Providers::FinishReason
|
|
7
|
-
# The normalized vocabulary every provider maps into.
|
|
8
5
|
VALUES = %i[stop length tool_calls content_filter context_window malformed_output error other].freeze #: Array[Symbol]
|
|
9
6
|
|
|
10
|
-
# The normalized reason.
|
|
11
7
|
attr_reader :reason #: Symbol # @dynamic reason
|
|
12
8
|
|
|
13
|
-
# The provider's raw finish-reason value, when one exists on the wire.
|
|
14
9
|
attr_reader :raw #: String? # @dynamic raw
|
|
15
10
|
|
|
16
|
-
# Raises Riffer::ArgumentError when +reason+ is outside VALUES.
|
|
17
11
|
#--
|
|
18
12
|
#: (reason: Symbol, ?raw: String?) -> void
|
|
19
13
|
def initialize(reason:, raw: nil)
|
|
@@ -5,18 +5,8 @@ require "json"
|
|
|
5
5
|
require "net/http"
|
|
6
6
|
require "uri"
|
|
7
7
|
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# and assign it to <tt>Riffer.config.gemini.client</tt>. Any object
|
|
11
|
-
# implementing +post+ and +post_stream+ with these contracts works there —
|
|
12
|
-
# the class is a default implementation, not a required base.
|
|
13
|
-
#
|
|
14
|
-
# Riffer.configure do |config|
|
|
15
|
-
# config.gemini.client = Riffer::Providers::Gemini::Client.new(
|
|
16
|
-
# api_key: ENV["GEMINI_API_KEY"],
|
|
17
|
-
# read_timeout: 120
|
|
18
|
-
# )
|
|
19
|
-
# end
|
|
8
|
+
# <tt>Riffer.config.gemini.client</tt> accepts any object implementing +post+ and +post_stream+
|
|
9
|
+
# with these contracts; this class is the default, not a required base.
|
|
20
10
|
class Riffer::Providers::Gemini::Client
|
|
21
11
|
# @rbs @api_key: String?
|
|
22
12
|
# @rbs @base_url: String
|
|
@@ -43,8 +33,7 @@ class Riffer::Providers::Gemini::Client
|
|
|
43
33
|
@proxy_port = proxy_port
|
|
44
34
|
end
|
|
45
35
|
|
|
46
|
-
#
|
|
47
|
-
# Raises Riffer::Error when the API responds with a non-success status.
|
|
36
|
+
# Raises Riffer::Error on a non-success status.
|
|
48
37
|
#--
|
|
49
38
|
#: (String, Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
50
39
|
def post(path, body)
|
|
@@ -54,9 +43,7 @@ class Riffer::Providers::Gemini::Client
|
|
|
54
43
|
JSON.parse(response.body, symbolize_names: true)
|
|
55
44
|
end
|
|
56
45
|
|
|
57
|
-
#
|
|
58
|
-
# they arrive. Raises Riffer::Error when the API responds with a
|
|
59
|
-
# non-success status.
|
|
46
|
+
# Raises Riffer::Error on a non-success status.
|
|
60
47
|
#--
|
|
61
48
|
#: (String, Hash[Symbol, untyped]) { (String) -> void } -> void
|
|
62
49
|
def post_stream(path, body, &block)
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
require "json"
|
|
5
5
|
require "securerandom"
|
|
6
6
|
|
|
7
|
-
# Google Gemini provider for Gemini models via the Gemini REST API.
|
|
8
7
|
class Riffer::Providers::Gemini < Riffer::Providers::Base
|
|
9
8
|
VALID_MODEL_PATTERN = /\A[a-zA-Z0-9._-]+\z/ #: Regexp
|
|
10
9
|
|
|
@@ -29,7 +28,6 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
|
|
|
29
28
|
"FINISH_REASON_UNSPECIFIED" => :other,
|
|
30
29
|
}.freeze #: Hash[String, Symbol]
|
|
31
30
|
|
|
32
|
-
# The GenAI semconv well-known provider name.
|
|
33
31
|
#--
|
|
34
32
|
#: () -> String
|
|
35
33
|
def self.semconv_provider_name
|
|
@@ -76,10 +74,8 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
|
|
|
76
74
|
}]
|
|
77
75
|
end
|
|
78
76
|
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
# stripped here rather than mapped. Native labels would arrive with a Vertex
|
|
82
|
-
# adapter. See docs/CONFIGURATION.md.
|
|
77
|
+
# The Gemini Developer API has no request labels field and rejects unknown
|
|
78
|
+
# body fields, so :tags reach observability only.
|
|
83
79
|
generation_config = options.except(:tools, :structured_output, :tags)
|
|
84
80
|
|
|
85
81
|
if structured_output
|
|
@@ -144,8 +140,6 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
|
|
|
144
140
|
build_finish_reason(response.dig(:candidates, 0, :finishReason), tool_calls: has_function_call)
|
|
145
141
|
end
|
|
146
142
|
|
|
147
|
-
# Gemini reports STOP even when the candidate carries functionCall parts,
|
|
148
|
-
# so tool-call presence overrides the raw value.
|
|
149
143
|
#--
|
|
150
144
|
#: (String?, tool_calls: bool) -> Riffer::Providers::FinishReason?
|
|
151
145
|
def build_finish_reason(raw_reason, tool_calls:)
|
|
@@ -153,18 +147,18 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
|
|
|
153
147
|
|
|
154
148
|
raw = raw_reason.to_s
|
|
155
149
|
reason = FINISH_REASONS.fetch(raw, :other)
|
|
150
|
+
# Gemini reports STOP even when the candidate carries functionCall parts.
|
|
156
151
|
reason = :tool_calls if reason == :stop && tool_calls
|
|
157
152
|
Riffer::Providers::FinishReason.new(reason: reason, raw: raw)
|
|
158
153
|
end
|
|
159
154
|
|
|
160
|
-
# Gemini reports thinking tokens outside +candidatesTokenCount+;
|
|
161
|
-
# TokenUsage's output includes them.
|
|
162
155
|
#--
|
|
163
156
|
#: (Hash[Symbol, untyped]) -> Riffer::Providers::TokenUsage
|
|
164
157
|
def build_token_usage(usage)
|
|
165
158
|
apply_pricing(
|
|
166
159
|
Riffer::Providers::TokenUsage.new(
|
|
167
160
|
input_tokens: usage[:promptTokenCount] || 0,
|
|
161
|
+
# Gemini reports thinking tokens outside candidatesTokenCount.
|
|
168
162
|
output_tokens: (usage[:candidatesTokenCount] || 0) + (usage[:thoughtsTokenCount] || 0),
|
|
169
163
|
cache_read_tokens: usage[:cachedContentTokenCount],
|
|
170
164
|
),
|
|
@@ -1,41 +1,28 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
# rbs_inline: enabled
|
|
3
3
|
|
|
4
|
-
# Mock provider for mocking LLM responses in tests; no external gems required.
|
|
5
4
|
class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
6
5
|
# @rbs @responses: Array[Hash[Symbol, untyped]]
|
|
7
6
|
# @rbs @current_index: Integer
|
|
8
7
|
# @rbs @stubbed_responses: Array[Hash[Symbol, untyped]]
|
|
9
8
|
|
|
10
|
-
# Returns the skill adapter for the mock model — XML when the model name
|
|
11
|
-
# contains +claude+ (mirroring a real Claude provider), else Markdown.
|
|
12
9
|
#--
|
|
13
10
|
#: (?String?) -> singleton(Riffer::Skills::Adapter)
|
|
14
11
|
def self.skills_adapter(model = nil)
|
|
12
|
+
# Mirrors the adapter a real Claude provider picks.
|
|
15
13
|
return Riffer::Skills::XmlAdapter if model&.include?("claude")
|
|
16
14
|
|
|
17
15
|
Riffer::Skills::MarkdownAdapter
|
|
18
16
|
end
|
|
19
17
|
|
|
20
|
-
# The GenAI semconv well-known provider name.
|
|
21
18
|
#--
|
|
22
19
|
#: () -> String
|
|
23
20
|
def self.semconv_provider_name
|
|
24
21
|
"mock"
|
|
25
22
|
end
|
|
26
23
|
|
|
27
|
-
# Array of recorded method calls for assertions.
|
|
28
24
|
attr_reader :calls #: Array[Hash[Symbol, untyped]] # @dynamic calls
|
|
29
25
|
|
|
30
|
-
# +responses:+ pre-configures canned responses (same shape as
|
|
31
|
-
# +#stub_response+) for standalone use; agent tests queue responses on
|
|
32
|
-
# <tt>agent.provider</tt> via +#stub_response+ instead.
|
|
33
|
-
#
|
|
34
|
-
# Riffer::Providers::Mock.new(responses: [
|
|
35
|
-
# {content: "", tool_calls: [{name: "tool_a", arguments: "{}"}]},
|
|
36
|
-
# {content: "Final answer"}
|
|
37
|
-
# ])
|
|
38
|
-
#
|
|
39
26
|
#--
|
|
40
27
|
#: (?responses: Array[Hash[Symbol, untyped]]) -> void
|
|
41
28
|
def initialize(responses: [])
|
|
@@ -46,16 +33,6 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
46
33
|
@stubbed_responses = []
|
|
47
34
|
end
|
|
48
35
|
|
|
49
|
-
# Stubs the next response; call repeatedly to queue several. +finish_reason+
|
|
50
|
-
# defaults to +:tool_calls+ when tool calls are present, else +:stop+.
|
|
51
|
-
#
|
|
52
|
-
# provider.stub_response("Hello")
|
|
53
|
-
# provider.stub_response("", tool_calls: [{name: "my_tool", arguments: '{"key":"value"}'}])
|
|
54
|
-
# provider.stub_response("Final response",
|
|
55
|
-
# token_usage: Riffer::Providers::TokenUsage.new(input_tokens: 10, output_tokens: 5))
|
|
56
|
-
# provider.stub_response("Truncated...", finish_reason: :length)
|
|
57
|
-
# provider.stub_response("Answer", reasoning: [{type: :text, text: "Thinking...", format: "mock-v1"}])
|
|
58
|
-
#
|
|
59
36
|
#--
|
|
60
37
|
#: (String, ?tool_calls: Array[Hash[Symbol, untyped]], ?token_usage: Riffer::Providers::TokenUsage?, ?finish_reason: Symbol?, ?reasoning: Array[Hash[Symbol, untyped] | Riffer::Messages::Assistant::ReasoningPart]) -> void
|
|
61
38
|
def stub_response(content, tool_calls: [], token_usage: nil, finish_reason: nil, reasoning: [])
|
|
@@ -68,8 +45,6 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
68
45
|
)
|
|
69
46
|
end
|
|
70
47
|
|
|
71
|
-
# Clears all stubbed responses.
|
|
72
|
-
#
|
|
73
48
|
#--
|
|
74
49
|
#: () -> void
|
|
75
50
|
def clear_stubs
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
# rbs_inline: enabled
|
|
3
3
|
|
|
4
|
-
# OpenAI provider for GPT models. Requires the +openai+ gem.
|
|
5
4
|
class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
6
5
|
WEB_SEARCH_TOOL_TYPE = "web_search_preview" #: String
|
|
7
6
|
|
|
8
|
-
# The Responses API has no finish_reason field
|
|
9
|
-
#
|
|
10
|
-
# with <tt>incomplete_details.reason</tt>, so that branch nests one level.
|
|
7
|
+
# The Responses API has no finish_reason field, and an +incomplete+ status is only meaningful
|
|
8
|
+
# together with <tt>incomplete_details.reason</tt>.
|
|
11
9
|
FINISH_REASONS = {
|
|
12
10
|
"completed" => :stop,
|
|
13
11
|
"incomplete" => {
|
|
@@ -20,7 +18,6 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
20
18
|
"queued" => :other,
|
|
21
19
|
}.freeze #: Hash[String, Symbol | Hash[String, Symbol]]
|
|
22
20
|
|
|
23
|
-
# The GenAI semconv well-known provider name.
|
|
24
21
|
#--
|
|
25
22
|
#: () -> String
|
|
26
23
|
def self.semconv_provider_name
|
|
@@ -48,12 +45,11 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
48
45
|
Riffer.config.openai.client
|
|
49
46
|
end
|
|
50
47
|
|
|
51
|
-
# Compacted so an unset value stays absent: the SDK reads +OPENAI_API_KEY+ /
|
|
52
|
-
# +OPENAI_BASE_URL+ only for a missing argument, and an explicit nil would
|
|
53
|
-
# suppress that fallback.
|
|
54
48
|
#--
|
|
55
49
|
#: () -> untyped
|
|
56
50
|
def build_client
|
|
51
|
+
# The SDK falls back to +OPENAI_API_KEY+ / +OPENAI_BASE_URL+ only for a missing argument; an
|
|
52
|
+
# explicit nil suppresses that fallback.
|
|
57
53
|
::OpenAI::Client.new(**{
|
|
58
54
|
api_key: Riffer.config.openai.api_key,
|
|
59
55
|
base_url: Riffer.config.openai.base_url,
|
|
@@ -80,10 +76,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
80
76
|
} #: Hash[Symbol, untyped]
|
|
81
77
|
|
|
82
78
|
unless tags.empty?
|
|
83
|
-
# Merged over any metadata set in model_options; a tag wins on a shared key.
|
|
84
79
|
params[:metadata] = (params[:metadata] || {}).merge(tags)
|
|
85
|
-
# The reserved user_id also maps to the native safety identifier while
|
|
86
|
-
# staying in metadata as an ordinary tag.
|
|
87
80
|
user_id = tags["user_id"]
|
|
88
81
|
params[:safety_identifier] = user_id if user_id
|
|
89
82
|
end
|
|
@@ -165,7 +158,6 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
165
158
|
Riffer::Providers::FinishReason.new(reason: reason, raw: detail || status)
|
|
166
159
|
end
|
|
167
160
|
|
|
168
|
-
# The nested field that names the cause behind an ambiguous status.
|
|
169
161
|
#--
|
|
170
162
|
#: (untyped, String) -> String?
|
|
171
163
|
def finish_detail(response, status)
|
|
@@ -181,16 +173,12 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
181
173
|
#: (untyped) -> String
|
|
182
174
|
def extract_content(response)
|
|
183
175
|
typed_response = response #: OpenAI::Models::Responses::Response
|
|
184
|
-
text_content = ""
|
|
185
176
|
|
|
186
|
-
typed_response.output.
|
|
187
|
-
next unless item.is_a?(::OpenAI::Models::Responses::ResponseOutputMessage)
|
|
188
|
-
|
|
189
|
-
text_block = item.content.find { |c| c.is_a?(::OpenAI::Models::Responses::ResponseOutputText) }
|
|
190
|
-
text_content = text_block.text if text_block.is_a?(::OpenAI::Models::Responses::ResponseOutputText)
|
|
191
|
-
end
|
|
177
|
+
typed_response.output.flat_map do |item|
|
|
178
|
+
next [] unless item.is_a?(::OpenAI::Models::Responses::ResponseOutputMessage)
|
|
192
179
|
|
|
193
|
-
|
|
180
|
+
item.content.filter_map { |c| c.text if c.is_a?(::OpenAI::Models::Responses::ResponseOutputText) }
|
|
181
|
+
end.join
|
|
194
182
|
end
|
|
195
183
|
|
|
196
184
|
#--
|
|
@@ -212,10 +200,47 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
212
200
|
tool_calls
|
|
213
201
|
end
|
|
214
202
|
|
|
203
|
+
#--
|
|
204
|
+
#: (untyped) -> Array[Riffer::Messages::Assistant::ReasoningPart]
|
|
205
|
+
def extract_reasoning(response)
|
|
206
|
+
typed_response = response #: OpenAI::Models::Responses::Response
|
|
207
|
+
typed_response.output.
|
|
208
|
+
grep(::OpenAI::Models::Responses::ResponseReasoningItem).
|
|
209
|
+
flat_map { |item| build_reasoning_parts(item) }
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# The trailing +:encrypted+ part is always present, even without a payload, so the item's id
|
|
213
|
+
# survives for replay: with +store+ on, OpenAI resolves the item from the id alone.
|
|
214
|
+
#--
|
|
215
|
+
#: (untyped) -> Array[Riffer::Messages::Assistant::ReasoningPart]
|
|
216
|
+
def build_reasoning_parts(item)
|
|
217
|
+
typed_item = item #: OpenAI::Models::Responses::ResponseReasoningItem
|
|
218
|
+
id = typed_item.id
|
|
219
|
+
summaries = typed_item.summary.map { |summary| build_reasoning_part(type: :summary, id: id, text: summary.text) }
|
|
220
|
+
texts = (typed_item.content || []).map { |content| build_reasoning_part(type: :text, id: id, text: content.text) }
|
|
221
|
+
|
|
222
|
+
[*summaries, *texts, build_reasoning_part(type: :encrypted, id: id, data: typed_item.encrypted_content)]
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
#--
|
|
226
|
+
#: (type: Symbol, id: String, ?text: String?, ?data: String?) -> Riffer::Messages::Assistant::ReasoningPart
|
|
227
|
+
def build_reasoning_part(type:, id:, text: nil, data: nil)
|
|
228
|
+
Riffer::Messages::Assistant::ReasoningPart.new(type: type, text: text, data: data, id: id, format: reasoning_format)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Distinct from OpenRouter's +openai-responses-v1+: reasoning relayed through OpenRouter is
|
|
232
|
+
# encrypted and stored under OpenRouter's organization, so neither side can replay the other's.
|
|
233
|
+
#--
|
|
234
|
+
#: () -> String
|
|
235
|
+
def reasoning_format
|
|
236
|
+
"openai-v1"
|
|
237
|
+
end
|
|
238
|
+
|
|
215
239
|
#--
|
|
216
240
|
#: (Hash[Symbol, untyped], Riffer::Providers::_EventSink) -> void
|
|
217
241
|
def execute_stream(params, yielder)
|
|
218
242
|
current_state = {
|
|
243
|
+
text: nil,
|
|
219
244
|
tool_info: {},
|
|
220
245
|
} #: Hash[Symbol, untyped]
|
|
221
246
|
|
|
@@ -231,12 +256,8 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
231
256
|
end
|
|
232
257
|
when :"response.output_text.delta"
|
|
233
258
|
handle_output_text_delta(event, state: current_state, yielder: yielder)
|
|
234
|
-
when :"response.output_text.done"
|
|
235
|
-
handle_output_text_done(event, state: current_state, yielder: yielder)
|
|
236
259
|
when :"response.reasoning_summary_text.delta"
|
|
237
260
|
handle_reasoning_summary_text_delta(event, state: current_state, yielder: yielder)
|
|
238
|
-
when :"response.reasoning_summary_text.done"
|
|
239
|
-
handle_reasoning_summary_text_done(event, state: current_state, yielder: yielder)
|
|
240
261
|
when :"response.function_call_arguments.delta"
|
|
241
262
|
handle_function_call_arguments_delta(event, state: current_state, yielder: yielder)
|
|
242
263
|
when :"response.function_call_arguments.done"
|
|
@@ -248,16 +269,18 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
248
269
|
when :"response.web_search_call.completed"
|
|
249
270
|
handle_web_search_status(event, status: "completed", yielder: yielder)
|
|
250
271
|
when :"response.output_item.done"
|
|
251
|
-
|
|
272
|
+
case event.item&.type
|
|
273
|
+
when :web_search_call then handle_output_item_done_web_search(event, yielder: yielder)
|
|
274
|
+
when :reasoning then handle_output_item_done_reasoning(event, yielder: yielder)
|
|
275
|
+
end
|
|
252
276
|
when :"response.completed", :"response.incomplete", :"response.failed"
|
|
253
277
|
stream_completed = true
|
|
254
278
|
handle_response_finished(event, state: current_state, yielder: yielder)
|
|
255
279
|
end
|
|
256
280
|
end
|
|
257
281
|
ensure
|
|
258
|
-
#
|
|
259
|
-
#
|
|
260
|
-
# socket leaks until GC. close is idempotent and a no-op after EOF.
|
|
282
|
+
# The SDK doesn't close the SSE socket when iteration is interrupted (raise / fiber
|
|
283
|
+
# cancellation), so it leaks until GC. close is idempotent and a no-op after EOF.
|
|
261
284
|
stream.close
|
|
262
285
|
end
|
|
263
286
|
|
|
@@ -279,27 +302,19 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
279
302
|
#--
|
|
280
303
|
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
281
304
|
def handle_output_text_delta(event, state:, yielder:)
|
|
305
|
+
# << avoids += copying the whole buffer per delta (O(n^2)); safe because
|
|
306
|
+
# nothing reads the buffer before the response finishes and TextDone takes it.
|
|
307
|
+
state[:text] ||= +""
|
|
308
|
+
state[:text] << event.delta
|
|
282
309
|
yielder << Riffer::StreamEvents::TextDelta.new(event.delta)
|
|
283
310
|
end
|
|
284
311
|
|
|
285
|
-
#--
|
|
286
|
-
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
287
|
-
def handle_output_text_done(event, state:, yielder:)
|
|
288
|
-
yielder << Riffer::StreamEvents::TextDone.new(event.text)
|
|
289
|
-
end
|
|
290
|
-
|
|
291
312
|
#--
|
|
292
313
|
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
293
314
|
def handle_reasoning_summary_text_delta(event, state:, yielder:)
|
|
294
315
|
yielder << Riffer::StreamEvents::ReasoningDelta.new(event.delta)
|
|
295
316
|
end
|
|
296
317
|
|
|
297
|
-
#--
|
|
298
|
-
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
299
|
-
def handle_reasoning_summary_text_done(event, state:, yielder:)
|
|
300
|
-
yield_reasoning_done(yielder, event.text)
|
|
301
|
-
end
|
|
302
|
-
|
|
303
318
|
#--
|
|
304
319
|
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
305
320
|
def handle_function_call_arguments_delta(event, state:, yielder:)
|
|
@@ -326,6 +341,8 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
326
341
|
#--
|
|
327
342
|
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
328
343
|
def handle_response_finished(event, state:, yielder:)
|
|
344
|
+
yielder << Riffer::StreamEvents::TextDone.new(state[:text]) if state[:text]
|
|
345
|
+
|
|
329
346
|
response = event.response
|
|
330
347
|
return unless response
|
|
331
348
|
|
|
@@ -349,8 +366,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
349
366
|
action = event.item.action
|
|
350
367
|
case action
|
|
351
368
|
when ::OpenAI::Models::Responses::ResponseFunctionWebSearch::Action::OpenPage
|
|
352
|
-
# OpenPage carries a url but no query or sources, so it doesn't fit
|
|
353
|
-
# WebSearchDone — emit as a status notification instead.
|
|
369
|
+
# OpenPage carries a url but no query or sources, so it doesn't fit WebSearchDone.
|
|
354
370
|
yielder << Riffer::StreamEvents::WebSearchStatus.new("open_page", url: action.url)
|
|
355
371
|
when ::OpenAI::Models::Responses::ResponseFunctionWebSearch::Action::Search
|
|
356
372
|
sources = (action.sources || []).map { |s| { title: nil, url: s.url } }
|
|
@@ -358,6 +374,12 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
358
374
|
end
|
|
359
375
|
end
|
|
360
376
|
|
|
377
|
+
#--
|
|
378
|
+
#: (untyped, yielder: Riffer::Providers::_EventSink) -> void
|
|
379
|
+
def handle_output_item_done_reasoning(event, yielder:)
|
|
380
|
+
build_reasoning_parts(event.item).each { |part| yielder << Riffer::StreamEvents::ReasoningDone.new(part) }
|
|
381
|
+
end
|
|
382
|
+
|
|
361
383
|
#--
|
|
362
384
|
#: (Array[Riffer::Messages::Base]) -> Array[Hash[Symbol, untyped]]
|
|
363
385
|
def convert_messages_to_openai_format(messages)
|
|
@@ -388,12 +410,13 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
388
410
|
end
|
|
389
411
|
|
|
390
412
|
#--
|
|
391
|
-
#: (Riffer::Messages::Assistant) ->
|
|
413
|
+
#: (Riffer::Messages::Assistant) -> Array[Hash[Symbol, untyped]]
|
|
392
414
|
def convert_assistant_to_openai_format(message)
|
|
415
|
+
items = convert_reasoning_to_openai_format(message.reasoning)
|
|
416
|
+
|
|
393
417
|
if message.tool_calls.empty?
|
|
394
|
-
{ role: "assistant", content: message.content }
|
|
418
|
+
items << { role: "assistant", content: message.content }
|
|
395
419
|
else
|
|
396
|
-
items = [] #: Array[Hash[Symbol, untyped]]
|
|
397
420
|
if message.content && !message.content.empty?
|
|
398
421
|
items << { type: "message", role: "assistant",
|
|
399
422
|
content: message.content, }
|
|
@@ -406,8 +429,33 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
406
429
|
arguments: tc.arguments,
|
|
407
430
|
}
|
|
408
431
|
end
|
|
409
|
-
items
|
|
410
432
|
end
|
|
433
|
+
|
|
434
|
+
items
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
#--
|
|
438
|
+
#: (Array[Riffer::Messages::Assistant::ReasoningPart]) -> Array[Hash[Symbol, untyped]]
|
|
439
|
+
def convert_reasoning_to_openai_format(reasoning)
|
|
440
|
+
reasoning.
|
|
441
|
+
select { |part| part.format == reasoning_format }.
|
|
442
|
+
chunk_while { |a, b| a.id == b.id }.
|
|
443
|
+
map { |parts| convert_reasoning_item_to_openai_format(parts) }
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
#--
|
|
447
|
+
#: (Array[Riffer::Messages::Assistant::ReasoningPart]) -> Hash[Symbol, untyped]
|
|
448
|
+
def convert_reasoning_item_to_openai_format(parts)
|
|
449
|
+
summary = parts.filter_map { |part| { type: "summary_text", text: part.text } if part.type == :summary }
|
|
450
|
+
content = parts.filter_map { |part| { type: "reasoning_text", text: part.text } if part.type == :text }
|
|
451
|
+
|
|
452
|
+
{
|
|
453
|
+
type: "reasoning",
|
|
454
|
+
id: parts.first&.id,
|
|
455
|
+
summary: summary,
|
|
456
|
+
content: content.empty? ? nil : content,
|
|
457
|
+
encrypted_content: parts.find { |part| part.type == :encrypted }&.data,
|
|
458
|
+
}.compact
|
|
411
459
|
end
|
|
412
460
|
|
|
413
461
|
#--
|