Skip to content

Commit 2a1b889

Browse files
committed
feat: add Together AI provider support (#41)
Extends OpenAICompatibleProvider with Together-specific reasoning and thinking extraction. Vendor-prefixed model names passed through unchanged. - TogetherProvider: hybrid reasoning via { reasoning: { enabled: true } } - DeepSeek-R1 thinking: extracts <think> tags from content field, falls back to message.reasoning for other models - 10 models in registry: Llama, DeepSeek, Qwen, Gemma, Mistral - ReasoningModelDetector updated for together reasoning models - Provider count now 6 (30/30 tests pass)
1 parent 7359a2f commit 2a1b889

8 files changed

Lines changed: 227 additions & 6 deletions

File tree

‎docs/CONFIGURATION.md‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ This allows you to:
1717
## Immediate Validation
1818
Starting from this version, configuration is validated immediately:
1919
- When you call `llm:load-config` or `llm:set-provider`, the extension checks if the provider is ready
20-
- **Cloud providers** (OpenAI, Anthropic, Gemini, OpenRouter) require API keys
20+
- **Cloud providers** (OpenAI, Anthropic, Gemini, OpenRouter, Together AI) require API keys
2121
- **Ollama** requires the server to be running and reachable
2222
- If validation fails, you get a clear error message with setup instructions
2323
- Use `print llm:provider-help "provider-name"` to get detailed setup guidance
@@ -28,8 +28,8 @@ Starting from this version, configuration is validated immediately:
2828
- No quotes required; avoid trailing spaces around `=`.
2929
- **Supported keys**:
3030
- Common: `provider`, `model`, `temperature`, `max_tokens`, `timeout_seconds`
31-
- Provider-specific API keys: `openai_api_key`, `anthropic_api_key`, `gemini_api_key`, `openrouter_api_key`
32-
- Provider-specific base URLs: `openai_base_url`, `anthropic_base_url`, `gemini_base_url`, `ollama_base_url`, `openrouter_base_url`
31+
- Provider-specific API keys: `openai_api_key`, `anthropic_api_key`, `gemini_api_key`, `openrouter_api_key`, `together_api_key`
32+
- Provider-specific base URLs: `openai_base_url`, `anthropic_base_url`, `gemini_base_url`, `ollama_base_url`, `openrouter_base_url`, `together_base_url`
3333
- Legacy (still supported): `api_key`, `base_url` (applies to current provider)
3434

3535
## Where to Save the File
@@ -85,6 +85,16 @@ max_tokens=1000
8585
timeout_seconds=30
8686
```
8787

88+
### Together AI (open-source models)
89+
```
90+
provider=together
91+
together_api_key=REPLACE_ME
92+
model=meta-llama/Llama-3.3-70B-Instruct-Turbo
93+
temperature=0.7
94+
max_tokens=1000
95+
timeout_seconds=30
96+
```
97+
8898
### Local (Ollama, no API key)
8999
```
90100
provider=ollama
@@ -103,6 +113,7 @@ openai_api_key=sk-REPLACE_ME
103113
anthropic_api_key=sk-ant-REPLACE_ME
104114
gemini_api_key=REPLACE_ME
105115
openrouter_api_key=sk-or-REPLACE_ME
116+
together_api_key=REPLACE_ME
106117
107118
# Set the active provider
108119
provider=openai

‎docs/PROVIDER-GUIDE.md‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,65 @@ llm:set-thinking true
301301
llm:set-reasoning-effort "high"
302302
```
303303

304+
## Together AI Configuration
305+
306+
### API Setup
307+
308+
1. **Get API Key**: Visit [api.together.ai/settings/api-keys](https://api.together.ai/settings/api-keys)
309+
2. **Check Usage**: Monitor at [api.together.ai/settings/billing](https://api.together.ai/settings/billing)
310+
3. **Browse Models**: Explore at [api.together.ai/models](https://api.together.ai/models)
311+
312+
### Configuration Parameters
313+
314+
```ini
315+
# Required Parameters
316+
provider=together
317+
together_api_key=your-together-key-here
318+
model=meta-llama/Llama-3.3-70B-Instruct-Turbo
319+
320+
# Optional Parameters
321+
together_base_url=https://api.together.xyz/v1
322+
temperature=0.7
323+
max_tokens=1000
324+
timeout_seconds=30
325+
```
326+
327+
### Available Models
328+
329+
Model names use vendor prefixes (`vendor/model-name`):
330+
331+
| Model | Description | Context |
332+
|-------|-------------|---------|
333+
| `meta-llama/Llama-3.3-70B-Instruct-Turbo` | Fast Llama 3.3 70B | 128K |
334+
| `meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo` | Largest Llama | 128K |
335+
| `meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo` | Small, fast Llama | 128K |
336+
| `deepseek-ai/DeepSeek-R1` | DeepSeek reasoning model | 64K |
337+
| `deepseek-ai/DeepSeek-V3` | DeepSeek V3 chat | 64K |
338+
| `Qwen/Qwen2.5-72B-Instruct-Turbo` | Qwen 2.5 72B | 128K |
339+
| `google/gemma-3-27b-it` | Google Gemma 3 | 128K |
340+
| `mistralai/Mistral-Small-24B-Instruct-2501` | Mistral Small | 32K |
341+
342+
**Recommended for NetLogo**: `meta-llama/Llama-3.3-70B-Instruct-Turbo` (fast, capable, good value)
343+
344+
### Why Together AI?
345+
346+
- **Fast inference** on popular open-source models
347+
- **Pay-per-token** pricing, often cheaper than proprietary APIs
348+
- **Open-source models** — Llama, DeepSeek, Qwen, Gemma, Mistral
349+
- **Reasoning support** — DeepSeek-R1 with thinking output
350+
351+
### Thinking/Reasoning Models
352+
353+
Together AI supports reasoning models like DeepSeek-R1:
354+
355+
```netlogo
356+
llm:set-provider "together"
357+
llm:set-model "deepseek-ai/DeepSeek-R1"
358+
llm:set-thinking true
359+
let result llm:chat-with-thinking "What is 15 * 17?"
360+
; result is [answer thinking-text]
361+
```
362+
304363
## Ollama (Local) Configuration
305364

306365
### Setup Requirements

‎src/main/providers/ModelRegistry.scala‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ object ModelRegistry {
3636
), isCustom = false),
3737
"gemini" -> ProviderModels(Set("gemini-1.5-pro", "gemini-1.5-flash", "gemini-2.0-flash-exp"), isCustom = false),
3838
"ollama" -> ProviderModels(Set("llama3.2", "llama3.1", "mistral", "phi4"), isCustom = false),
39-
"openrouter" -> ProviderModels(Set("openai/gpt-4o", "openai/gpt-4o-mini", "anthropic/claude-3.5-sonnet", "deepseek/deepseek-r1"), isCustom = false)
39+
"openrouter" -> ProviderModels(Set("openai/gpt-4o", "openai/gpt-4o-mini", "anthropic/claude-3.5-sonnet", "deepseek/deepseek-r1"), isCustom = false),
40+
"together" -> ProviderModels(Set("meta-llama/Llama-3.3-70B-Instruct-Turbo", "deepseek-ai/DeepSeek-R1", "Qwen/Qwen2.5-72B-Instruct-Turbo"), isCustom = false)
4041
)
4142

4243
/**

‎src/main/providers/ProviderRegistrations.scala‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,5 +170,40 @@ object ProviderRegistrations {
170170
|Browse 200+ models: https://openrouter.ai/models""".stripMargin,
171171
factory = ec => new OpenRouterProvider()(using ec)
172172
))
173+
174+
ProviderRegistry.register(ProviderDescriptor(
175+
name = "together",
176+
displayName = "Together AI",
177+
apiKeyConfigKey = "together_api_key",
178+
baseUrlConfigKey = "together_base_url",
179+
defaultBaseUrl = "https://api.together.xyz/v1",
180+
defaultModel = "meta-llama/Llama-3.3-70B-Instruct-Turbo",
181+
defaultMaxTokens = "1000",
182+
requiresApiKey = true,
183+
apiKeyPrefix = None,
184+
readinessCheck = ReadinessCheck.ApiKey,
185+
exposesThinking = true,
186+
helpText =
187+
"""Together AI Setup Instructions:
188+
|
189+
|1. Get an API key:
190+
| - Visit https://api.together.ai/settings/api-keys
191+
| - Create a new API key
192+
|
193+
|2. Set the key:
194+
| - In config file: together_api_key=your-key-here
195+
| - Or at runtime: llm:set-api-key "your-key-here"
196+
|
197+
|3. Set a model (vendor-prefixed):
198+
| - llm:set-model "meta-llama/Llama-3.3-70B-Instruct-Turbo"
199+
| - llm:set-model "deepseek-ai/DeepSeek-R1"
200+
| - llm:set-model "Qwen/Qwen2.5-72B-Instruct-Turbo"
201+
|
202+
|4. Verify:
203+
| - Check llm:provider-status for "has-key: true"
204+
|
205+
|Browse models: https://api.together.ai/models""".stripMargin,
206+
factory = ec => new TogetherProvider()(using ec)
207+
))
173208
}
174209
}

‎src/main/providers/ReasoningModelDetector.scala‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ object ReasoningModelDetector {
105105
m.startsWith("openai/o1") || m.startsWith("openai/o3") || m.startsWith("openai/o4") ||
106106
m.contains("claude-3-7") || m.contains("claude-4") ||
107107
m.contains("deepseek-r1") || m.contains("qwq")
108+
case "together" =>
109+
val m = model.toLowerCase
110+
m.contains("deepseek-r1") || m.contains("qwq") || m.contains("qwen3")
108111
case _ => false
109112
}
110113
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// ABOUTME: Together AI provider — fast open-source model inference
2+
// ABOUTME: Extends OpenAICompatibleProvider with Together-specific reasoning and thinking extraction
3+
package org.nlogo.extensions.llm.providers
4+
5+
import org.nlogo.extensions.llm.models.ChatRequest
6+
import scala.concurrent.ExecutionContext
7+
8+
/**
9+
* Together AI provider implementation.
10+
*
11+
* Together AI provides fast inference for open-source models (Llama, DeepSeek,
12+
* Qwen, Gemma, Mistral, etc.) via an OpenAI-compatible API.
13+
*
14+
* Key differences from direct OpenAI:
15+
* - Model names are vendor-prefixed (e.g. "meta-llama/Llama-3.3-70B-Instruct-Turbo")
16+
* - Hybrid reasoning models use { reasoning: { enabled: true } }
17+
* - DeepSeek-R1 embeds thinking in <think>...</think> tags within content
18+
*/
19+
class TogetherProvider(implicit ec: ExecutionContext) extends OpenAICompatibleProvider {
20+
21+
override def providerName: String = "together"
22+
23+
override def defaultModel: String = "meta-llama/Llama-3.3-70B-Instruct-Turbo"
24+
25+
override protected def defaultBaseUrl: String = "https://api.together.xyz/v1"
26+
27+
override protected def baseUrlConfigKey: String = "together_base_url"
28+
29+
override protected def apiKeyConfigKey: String = "together_api_key"
30+
31+
override protected def defaultMaxTokens: String = "1000"
32+
33+
override protected def requiresApiKey: Boolean = true
34+
35+
// No extra headers needed (unlike OpenRouter)
36+
37+
/**
38+
* Together hybrid models use { reasoning: { enabled: true } }.
39+
* Adjustable-effort models use top-level reasoning_effort (the default from base class).
40+
* We send both when thinking is enabled — the API ignores unrecognized fields.
41+
*/
42+
override protected def applyReasoningFields(baseObj: ujson.Obj, request: ChatRequest): Unit = {
43+
// Enable reasoning for hybrid models
44+
baseObj("reasoning") = ujson.Obj("enabled" -> true)
45+
// Also pass effort level if specified (for adjustable-effort models)
46+
request.thinkingConfig.flatMap(_.reasoningEffort).foreach { effort =>
47+
baseObj("reasoning_effort") = effort
48+
}
49+
}
50+
51+
/**
52+
* Extract thinking text from Together AI responses.
53+
*
54+
* Two extraction paths:
55+
* 1. message.reasoning field (most reasoning models)
56+
* 2. <think>...</think> tags in message.content (DeepSeek-R1)
57+
*/
58+
override protected def extractThinking(message: ujson.Value): Option[String] = {
59+
// Path 1: check message.reasoning field
60+
val fromReasoning = try {
61+
message.obj.get("reasoning").flatMap { v =>
62+
val text = v.str.trim
63+
if (text.nonEmpty) Some(text) else None
64+
}
65+
} catch {
66+
case _: Exception => None
67+
}
68+
69+
if (fromReasoning.isDefined) return fromReasoning
70+
71+
// Path 2: parse <think>...</think> tags from content (DeepSeek-R1)
72+
try {
73+
val content = message("content").str
74+
val thinkPattern = """(?s)<think>(.*?)</think>""".r
75+
thinkPattern.findFirstMatchIn(content).map(_.group(1).trim).filter(_.nonEmpty)
76+
} catch {
77+
case _: Exception => None
78+
}
79+
}
80+
}

‎src/main/resources/config/models.yaml‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,27 @@ openrouter:
101101
# Qwen models via OpenRouter
102102
- qwen/qwen-max
103103

104+
together:
105+
# Meta Llama
106+
- meta-llama/Llama-3.3-70B-Instruct-Turbo
107+
- meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo
108+
- meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo
109+
- meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo
110+
111+
# DeepSeek
112+
- deepseek-ai/DeepSeek-R1
113+
- deepseek-ai/DeepSeek-V3
114+
115+
# Qwen
116+
- Qwen/Qwen2.5-72B-Instruct-Turbo
117+
- Qwen/Qwen2.5-7B-Instruct-Turbo
118+
119+
# Google Gemma
120+
- google/gemma-3-27b-it
121+
122+
# Mistral
123+
- mistralai/Mistral-Small-24B-Instruct-2501
124+
104125
ollama:
105126
# Top reasoning models
106127
- deepseek-r1:70b

‎tests.txt‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,19 @@ LLMConfigPrimitives
1010

1111
LLMProvidersAll
1212
extensions [llm]
13-
length llm:providers-all => 5
13+
length llm:providers-all => 6
1414
member? "openai" llm:providers-all => true
1515
member? "anthropic" llm:providers-all => true
1616
member? "gemini" llm:providers-all => true
1717
member? "ollama" llm:providers-all => true
1818
member? "openrouter" llm:providers-all => true
19+
member? "together" llm:providers-all => true
1920

2021
LLMProviderStatus
2122
extensions [llm]
2223
O> llm:set-api-key "test-key"
2324
O> llm:set-provider "openai"
24-
length llm:provider-status => 5
25+
length llm:provider-status => 6
2526

2627
LLMListModels
2728
extensions [llm]
@@ -259,6 +260,16 @@ LLMOpenRouterProvider
259260
O> llm:set-model "openai/gpt-4o"
260261
item 1 llm:active => "openai/gpt-4o"
261262

263+
LLMTogetherProvider
264+
extensions [llm]
265+
O> llm:set-api-key "test-key"
266+
O> llm:set-provider "together"
267+
item 0 llm:active => "together"
268+
item 1 llm:active => "meta-llama/Llama-3.3-70B-Instruct-Turbo"
269+
empty? (llm:provider-help "together") => false
270+
O> llm:set-model "deepseek-ai/DeepSeek-R1"
271+
item 1 llm:active => "deepseek-ai/DeepSeek-R1"
272+
262273
LLMConfigLoading
263274
extensions [llm]
264275
O> llm:load-config "demos/config"

0 commit comments

Comments
 (0)