ruby-spacy 0.4.0 → 0.5.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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +58 -0
  3. data/README.md +133 -4
  4. data/Rakefile +13 -0
  5. data/examples/get_started/lexeme.rb +0 -0
  6. data/examples/get_started/linguistic_annotations.rb +0 -0
  7. data/examples/get_started/morphology.rb +0 -0
  8. data/examples/get_started/most_similar.rb +0 -0
  9. data/examples/get_started/named_entities.rb +0 -0
  10. data/examples/get_started/outputs/test_dep.svg +0 -0
  11. data/examples/get_started/outputs/test_dep_compact.svg +0 -0
  12. data/examples/get_started/outputs/test_ent.html +0 -0
  13. data/examples/get_started/pos_tags_and_dependencies.rb +0 -0
  14. data/examples/get_started/similarity.rb +0 -0
  15. data/examples/get_started/tokenization.rb +0 -0
  16. data/examples/get_started/visualizing_dependencies.rb +0 -0
  17. data/examples/get_started/visualizing_dependencies_compact.rb +0 -0
  18. data/examples/get_started/visualizing_named_entities.rb +0 -0
  19. data/examples/get_started/vocab.rb +0 -0
  20. data/examples/get_started/word_vectors.rb +0 -0
  21. data/examples/japanese/ancestors.rb +0 -0
  22. data/examples/japanese/entity_annotations_and_labels.rb +0 -0
  23. data/examples/japanese/information_extraction.rb +0 -0
  24. data/examples/japanese/lemmatization.rb +0 -0
  25. data/examples/japanese/most_similar.rb +0 -0
  26. data/examples/japanese/named_entity_recognition.rb +0 -0
  27. data/examples/japanese/navigating_parse_tree.rb +0 -0
  28. data/examples/japanese/noun_chunks.rb +0 -0
  29. data/examples/japanese/outputs/test_dep.svg +0 -0
  30. data/examples/japanese/outputs/test_ent.html +0 -0
  31. data/examples/japanese/pos_tagging.rb +0 -0
  32. data/examples/japanese/sentence_segmentation.rb +0 -0
  33. data/examples/japanese/similarity.rb +0 -0
  34. data/examples/japanese/tokenization.rb +0 -0
  35. data/examples/japanese/visualizing_dependencies.rb +0 -0
  36. data/examples/japanese/visualizing_named_entities.rb +0 -0
  37. data/examples/linguistic_features/ancestors.rb +0 -0
  38. data/examples/linguistic_features/entity_annotations_and_labels.rb +0 -0
  39. data/examples/linguistic_features/finding_a_verb_with_a_subject.rb +0 -0
  40. data/examples/linguistic_features/information_extraction.rb +0 -0
  41. data/examples/linguistic_features/iterating_children.rb +0 -0
  42. data/examples/linguistic_features/iterating_lefts_and_rights.rb +0 -0
  43. data/examples/linguistic_features/lemmatization.rb +0 -0
  44. data/examples/linguistic_features/named_entity_recognition.rb +0 -0
  45. data/examples/linguistic_features/navigating_parse_tree.rb +0 -0
  46. data/examples/linguistic_features/noun_chunks.rb +0 -0
  47. data/examples/linguistic_features/outputs/test_ent.html +0 -0
  48. data/examples/linguistic_features/pos_tagging.rb +0 -0
  49. data/examples/linguistic_features/retokenize_1.rb +0 -0
  50. data/examples/linguistic_features/retokenize_2.rb +0 -0
  51. data/examples/linguistic_features/rule_based_morphology.rb +0 -0
  52. data/examples/linguistic_features/sentence_segmentation.rb +0 -0
  53. data/examples/linguistic_features/similarity.rb +0 -0
  54. data/examples/linguistic_features/similarity_between_lexemes.rb +0 -0
  55. data/examples/linguistic_features/similarity_between_spans.rb +0 -0
  56. data/examples/linguistic_features/tokenization.rb +0 -0
  57. data/examples/llm/anthropic_chat.rb +21 -0
  58. data/examples/llm/local_llm_ollama.rb +27 -0
  59. data/examples/llm/structured_ner_comparison.rb +49 -0
  60. data/examples/openai_integration/openai_embeddings.rb +0 -0
  61. data/examples/rule_based_matching/creating_spans_from_matches.rb +0 -0
  62. data/examples/rule_based_matching/matcher.rb +0 -0
  63. data/lib/ruby-spacy/anthropic_client.rb +66 -0
  64. data/lib/ruby-spacy/anthropic_helper.rb +118 -0
  65. data/lib/ruby-spacy/llm_client_base.rb +120 -0
  66. data/lib/ruby-spacy/openai_client.rb +37 -107
  67. data/lib/ruby-spacy/openai_helper.rb +60 -16
  68. data/lib/ruby-spacy/version.rb +1 -1
  69. data/lib/ruby-spacy.rb +62 -14
  70. metadata +7 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6185c586feb32fa51efcd4349398cd4ca9541280a5cc8a1b6a73eb93a987d4ac
