Skip to content

feat: add OpenRouter and Together AI provider support#43

Merged
JNK234 merged 6 commits into
mainfrom
openrouter-support
May 1, 2026
Merged

feat: add OpenRouter and Together AI provider support#43
JNK234 merged 6 commits into
mainfrom
openrouter-support

Conversation

@JNK234

@JNK234 JNK234 commented Apr 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

What changed

Registry refactor (addresses #31 partially)

  • ProviderDescriptor case class holds all per-provider metadata
  • ProviderRegistry singleton replaces 17+ hardcoded match/case blocks across 5 files
  • OpenAICompatibleProvider base class extracted from OpenAIProvider — shared by OpenAI, OpenRouter, Together AI
  • Net -264 lines in the refactor alone

OpenRouter provider

  • Vendor-prefixed model names (openai/gpt-4o, anthropic/claude-3.5-sonnet)
  • Custom headers (HTTP-Referer, X-Title) for OpenRouter attribution
  • Unified reasoning object for thinking models
  • Thinking text extraction from message.reasoning field

Together AI provider

  • Vendor-prefixed model names (meta-llama/Llama-3.3-70B-Instruct-Turbo)
  • Hybrid reasoning via { reasoning: { enabled: true } }
  • DeepSeek-R1 thinking extraction from <think> tags in content

Provider count: 4 → 6

OpenAI, Anthropic, Gemini, Ollama, OpenRouter, Together AI

Test plan

  • sbt compile — clean, zero warnings
  • sbt test — 30/30 pass (was 28 on main)
  • sbt assembly — fat JAR builds
  • New tests: LLMOpenRouterProvider, LLMTogetherProvider (set provider, default model, help text, set-model)
  • Provider count tests updated (4→6)
  • Codex rescue review — P1 issues fixed, architecture validated

Closes #40, closes #41

JNK234 added 2 commits April 15, 2026 18:28
Replace 17+ hardcoded match/case blocks across 5 files with a
ProviderDescriptor registry, making new provider addition a 2-3 file
operation instead of 5+.

Registry refactor:
- ProviderDescriptor case class holds all per-provider metadata
- ProviderRegistry singleton with @volatile safe publication
- ProviderRegistrations centralizes all provider registration
- ConfigStore, ProviderFactory, ModelRegistry, LLMExtension, and
  ReasoningModelDetector now read from the registry

OpenAICompatibleProvider base class:
- Extracts shared Chat Completions wire format from OpenAIProvider
- Three extension hooks: extraHeaders, applyReasoningFields, extractThinking
- OpenAIProvider slimmed from 108 to ~25 lines

OpenRouter provider:
- Vendor-prefixed model names (openai/gpt-4o, anthropic/claude-3.5-sonnet)
- HTTP-Referer and X-Title headers for attribution
- Unified reasoning object instead of reasoning_effort
- Thinking text extraction from response message.reasoning field
- 17 models in registry covering OpenAI, Anthropic, Google, Meta, DeepSeek

Net result: -264 lines, 29/29 tests passing, fat JAR builds clean.
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)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2a1b889811

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/main/LLMExtension.scala Outdated
s"Config loaded but $providerName provider requires an API key. Set '$keyName' in config. For help: print llm:provider-help \"$providerName\""
)
}
ProviderRegistry.get(providerName.toLowerCase.trim).foreach { desc =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject unknown provider names during config load

llm:load-config now validates with ProviderRegistry.get(...).foreach, so an unrecognized provider= value silently skips readiness checks instead of failing early. In configs where the provider name is mistyped (for example with only provider-specific keys present), loading succeeds and the user only sees an error later on first request when provider creation fails, which regresses the immediate-validation behavior expected from load-config.

Useful? React with 👍 / 👎.

Comment on lines +73 to +75
val content = message("content").str
val thinkPattern = """(?s)<think>(.*?)</think>""".r
thinkPattern.findFirstMatchIn(content).map(_.group(1).trim).filter(_.nonEmpty)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Strip think tags from Together answer content

For Together models that embed reasoning as <think>...</think> in message.content (the exact fallback handled here), this code extracts thinking text but does not remove that block from the returned assistant content. Because OpenAICompatibleProvider.parseProviderResponse uses message("content") as the answer, llm:chat-with-thinking can return reasoning text in both outputs (answer and thinking), leaking chain-of-thought instead of separating it.

Useful? React with 👍 / 👎.

JNK234 added 4 commits April 17, 2026 15:28
… Together content

Addresses two P2 findings from the Codex review on #43.

1. LoadConfigCommand now fails fast when `provider=` is unknown.
   Previously the .foreach pattern silently skipped validation,
   deferring the error to the first chat request. Model names
   still warn-only (preserves flexibility for unlisted models).

2. Add cleanContent hook in OpenAICompatibleProvider (default
   identity). TogetherProvider overrides it to strip <think>...</think>
   blocks so llm:chat-with-thinking no longer leaks reasoning into
   the answer slot for DeepSeek-R1.

Tests: 31/31 pass. New LLMLoadConfigRejectsUnknownProvider fixture
verifies the typo'd provider name surfaces in the error message.
Second round of fixes for Codex review on #43.

1. Revert the cleanContent hook and Together <think> tag stripping.
   Faithful pass-through of API responses is more important than
   cosmetic cleanup. Users who want thinking separated can use
   llm:chat-with-thinking, which still extracts thinking via the
   existing extractThinking hook without mutating content.

2. llm:load-config now validates the provider name against the raw
   config map before touching configStore, and snapshots + rolls back
   on readiness-check failure. A rejected load no longer leaves the
   session with a half-loaded config that breaks llm:active.

Tests: 31/31 pass.
Rewrite demos/tests/tests.nlogox with linear, readable test procedures
that exercise all 17 primitives plus provider-specific behavior:

- providers + provider-help registry coverage (6 providers)
- invalid-provider rejection (validates f539a73 fix)
- config-rollback on bad path (validates d9a33de fix)
- sync/async chat, choose, history
- thinking-config + chat-with-thinking
- openrouter-vendor-prefix (skip-aware: only when openrouter active)
- together-thinking (skip-aware: only when together + DeepSeek)
- reasoning-marker visibility in list-models

Drop heavy `carefully` defensive wrappers — keep them only where
asserting an error is the test. Final summary prints
"RESULTS: N passed, M failed" via globals.

Add demos/tests/config.txt.example as a sanitized template covering
all 6 providers with REPLACE_ME placeholders. Real config.txt
remains gitignored. Update README to point users at the template.
Bring all top-level docs in line with the data-driven provider registry
that now supports 6 providers (openai, anthropic, gemini, ollama,
openrouter, together).

- README.md: lead description and Quick Links cover all 6 providers
- SETUP.md: add OpenRouter and Together AI setup sections, update
  parameter table and provider-specific defaults; note DeepSeek-R1
  thinking-tag behavior + max_tokens guidance
- USAGE.md: provider list in `set-provider` reference; add a
  reasoning/thinking example block (`set-thinking`,
  `set-reasoning-effort`, `set-thinking-budget`,
  `chat-with-thinking`); expand discovery examples
- API-REFERENCE.md: add quick-table rows for the 4 reasoning
  primitives, add a full Reasoning / Thinking Primitives section,
  extend Valid Providers list, readiness-check table, and
  provider-status example to include OpenRouter and Together
- EXAMPLES.md: include OpenRouter and Together in the
  compare-providers list; add Reasoning Models and OpenRouter
  one-key-many-models examples
- TESTING.md: document `demos/tests/config.txt.example` workflow,
  enumerate the 13 test procedures (including the two skip-aware
  provider-specific ones), list OPENROUTER_API_KEY / TOGETHER_API_KEY
  for future smoke-test secrets
@JNK234
JNK234 merged commit ad1cc0b into main May 1, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant