ask-llm-providers 0.1.5 → 0.1.8
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/lib/ask/llm/version.rb +1 -1
- data/lib/ask/provider/deepseek.rb +36 -0
- data/lib/ask/provider/openai.rb +11 -24
- data/lib/ask/provider/openrouter.rb +37 -0
- data/lib/ask/skills/providers.model_select/SKILL.md +164 -0
- data/lib/ask-llm-providers.rb +4 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e0a952555ce477b6a2e1ad523112f779a25cd526e0f255a8b6bac78dca33e1c8
|
|
4
|
+
data.tar.gz: 1b90e7fe032a0295fff1a9284045b08bb1a3f82d6c3559b9ecf7f47b7a0d2d0c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 57eea3bd9cb97969545688494f37be34cc029e94ea7ee0224efd8041b7534e8b6fab82af2c35f276ef9a7b7f8a64a84edff94972b92508554d37b4dafe556f90
|
|
7
|
+
data.tar.gz: 0fc0ba47aa5437fd38cc0734ca84b15736072d925dea337d3a8f12f2226379c0fa6824e9648829ca5c67860460039457402ec87740690844c89eed97d6c6a3eb
|
data/lib/ask/llm/version.rb
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ask
|
|
4
|
+
module Providers
|
|
5
|
+
# DeepSeek API — an OpenAI-compatible provider at api.deepseek.com.
|
|
6
|
+
# Supports DeepSeek V2, V3, V4 Chat, R1 reasoning, and all DeepSeek models
|
|
7
|
+
# via the OpenAI-compatible endpoint.
|
|
8
|
+
#
|
|
9
|
+
# Configuration via environment:
|
|
10
|
+
# DEEPSEEK_API_KEY — required, your DeepSeek API key
|
|
11
|
+
# DEEPSEEK_API_BASE — optional, base URL (default: https://api.deepseek.com)
|
|
12
|
+
class DeepSeek < OpenAI
|
|
13
|
+
def api_base
|
|
14
|
+
@config.base_url || ENV["DEEPSEEK_API_BASE"] || "https://api.deepseek.com"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def headers
|
|
18
|
+
key = @config.api_key || ENV["DEEPSEEK_API_KEY"]
|
|
19
|
+
h = { "Content-Type" => "application/json" }
|
|
20
|
+
h["Authorization"] = "Bearer #{key}" if key
|
|
21
|
+
h
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class << self
|
|
25
|
+
def slug; "deepseek"; end
|
|
26
|
+
def configuration_options; %i[api_key base_url]; end
|
|
27
|
+
def configuration_requirements; %i[api_key]; end
|
|
28
|
+
def configured?(config)
|
|
29
|
+
key = config.respond_to?(:api_key) ? config.api_key : nil
|
|
30
|
+
key ||= ENV["DEEPSEEK_API_KEY"]
|
|
31
|
+
key.to_s.length > 0
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/ask/provider/openai.rb
CHANGED
|
@@ -71,16 +71,23 @@ module Ask
|
|
|
71
71
|
config.reject { |k, _| known.include?(k.to_sym) }
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
-
#
|
|
74
|
+
# Resolve provider config from passed options, env vars, and ask-auth chain.
|
|
75
75
|
def normalize_config(config)
|
|
76
76
|
return config if !config.is_a?(Hash)
|
|
77
77
|
|
|
78
|
+
slug = self.class.slug
|
|
79
|
+
env_key = ENV["#{slug.upcase}_API_KEY"]
|
|
80
|
+
auth_key = Ask::Auth.resolve(:"#{slug}_api_key") rescue nil
|
|
81
|
+
|
|
78
82
|
merged = {
|
|
79
|
-
api_key: config[:api_key] || config["api_key"] ||
|
|
80
|
-
|
|
83
|
+
api_key: config[:api_key] || config["api_key"] ||
|
|
84
|
+
config[:"#{slug}_api_key"] || config[:openai_api_key] ||
|
|
85
|
+
env_key || auth_key,
|
|
86
|
+
base_url: config[:base_url] || config["base_url"] ||
|
|
87
|
+
ENV["#{slug.upcase}_API_BASE"],
|
|
81
88
|
organization_id: config[:organization_id] || config["organization_id"],
|
|
82
89
|
project_id: config[:project_id] || config["project_id"]
|
|
83
|
-
}.merge(
|
|
90
|
+
}.merge(config.reject { |k, _| %i[api_key base_url organization_id project_id openai_api_key].include?(k.to_sym) })
|
|
84
91
|
|
|
85
92
|
Ask::LLM::Config.new(merged)
|
|
86
93
|
end
|
|
@@ -174,23 +181,3 @@ module Ask
|
|
|
174
181
|
end
|
|
175
182
|
end
|
|
176
183
|
end
|
|
177
|
-
|
|
178
|
-
# When the OpenAI provider is subclassed (e.g. OpenCode), normalize_config
|
|
179
|
-
# should also check for env vars matching the subclass slug.
|
|
180
|
-
def normalize_config(config)
|
|
181
|
-
return config if !config.is_a?(Hash)
|
|
182
|
-
|
|
183
|
-
slug = self.class.slug
|
|
184
|
-
env_key = ENV["#{slug.upcase}_API_KEY"]
|
|
185
|
-
env_base = ENV["#{slug.upcase}_API_BASE"]
|
|
186
|
-
|
|
187
|
-
merged = {
|
|
188
|
-
api_key: config[:api_key] || config["api_key"] || config[:"#{slug}_api_key"] || config[:"#{slug}_api_key"] || config[:openai_api_key] || env_key,
|
|
189
|
-
base_url: config[:base_url] || config["base_url"] || env_base,
|
|
190
|
-
organization_id: config[:organization_id] || config["organization_id"],
|
|
191
|
-
project_id: config[:project_id] || config["project_id"]
|
|
192
|
-
}.merge(config.reject { |k, _| %i[api_key base_url organization_id project_id openai_api_key].include?(k.to_sym) })
|
|
193
|
-
|
|
194
|
-
# Also preserve original config for subclass-specific key access
|
|
195
|
-
Ask::LLM::Config.new(merged)
|
|
196
|
-
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ask
|
|
4
|
+
module Providers
|
|
5
|
+
# OpenRouter API — an OpenAI-compatible aggregator at openrouter.ai.
|
|
6
|
+
# Provides access to many models through a single endpoint.
|
|
7
|
+
#
|
|
8
|
+
# Configuration via environment:
|
|
9
|
+
# OPENROUTER_API_KEY — required, your OpenRouter API key
|
|
10
|
+
# OPENROUTER_API_BASE — optional, base URL (default: https://openrouter.ai/api/v1)
|
|
11
|
+
class OpenRouter < OpenAI
|
|
12
|
+
def api_base
|
|
13
|
+
@config.base_url || ENV["OPENROUTER_API_BASE"] || "https://openrouter.ai/api/v1"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def headers
|
|
17
|
+
h = super
|
|
18
|
+
key = @config.api_key || ENV["OPENROUTER_API_KEY"]
|
|
19
|
+
h["Authorization"] = "Bearer #{key}" if key
|
|
20
|
+
h["HTTP-Referer"] = ENV["OPENROUTER_REFERER"] || "https://github.com/ask-rb"
|
|
21
|
+
h["X-Title"] = ENV["OPENROUTER_APP_TITLE"] || "ask-rb"
|
|
22
|
+
h
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class << self
|
|
26
|
+
def slug; "openrouter"; end
|
|
27
|
+
def configuration_options; %i[api_key base_url]; end
|
|
28
|
+
def configuration_requirements; %i[api_key]; end
|
|
29
|
+
def configured?(config)
|
|
30
|
+
key = config.respond_to?(:api_key) ? config.api_key : nil
|
|
31
|
+
key ||= ENV["OPENROUTER_API_KEY"]
|
|
32
|
+
key.to_s.length > 0
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: providers.model_select
|
|
3
|
+
description: How to select the right LLM model for a task — balancing cost, capability, latency, and context window
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Use this skill when choosing an LLM model for a specific task. The ask-rb
|
|
7
|
+
ecosystem uses `Ask::ModelCatalog` to resolve models and check capabilities.
|
|
8
|
+
|
|
9
|
+
## Step 1: Classify the Task
|
|
10
|
+
|
|
11
|
+
Determine what kind of task you're solving:
|
|
12
|
+
|
|
13
|
+
| Task Type | Examples | Key Requirements |
|
|
14
|
+
|-----------|----------|-----------------|
|
|
15
|
+
| **Simple chat** | Q&A, summarization, translation | Speed, low cost |
|
|
16
|
+
| **Code generation** | Write functions, review PRs | Strong coding, large context |
|
|
17
|
+
| **Reasoning/analysis** | Debugging, architecture, planning | Deep reasoning, structured output |
|
|
18
|
+
| **Structured extraction** | Parse logs, extract data | JSON mode, function calling |
|
|
19
|
+
| **Vision/multimodal** | Screenshot analysis, document OCR | Image input support |
|
|
20
|
+
| **Long document** | Analyze 100+ page docs | Large context window (200K+) |
|
|
21
|
+
| **Embeddings** | Semantic search, RAG | Embedding model, dimensions |
|
|
22
|
+
|
|
23
|
+
## Step 2: Query the Model Catalog
|
|
24
|
+
|
|
25
|
+
Access available models through the catalog:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
# List all models
|
|
29
|
+
Ask::ModelCatalog.all
|
|
30
|
+
|
|
31
|
+
# Filter by capability
|
|
32
|
+
Ask::ModelCatalog.chat_models
|
|
33
|
+
Ask::ModelCatalog.by_provider("openai")
|
|
34
|
+
Ask::ModelCatalog.by_family("gpt")
|
|
35
|
+
Ask::ModelCatalog.embedding_models
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Find a specific model by ID:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
model = Ask::ModelCatalog.find("gpt-4o")
|
|
42
|
+
model.context_window # => 128000
|
|
43
|
+
model.max_output_tokens # => 16384
|
|
44
|
+
model.supports?(:function_calling) # => true
|
|
45
|
+
model.capabilities # => ["function_calling", "structured_output", "reasoning", "vision"]
|
|
46
|
+
model.modalities # => { input: ["text", "image"], output: ["text"] }
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
If the catalog doesn't have the model you need, refresh:
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
Ask::ModelCatalog.refresh!
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Step 3: Evaluate Cost vs Capability
|
|
56
|
+
|
|
57
|
+
Use pricing data from the catalog:
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
model = Ask::ModelCatalog.find("gpt-4o")
|
|
61
|
+
pricing = model.pricing.dig(:text_tokens, :standard)
|
|
62
|
+
pricing[:input_per_million] # $ per 1M input tokens
|
|
63
|
+
pricing[:output_per_million] # $ per 1M output tokens
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Cost comparison (approximate):**
|
|
67
|
+
|
|
68
|
+
| Tier | Models | Cost/M tokens (in) | Best For |
|
|
69
|
+
|------|--------|-------------------|----------|
|
|
70
|
+
| **Frontier** | GPT-4o, Claude 4 Sonnet, Gemini 2.5 Pro | $3-15 | Complex reasoning, code generation |
|
|
71
|
+
| **Fast/Cheap** | GPT-4o-mini, Claude 4 Haiku, Gemini 2.5 Flash | $0.15-1.00 | Simple chat, extraction, classification |
|
|
72
|
+
| **Reasoning** | o3, o4-mini, DeepSeek R1 | $2-10 | Deep analysis, math, multi-step tasks |
|
|
73
|
+
| **Specialized** | Embedding, image, audio models | Varies | Non-chat tasks |
|
|
74
|
+
|
|
75
|
+
## Step 4: Match Capabilities to Task Requirements
|
|
76
|
+
|
|
77
|
+
Check if a model supports the features you need:
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
model.supports?(:function_calling) # For tool use
|
|
81
|
+
model.supports?(:structured_output) # For JSON mode
|
|
82
|
+
model.supports?(:vision) # For image analysis
|
|
83
|
+
model.supports?(:reasoning) # For complex reasoning
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Capability requirements by task:**
|
|
87
|
+
|
|
88
|
+
| Need | Check | Fallback |
|
|
89
|
+
|------|-------|----------|
|
|
90
|
+
| Tool calling | `supports?(:function_calling)` | Use text instruction instead |
|
|
91
|
+
| JSON output | `supports?(:structured_output)` | Prompt-engineering |
|
|
92
|
+
| Image processing | `modalities[:input].include?("image")` | Describe image in text |
|
|
93
|
+
| Audio processing | `modalities[:input].include?("audio")` | Transcribe first |
|
|
94
|
+
| Deep reasoning | `supports?(:reasoning)` | Chain-of-thought prompting |
|
|
95
|
+
|
|
96
|
+
## Step 5: Consider Context Window Requirements
|
|
97
|
+
|
|
98
|
+
Choose context window based on your input size:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
model.context_window # total tokens the model can process
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Guidelines:**
|
|
105
|
+
- **8K-16K** — Simple Q&A, short conversations
|
|
106
|
+
- **32K-64K** — Code review, medium documents, multi-turn conversations
|
|
107
|
+
- **100K-200K** — Large codebases, long documents, RAG with many chunks
|
|
108
|
+
- **1M-2M** — Gemini 2.5 Pro, Gemini 2.0 Flash for massive documents
|
|
109
|
+
|
|
110
|
+
Be aware that large context windows increase latency and cost even if you don't
|
|
111
|
+
use them all.
|
|
112
|
+
|
|
113
|
+
## Step 6: Pick the Right Embedding Model
|
|
114
|
+
|
|
115
|
+
For RAG and semantic search:
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
Ask::ModelCatalog.embedding_models
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Recommendations:**
|
|
122
|
+
- **General purpose**: `text-embedding-3-large` (256-3072 dims)
|
|
123
|
+
- **Best accuracy**: `text-embedding-3-large` with 3072 dimensions
|
|
124
|
+
- **Fast/Cheap**: `text-embedding-3-small` (512 dimensions)
|
|
125
|
+
- **Multilingual**: `text-embedding-3-small` (supports 100+ languages)
|
|
126
|
+
|
|
127
|
+
## Decision Tree
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
Task Type?
|
|
131
|
+
├── Simple chat / extraction
|
|
132
|
+
│ └── Fast model (GPT-4o-mini, Claude 4 Haiku)
|
|
133
|
+
│ → Cheapest adequate model
|
|
134
|
+
├── Code generation / review
|
|
135
|
+
│ └── Frontier model (GPT-4o, Claude 4 Sonnet)
|
|
136
|
+
│ → Needs function calling + max capability
|
|
137
|
+
├── Deep reasoning / debugging
|
|
138
|
+
│ └── Reasoning model (o4-mini, DeepSeek R1, o3)
|
|
139
|
+
│ → Needs chain-of-thought + analysis
|
|
140
|
+
├── Long document analysis
|
|
141
|
+
│ └── Large context (Gemini 2.5 Pro 1M, GPT-4o)
|
|
142
|
+
│ → Needs context window > input size
|
|
143
|
+
├── Multimodal (image/video)
|
|
144
|
+
│ └── Vision-capable (GPT-4o, Claude 4 Sonnet, Gemini 2.5)
|
|
145
|
+
│ → Check modalities[:input] includes image
|
|
146
|
+
├── Embeddings / RAG
|
|
147
|
+
│ └── text-embedding-3-large / small
|
|
148
|
+
│ → Not a chat model
|
|
149
|
+
└── Audio / Voice
|
|
150
|
+
└── GPT-4o-audio, Gemini Audio
|
|
151
|
+
→ Check modalities[:output] includes audio
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Provider Selection
|
|
155
|
+
|
|
156
|
+
Consider provider reliability and features:
|
|
157
|
+
|
|
158
|
+
| Provider | Strengths | Weaknesses |
|
|
159
|
+
|----------|-----------|------------|
|
|
160
|
+
| **OpenAI** | Best tool calling, broad model range | Higher cost for frontier |
|
|
161
|
+
| **Anthropic** | Excellent code, long context | Slower for simple tasks |
|
|
162
|
+
| **Google Gemini** | Massive context (1M+), fast | Fewer integration tools |
|
|
163
|
+
| **DeepSeek** | Cheap reasoning, open weights | Limited ecosystem |
|
|
164
|
+
| **Ollama** | Local, free, private | Slow, no hosted offerings |
|
data/lib/ask-llm-providers.rb
CHANGED
|
@@ -23,6 +23,8 @@ require_relative "ask/provider/cloudflare"
|
|
|
23
23
|
require_relative "ask/provider/opencode"
|
|
24
24
|
require_relative "ask/provider/opencode_go"
|
|
25
25
|
require_relative "ask/provider/mimo"
|
|
26
|
+
require_relative "ask/provider/deepseek"
|
|
27
|
+
require_relative "ask/provider/openrouter"
|
|
26
28
|
|
|
27
29
|
# Register providers with the Ask::Provider registry
|
|
28
30
|
Ask::Provider.register(:openai, Ask::Providers::OpenAI)
|
|
@@ -35,6 +37,8 @@ Ask::Provider.register(:cloudflare, Ask::Providers::Cloudflare)
|
|
|
35
37
|
Ask::Provider.register(:opencode, Ask::Providers::OpenCode)
|
|
36
38
|
Ask::Provider.register(:opencode_go, Ask::Providers::OpenCodeGo)
|
|
37
39
|
Ask::Provider.register(:mimo, Ask::Providers::Mimo)
|
|
40
|
+
Ask::Provider.register(:deepseek, Ask::Providers::DeepSeek)
|
|
41
|
+
Ask::Provider.register(:openrouter, Ask::Providers::OpenRouter)
|
|
38
42
|
|
|
39
43
|
# Register known models for each provider in the catalog
|
|
40
44
|
[
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ask-llm-providers
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kaka Ruto
|
|
@@ -182,6 +182,7 @@ files:
|
|
|
182
182
|
- lib/ask/provider/anthropic.rb
|
|
183
183
|
- lib/ask/provider/bedrock.rb
|
|
184
184
|
- lib/ask/provider/cloudflare.rb
|
|
185
|
+
- lib/ask/provider/deepseek.rb
|
|
185
186
|
- lib/ask/provider/google.rb
|
|
186
187
|
- lib/ask/provider/mimo.rb
|
|
187
188
|
- lib/ask/provider/mistral.rb
|
|
@@ -189,6 +190,8 @@ files:
|
|
|
189
190
|
- lib/ask/provider/openai.rb
|
|
190
191
|
- lib/ask/provider/opencode.rb
|
|
191
192
|
- lib/ask/provider/opencode_go.rb
|
|
193
|
+
- lib/ask/provider/openrouter.rb
|
|
194
|
+
- lib/ask/skills/providers.model_select/SKILL.md
|
|
192
195
|
homepage: https://github.com/ask-rb/ask-llm-providers
|
|
193
196
|
licenses:
|
|
194
197
|
- MIT
|