4
- data.tar.gz: a146a9c40e2d5293e2401cb16b8ac6866cbb577e11a10d9657c406f933e7a3aa
3
+ metadata.gz: b759c507a1cad8e726bd36ad39e4539894395407e2ac5d64a31b7b907a4e34d8
4
+ data.tar.gz: 0f02f9b8f6bcf39a49b50b1934905e72c0c13bb9652eee8fbaad3a3b7b58b7e7
5
5
  SHA512:
6
- metadata.gz: bf558d4e9a7a6765fd7d088bbf8324a6ee0e4f4186962551d71e5a991e0aefd1e51a186f19c2824fabcc6afd0c83960771f082237febece52c2a522ccb39a5cf
7
- data.tar.gz: 3a64559cf8c169d1ac1ecdef526d26e5776989b9cc203a8ed30e0dd5d87ff62a4d1b741aff30c8cb49e5ffb716c6068f9af3a12d50d0d4de8ad6f22ebe80ea0d
6
+ metadata.gz: 5b7d8078148e05c860f3ef698b5103cfca533fabfac5001c5afd12419efaae21d6492aa22fcebfba9d9d22bf8701fe2c6a21eb1ee879344cdbd5d897af9b8853
7
+ data.tar.gz: 4ffab4be8e0338e86c34aad0285cd9471a3d5c570fbf48b4214cdbe5ca5cc01c7d251d4f6d5edc778de2185d89fc3858e094c363499c81f89d5c3a61ab8859c2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,63 @@
1
1
  # Change Log
2
2
 
