Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.

Commit 8c5ee46

Browse files
committed
FIX: normalize keys in structured output
Previously we did not validate the hash passed in to structured outputs which could either be string based or symbol base Specifically this broke structured outputs for Gemini in some specific cases.
1 parent 73768ce commit 8c5ee46

File tree

2 files changed

+25
-1
lines changed

2 files changed

+25
-1
lines changed

lib/completions/llm.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def presets
8080
tokens: 800_000,
8181
endpoint:
8282
"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash",
83-
display_name: "Gemini 2.5 Pro",
83+
display_name: "Gemini 2.5 Flash",
8484
input_cost: 0.30,
8585
output_cost: 2.50,
8686
},
@@ -379,6 +379,12 @@ def generate(
379379

380380
model_params[:temperature] = temperature if temperature
381381
model_params[:top_p] = top_p if top_p
382+
383+
# internals expect symbolized keys, so we normalize here
384+
response_format =
385+
JSON.parse(response_format.to_json, symbolize_names: true) if response_format &&
386+
response_format.is_a?(Hash)
387+
382388
model_params[:response_format] = response_format if response_format
383389
model_params.merge!(extra_model_params) if extra_model_params
384390

spec/lib/completions/endpoints/gemini_spec.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,7 @@ def tool_response
612612
).to_return(status: 200, body: response)
613613

614614
structured_response = nil
615+
615616
llm.generate("Hello", response_format: schema, user: user) do |partial|
616617
structured_response = partial
617618
end
@@ -626,6 +627,23 @@ def tool_response
626627
schema.dig(:json_schema, :schema).except(:additionalProperties),
627628
)
628629
expect(parsed.dig(:generationConfig, :responseMimeType)).to eq("application/json")
630+
631+
structured_response = nil
632+
# once more but this time lets have the schema as string keys
633+
llm.generate("Hello", response_format: schema.as_json, user: user) do |partial|
634+
structured_response = partial
635+
end
636+
637+
expect(structured_response.read_buffered_property(:key)).to eq("Hello!\n there")
638+
expect(structured_response.read_buffered_property(:num)).to eq(42)
639+
640+
parsed = JSON.parse(req_body, symbolize_names: true)
641+
642+
# Verify that schema is passed following Gemini API specs.
643+
expect(parsed.dig(:generationConfig, :responseSchema)).to eq(
644+
schema.dig(:json_schema, :schema).except(:additionalProperties),
645+
)
646+
expect(parsed.dig(:generationConfig, :responseMimeType)).to eq("application/json")
629647
end
630648
end
631649

0 commit comments

Comments
 (0)