llm_specs 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 145115f78fbeb2a228a05c2c4752a80576ac693b972cc580f267c56e3d90f724
4
+ data.tar.gz: 45828acd7744da728d1ebcd017b918af9064f8c531e5e4f6a708e1dd3851b44d
5
+ SHA512:
6
+ metadata.gz: 2780eb5101b22334cafeeb7c7de12be3ec120d5daa8e0c4f8e2d9930c78c0f6353744c4f35562891d09b28bcc1fe25602cecf4b4b924b9baaf45dc6b4df12d41
7
+ data.tar.gz: 63ce73155eee23924dfbd0e43f62b66fbb02a224ae861923d17ece9e994fba7951653ffc593f04bd33851c688271cf1b5dd2ab33c20518a0f9c25fcff8c29eeb
data/.tldr.yml ADDED
@@ -0,0 +1,2 @@
1
+ emoji: true
2
+ verbose: true
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Max Power
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # LLMSpecs
2
+
3
+ LLMSpecs is a lightweight Ruby interface for fetching and caching
4
+ large language model (LLM) specifications from the [Parsera API](https://llmspecs.parsera.org).
5
+ It provides a simple, efficient way to access model metadata with built-in caching and query support.
6
+
7
+ ## Features
8
+
9
+ - Fetches LLM specs from: https://api.parsera.org/v1/llm-specs
10
+ - Caches results locally to models.json
11
+ - Provides a queryable catalog of models via .models
12
+ - Supports filtering and lookup by model attributes
13
+
14
+ ## Model Capabilities
15
+ Each model is represented by an instance of `LLMSpecs::Model`, a value object that encapsulates:
16
+
17
+ - `id`, `name`, `provider`, `family`
18
+ - Token limits (`context_window`, `max_output_tokens`)
19
+ - Supported modalities (e.g. `text`, `image`)
20
+ - Capabilities such as:
21
+ - `function_calling?`
22
+ - `structured_output?`
23
+ - `streaming?`
24
+ - `reasoning?`
25
+ - `citations?`
26
+ - `batch?`
27
+
28
+ - Pricing breakdowns via `input_pricing` and `output_pricing`
29
+
30
+ You can easily check capabilities or pricing:
31
+ ```ruby
32
+ model.function_calling? # => true/false
33
+ model.input_pricing # => $ per 1M input tokens (default)
34
+ model.output_pricing(:image_tokens)
35
+ ```
36
+
37
+ ## Example Usage
38
+ ```ruby
39
+ require 'llm_specs'
40
+
41
+ models = LLMSpecs.models
42
+ ```
43
+
44
+ ### Find a model by ID
45
+ ```ruby
46
+ model = LLMSpecs.models.find("claude-3-sonnet")
47
+ ```
48
+
49
+ ### Filter models
50
+ ```ruby
51
+ anthropic_models = LLMSpecs.models.where(provider: "anthropic")
52
+
53
+ claude_models = LLMSpecs.models.where(provider: "anthropic", family: "claude-3-7-sonnet")
54
+ ```
55
+
56
+ ### List all models that support streaming
57
+ ```ruby
58
+ streaming_models = LLMSpecs.models.select(&:streaming?)
59
+ ```
60
+
61
+ ## License
62
+
63
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "tldr/rake"
5
+
6
+ task default: :tldr
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+ module LLMSpecs
3
+ class Cache
4
+ def initialize(file, ttl: 86400)
5
+ @file = file
6
+ @ttl = ttl
7
+ end
8
+
9
+ def fetch
10
+ return read if valid?
11
+ data = yield
12
+ write(data)
13
+ data
14
+ end
15
+
16
+ def valid?
17
+ File.exist?(@file) && (Time.now - File.mtime(@file) < @ttl)
18
+ end
19
+
20
+ def read
21
+ JSON.parse(File.read(@file), symbolize_names: true)
22
+ end
23
+
24
+ def write(data)
25
+ File.write(@file, JSON.pretty_generate(data))
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+ module LLMSpecs
3
+ class Catalog
4
+ def initialize(api_uri:, cache_path:)
5
+ @uri = URI(api_uri)
6
+ @cache = Cache.new(cache_path)
7
+ end
8
+
9
+ def models
10
+ Collection.new fetch.map(&Model)
11
+ end
12
+
13
+ private
14
+
15
+ def fetch
16
+ @cache.fetch do
17
+ response = Net::HTTP.get_response(@uri)
18
+ raise FetchError.new(response) unless response.is_a? Net::HTTPSuccess
19
+ JSON.parse(response.body, symbolize_names: true)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+ module LLMSpecs
3
+ class Collection < Array
4
+ def find(id)
5
+ super() { it.id == id } || raise(ModelNotFound.new(id))
6
+ end
7
+
8
+ def where(**criteria)
9
+ select do |model|
10
+ criteria.all? { |k,v| model.send(k) == v }
11
+ end
12
+ end
13
+
14
+ %i[select reject filter].each do |method|
15
+ define_method(method) do |&block|
16
+ self.class.new(super(&block))
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ module LLMSpecs
3
+ class ModelNotFound < StandardError
4
+ def initialize(id)
5
+ super("Couldn't find model with id='#{id}'")
6
+ end
7
+ end
8
+
9
+ class FetchError < StandardError
10
+ attr_reader :response
11
+ def initialize(response)
12
+ @response = response
13
+ super("HTTP fetch failed (status: #{response.status})")
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+ module LLMSpecs
3
+ class Model < Data.define(:id, :name, :provider, :family, :context_window, :max_output_tokens, :modalities, :capabilities, :pricing)
4
+ def supports?(capability)
5
+ capabilities.include?(capability.to_s)
6
+ end
7
+
8
+ %i[function_calling structured_output batch reasoning citations streaming].each do |capability|
9
+ define_method(:"#{capability}?") { supports?(capability) }
10
+ end
11
+
12
+ def input_modalities
13
+ modalities[:input]
14
+ end
15
+
16
+ def output_modalities
17
+ modalities[:output]
18
+ end
19
+
20
+ def input_pricing(type=:text_tokens, price_type=:standard)
21
+ pricing.dig(type, price_type, :input_per_million)
22
+ end
23
+
24
+ def output_pricing(type=:text_tokens, price_type=:standard)
25
+ pricing.dig(type, price_type, :output_per_million)
26
+ end
27
+
28
+ def self.to_proc
29
+ ->(hash) { new(**hash) }
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ module LLMSpecs
3
+ VERSION = "0.1.0"
4
+ end
data/lib/llm_specs.rb ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+ require "json"
3
+ require "net/http"
4
+ require "forwardable"
5
+ require_relative "llm_specs/errors"
6
+ require_relative "llm_specs/cache"
7
+ require_relative "llm_specs/catalog"
8
+ require_relative "llm_specs/collection"
9
+ require_relative "llm_specs/model"
10
+
11
+ module LLMSpecs
12
+ API_URI = "https://api.parsera.org/v1/llm-specs"
13
+ CACHE_PATH = "models.json"
14
+
15
+ def self.models
16
+ @models ||= Catalog.new(api_uri: API_URI, cache_path: CACHE_PATH).models
17
+ end
18
+ end