3
+ ## 0.5.0 - 2026-07-21
4
+ ### Added
5
+ - `Language#with_llm(provider:)` — provider-neutral block-based LLM API
6
+ supporting `:openai`, `:anthropic` (Claude), and `:ollama` (local models)
7
+ - `AnthropicHelper` / `AnthropicClient` — Anthropic Messages API support
8
+ using only net/http (default model: `claude-sonnet-5`; requires
9
+ `ANTHROPIC_API_KEY`)
10
+ - `base_url:` option for OpenAI client/helper — works with any
11
+ OpenAI-compatible server (Ollama, LM Studio, llama.cpp server, vLLM,
12
+ OpenRouter, etc.)
13
+ - `schema:` option for `chat` — Structured Outputs on both providers;
14
+ returns a validated, parsed Ruby Hash
15
+ - New examples under `examples/llm/` (Anthropic, local Ollama, and a
16
+ spaCy-vs-LLM NER comparison with structured outputs)
17
+
18
+ ### Changed
19
+ - `temperature` is now sent only when explicitly specified; if a model
20
+ rejects it, the request is retried once without it. The model-name
21
+ heuristic (`temperature_unsupported?`) has been removed, so new models
22
+ work without gem updates. Note: previously a default of 0.7 was sent to
23
+ models that supported it (also from `Doc#openai_query` /
24
+ `Doc#openai_completion`); now the API default applies unless you pass
25
+ `temperature:` yourself
26
+ - Shared HTTP layer (`LLMClientBase`) extracted from `OpenAIClient`;
27
+ no behavior change for existing OpenAI usage
28
+ - Truncated responses (`finish_reason: length` / `stop_reason: max_tokens`)
29
+ now emit a warning; with `schema:`, unparseable (e.g. truncated) JSON
30
+ returns nil with a warning instead of raising `JSON::ParserError`
31
+ - LLM error messages are printed to stderr (`Kernel#warn`) instead of
32
+ stdout, keeping stdout clean for program output
33
+ - Default model names are now public constants:
34
+ `OpenAIClient::DEFAULT_MODEL`, `OpenAIClient::DEFAULT_EMBEDDINGS_MODEL`,
35
+ `AnthropicClient::DEFAULT_MODEL`
36
+
37
+ ## 0.4.1 - 2026-07-19
38
+ ### Fixed
39
+ - Gem packaging: normalize file permissions at build time so packaged files
40
+ are world-readable (0644 / 0755). Version 0.4.0 shipped 11 owner-only
41
+ (0600) files, which made the gem unusable after `sudo gem install`
42
+
43
+ ## 0.4.0 - 2026-02-23
44
+ ### Added
45
+ - `Language#with_openai` block API with `OpenAIHelper` for streamlined OpenAI integration
46
+ - `Doc#linguistic_summary` for JSON-formatted spaCy analysis output
47
+ - `Token#idx`, `Span#text`/`Span#to_s`, `Language#memory_zone` (spaCy 3.8+)
48
+
49
+ ### Fixed
50
+ - `Doc#ents` returns proper `Span` objects instead of raw Python objects
51
+
52
+ ### Changed
53
+ - Model name validation in `Language#initialize` for security
54
+ - OpenAI client: temperature support for o-series models, 429 retry with
55
+ exponential backoff, client reuse, `dimensions`/`response_format` parameters,
56
+ tool call depth limit
57
+ - Improved `respond_to_missing?` across all wrapper classes
58
+ - Added `instance_variables_to_inspect` for Ruby 4.0+ compatibility
59
+ - Added `base64` gem dependency (required for Ruby 3.4+)
60
+
3
61
  ## 0.3.0 - 2025-01-06
4
62
  ### Added
5
63
  - Ruby 4.0 support
data/README.md CHANGED
@@ -11,13 +11,15 @@
11
11
  | ✅ | Named entity recognition |
12
12
  | ✅ | Syntactic dependency visualization |
13
13
  | ✅ | Access to pre-trained word vectors |
14
- | ✅ | OpenAI Chat/Completion/Embeddings API integration |
14
+ | ✅ | LLM integration: OpenAI, Anthropic (Claude), and local models |
15
15
 
16
- Current Version: `0.3.0`
16
+ Current Version: `0.5.0`
17
17
 
18
18
  - Ruby 4.0 supported
19
19
  - spaCy 3.8 supported
20
- - OpenAI GPT-5 API integration
20
+ - Multi-provider LLM API: OpenAI, Anthropic (Claude), and local models via Ollama or any OpenAI-compatible server
21
+ - Structured outputs (JSON Schema) support
22
+ - Block-based LLM API with linguistic analysis
21
23
 
22
24
  ## Installation of Prerequisites
23
25
 
@@ -587,7 +589,9 @@ end
587
589
 
588
590
  ## OpenAI API Integration
589
591
 
590
- > ⚠️ This feature requires GPT-5 series models. Please refer to OpenAI's [API reference](https://platform.openai.com/docs/api-reference) for details. Note: GPT-5 models do not support the `temperature` parameter.
592
+ > ⚠️ This feature requires GPT-5 series models. Please refer to OpenAI's [API reference](https://platform.openai.com/docs/api-reference) for details.
593
+
594
+ > ℹ️ The `temperature` parameter is sent to the API only when you specify it explicitly. If the model does not support it (e.g., GPT-5 series and o-series models), the request is automatically retried once without it — no per-model configuration is needed.
591
595
 
592
596
  Easily leverage GPT models within ruby-spacy by using an OpenAI API key. When constructing prompts for the `Doc::openai_query` method, you can incorporate the following token properties of the document. These properties are retrieved through tool calls (made internally by GPT when necessary) and seamlessly integrated into your prompt. The available properties include:
593
597
 
@@ -843,6 +847,131 @@ Output:
843
847
  ...
844
848
  ```
845
849
 
850
+ ### Block-based OpenAI API
851
+
852
+ The `Language#with_openai` block API provides a streamlined way to combine spaCy's linguistic analysis with OpenAI. The `Doc#linguistic_summary` method generates a JSON summary of spaCy's analysis (tokens, entities, noun chunks, etc.) that can be passed directly to the LLM as context.
853
+
854
+ **Basic usage:**
855
+
856
+ ```ruby
857
+ require "ruby-spacy"
858
+
859
+ nlp = Spacy::Language.new("en_core_web_sm")
860
+ doc = nlp.read("Apple Inc. was founded by Steve Jobs in California.")
861
+
862
+ nlp.with_openai(model: "gpt-5-mini") do |ai|
863
+ result = ai.chat(
864
+ system: "You are a linguistic analyst. Analyze the given linguistic data.",
865
+ user: doc.linguistic_summary
866
+ )
867
+ puts result
868
+ end
869
+ ```
870
+
871
+ **Batch processing with `pipe`:**
872
+
873
+ ```ruby
874
+ require "ruby-spacy"
875
+
876
+ nlp = Spacy::Language.new("en_core_web_sm")
877
+ texts = ["The bank approved the loan.", "I sat on the river bank."]
878
+
879
+ nlp.with_openai(model: "gpt-5-mini") do |ai|
880
+ nlp.pipe(texts).each do |doc|
881
+ result = ai.chat(
882
+ system: "Identify the meaning of 'bank' in one word based on the linguistic context.",
883
+ user: doc.linguistic_summary
884
+ )
885
+ puts "#{doc.text} => #{result}"
886
+ end
887
+ end
888
+ ```
889
+
890
+ **Customizing linguistic summary:**
891
+
892
+ ```ruby
893
+ # Include sentences and morphology, exclude noun chunks
894
+ summary = doc.linguistic_summary(
895
+ sections: [:text, :tokens, :entities, :sentences],
896
+ token_attributes: [:text, :lemma, :pos, :dep, :head, :morphology]
897
+ )
898
+ ```
899
+
900
+ **Embeddings:**
901
+
902
+ ```ruby
903
+ nlp.with_openai do |ai|
904
+ vector = ai.embeddings("Hello world")
905
+ puts vector.length # => 1536
906
+ end
907
+ ```
908
+
909
+ ### Multi-provider LLM API
910
+
911
+ The `Language#with_llm` block API generalizes `with_openai` to multiple providers. The helper yielded to the block has the same `chat` interface for every provider.
912
+
913
+ **Anthropic (Claude):**
914
+
915
+ ```ruby
916
+ # Requires the ANTHROPIC_API_KEY environment variable
917
+ # Default model: claude-sonnet-5 (override with model: "...")
918
+ nlp.with_llm(provider: :anthropic) do |ai|
919
+ result = ai.chat(
920
+ system: "You are a linguistic analyst.",
921
+ user: doc.linguistic_summary
922
+ )
923
+ puts result
924
+ end
925
+ ```
926
+
927
+ **Local models via Ollama (no API key needed):**
928
+
929
+ ```ruby
930
+ # Requires a running Ollama server: https://ollama.com
931
+ nlp.with_llm(provider: :ollama, model: "llama3.2") do |ai|
932
+ puts ai.chat(user: doc.linguistic_summary)
933
+ end
934
+ ```
935
+
936
+ Any other OpenAI-compatible server (LM Studio, llama.cpp server, vLLM, OpenRouter, etc.) works via `base_url:`:
937
+
938
+ ```ruby
939
+ nlp.with_llm(provider: :openai, base_url: "http://localhost:1234/v1",
940
+ access_token: "not-needed", model: "your-model") do |ai|
941
+ puts ai.chat(user: "Hello!")
942
+ end
943
+ ```
944
+
945
+ **Structured outputs (`schema:`):** pass a JSON Schema and receive a validated, parsed Ruby Hash — useful for comparing LLM output with spaCy's analysis programmatically. Works with both `:openai` and `:anthropic`. Objects in the schema must set `additionalProperties: false`.
946
+
947
+ ```ruby
948
+ schema = {
949
+ type: "object",
950
+ properties: {
951
+ entities: {
952
+ type: "array",
953
+ items: {
954
+ type: "object",
955
+ properties: { text: { type: "string" }, label: { type: "string" } },
956
+ required: %w[text label],
957
+ additionalProperties: false
958
+ }
959
+ }
960
+ },
961
+ required: ["entities"],
962
+ additionalProperties: false
963
+ }
964
+
965
+ result = nlp.with_llm(provider: :openai) do |ai|
966
+ ai.chat(system: "Extract named entities.", user: doc.text, schema: schema)
967
+ end
968
+ result["entities"].each { |ent| puts "#{ent["text"]} (#{ent["label"]})" }
969
+ ```
970
+
971
+ **Note on `temperature`:** for all providers, `temperature` is omitted from requests unless you pass it explicitly (`ai.chat(user: "...", temperature: 0.3)`). Models that reject the parameter (e.g., GPT-5 series, o-series, and current Claude models) are automatically retried once without it, so any model works without per-model configuration.
972
+
973
+ See `examples/llm/` for complete scripts, including a spaCy-vs-LLM NER comparison.
974
+
846
975
  ## Advanced Usage
847
976
 
848
977
  ### Setting a Timeout
data/Rakefile CHANGED
@@ -10,3 +10,16 @@ Rake::TestTask.new(:test) do |t|
10
10
  end
11
11
 
12
12
  task default: :test
13
+
14
+ # Gem packaging preserves on-disk file modes; owner-only permissions here
15
+ # produce gems whose files are unreadable after a sudo install. Executable
16
+ # status is determined by path alone because local (Dropbox-synced) modes
17
+ # are unreliable.
18
+ task :normalize_permissions do
19
+ `git ls-files -z`.split("\x0").each do |f|
20
+ executable = f.start_with?("bin/", "exe/")
21
+ File.chmod(executable ? 0o755 : 0o644, f)
22
+ end
23
+ end
24
+
25
+ Rake::Task["build"].enhance([:normalize_permissions])
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # add path to ruby-spacy lib to load path
4
+ $LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
5
+
6
+ require "ruby-spacy"
7
+
8
+ # Requires the ANTHROPIC_API_KEY environment variable
9
+ # Default model: claude-sonnet-5 (override with model: "...")
10
+
11
+ nlp = Spacy::Language.new("en_core_web_sm")
12
+ doc = nlp.read("The cat sat on the mat while the dog slept under the table.")
13
+
14
+ result = nlp.with_llm(provider: :anthropic) do |ai|
15
+ ai.chat(
16
+ system: "You are a linguistics tutor. Explain the syntactic structure briefly.",
17
+ user: doc.linguistic_summary
18
+ )
19
+ end
20
+
21
+ puts result
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # add path to ruby-spacy lib to load path
4
+ $LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
5
+
6
+ require "ruby-spacy"
7
+
8
+ # Requires a running Ollama server (https://ollama.com):
9
+ # ollama pull llama3.2
10
+ # ollama serve
11
+ # No API key is needed.
12
+
13
+ nlp = Spacy::Language.new("en_core_web_sm")
14
+ doc = nlp.read("I sat on the bank of the river.")
15
+
16
+ result = nlp.with_llm(provider: :ollama, model: "llama3.2") do |ai|
17
+ ai.chat(
18
+ system: "Identify the meaning of the word 'bank' in one short sentence.",
19
+ user: doc.linguistic_summary
20
+ )
21
+ end
22
+
23
+ puts result
24
+
25
+ # Any OpenAI-compatible server works the same way via base_url, e.g.:
26
+ # nlp.with_llm(provider: :openai, base_url: "http://localhost:1234/v1",
27
+ # access_token: "not-needed", model: "your-model") { |ai| ... }
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ # add path to ruby-spacy lib to load path
4
+ $LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
5
+
6
+ require "ruby-spacy"
7
+
8
+ # Compares spaCy's named entity recognition with an LLM's, using structured
9
+ # outputs so the LLM's answer comes back as a validated Ruby Hash.
10
+ # Requires the OPENAI_API_KEY environment variable
11
+ # (or swap in provider: :anthropic with ANTHROPIC_API_KEY).
12
+
13
+ nlp = Spacy::Language.new("en_core_web_sm")
14
+ text = "Mr. Best flew to New York on Saturday morning to meet Tim Cook of Apple."
15
+ doc = nlp.read(text)
16
+
17
+ schema = {
18
+ type: "object",
19
+ properties: {
20
+ entities: {
21
+ type: "array",
22
+ items: {
23
+ type: "object",
24
+ properties: {
25
+ text: { type: "string" },
26
+ label: { type: "string", enum: %w[PERSON ORG GPE DATE TIME OTHER] }
27
+ },
28
+ required: %w[text label],
29
+ additionalProperties: false
30
+ }
31
+ }
32
+ },
33
+ required: ["entities"],
34
+ additionalProperties: false
35
+ }
36
+
37
+ llm_result = nlp.with_llm(provider: :openai) do |ai|
38
+ ai.chat(
39
+ system: "Extract all named entities from the user's text.",
40
+ user: text,
41
+ schema: schema
42
+ )
43
+ end
44
+
45
+ puts "spaCy entities:"
46
+ doc.ents.each { |ent| puts " #{ent.text} (#{ent.label})" }
47
+
48
+ puts "\nLLM entities:"
49
+ llm_result["entities"].each { |ent| puts " #{ent["text"]} (#{ent["label"]})" }
File without changes
File without changes
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "llm_client_base"
4
+
5
+ module Spacy
6
+ # A lightweight Anthropic Messages API client without external dependencies.
7
+ class AnthropicClient < LLMClientBase
8
+ # Default Anthropic API endpoint
9
+ API_ENDPOINT = "https://api.anthropic.com/v1"
10
+ # Messages API version header value
11
+ ANTHROPIC_VERSION = "2023-06-01"
12
+ # Default model for chat requests
13
+ DEFAULT_MODEL = "claude-sonnet-5"
14
+ # (see LLMClientBase::APIError)
15
+ APIError = LLMClientBase::APIError
16
+
17
+ # @param access_token [String] Anthropic API key
18
+ # @param timeout [Integer] request timeout in seconds
19
+ # @param base_url [String] API endpoint override
20
+ def initialize(access_token:, timeout: DEFAULT_TIMEOUT, base_url: API_ENDPOINT)
21
+ super(base_url: base_url, timeout: timeout)
22
+ @access_token = access_token
23
+ end
24
+
25
+ # Sends a Messages API request.
26
+ #
27
+ # The +temperature+ parameter is sent only when explicitly given. Current
28
+ # Claude models (Opus 4.7+) reject it; the request is then retried once
29
+ # without it.
30
+ #
31
+ # @param model [String] The model to use (e.g., "claude-sonnet-5")
32
+ # @param messages [Array<Hash>] The conversation messages
33
+ # @param system [String, nil] System prompt (top-level parameter)
34
+ # @param max_tokens [Integer] Maximum tokens in the response (required by the API)
35
+ # @param temperature [Float, nil] Sampling temperature (omitted when nil)
36
+ # @param output_config [Hash, nil] Output configuration, e.g.
37
+ # { format: { type: "json_schema", schema: {...} } } for structured outputs
38
+ # @return [Hash] The API response
39
+ def messages(model:, messages:, system: nil, max_tokens: 1000, temperature: nil, output_config: nil)
40
+ body = {
41
+ model: model,
42
+ max_tokens: max_tokens,
43
+ messages: messages
44
+ }
45
+ body[:system] = system if system
46
+ body[:temperature] = temperature unless temperature.nil?
47
+ body[:output_config] = output_config if output_config
48
+
49
+ post("/messages", body)
50
+ rescue APIError => e
51
+ raise unless body.key?(:temperature) && e.status_code == 400 && e.message.match?(/temperature/i)
52
+
53
+ warn "Warning: model does not support the temperature parameter; retrying without it"
54
+ post("/messages", body.reject { |key, _| key == :temperature })
55
+ end
56
+
57
+ private
58
+
59
+ def request_headers
60
+ {
61
+ "x-api-key" => @access_token,
62
+ "anthropic-version" => ANTHROPIC_VERSION
63
+ }
64
+ end
65
+ end
66
+ end