Skip to content

feat: implemented direct provider resolve#21

Open
arjunk820 wants to merge 2 commits into
mainfrom
Direct-to-Provider
Open

feat: implemented direct provider resolve#21
arjunk820 wants to merge 2 commits into
mainfrom
Direct-to-Provider

Conversation

@arjunk820

Copy link
Copy Markdown
Contributor

Background

Currently all LLM completions route through OpenRouter, requiring users to have an OpenRouter API key. Given the tight ties to Cerebras-provided inference for speed, we want to support direct Cerebras connections so users can bypass the OpenRouter hop for lower latency. This also lays groundwork for the future Raypaste backend, where RAYPASTE_API_KEY will authenticate against our own servers.

Changes

  • Multi-provider API key support: Added OPENROUTER_API_KEY and CEREBRAS_API_KEY config fields and environment variable bindings. RAYPASTE_API_KEY / api-key is preserved but reserved for the future Raypaste backend.
  • Smart provider routing: New ResolveProviderKey() in internal/config/config.go picks the best route — direct Cerebras key preferred, OpenRouter fallback, legacy sk-or-* key migration with deprecation notice.
  • LLM client parameterization: NewClient() now takes (provider, apiKey), sets the correct base URL (api.cerebras.ai vs openrouter.ai) and provider-appropriate headers.
  • Direct model IDs: Added DirectID field to the Model struct so Cerebras models carry their provider-native ID (e.g.,
    llama-3.1-8b-instruct) alongside the OpenRouter ID (meta-llama/llama-3.1-8b-instruct). BuildRequest selects the right one based on provider.
  • Config CLI: Added openrouter-api-key and cerebras-api-key to config set / config get commands with updated help text.
  • Updated docs: README Quick Start, Config Command, Configuration, and Troubleshooting sections updated. CHANGELOG entry for v0.4.0

Testing

  • All existing tests pass — updated NewClient and BuildRequest call signatures across client_test.go and router_test.go.
  • New TestResolveProviderKey: 9 table-driven cases covering: Cerebras direct, OpenRouter fallback, both keys set, OpenAI model with only Cerebras key (error), legacy sk-or-* migration, legacy key without prefix (error), no keys (error), unknown provider model.
  • New TestHasAnyAPIKey: 4 cases for each key source and no keys.
  • New TestGetOpenRouterAPIKey / TestGetCerebrasAPIKey: Config vs env var priority for each provider.
  • New TestBuildRequestDirectProvider: Verifies DirectID is used for direct provider routing and ID is used for OpenRouter
  • Linter: golangci-lint run ./...

Comment thread cmd/config.go Fixed
Comment thread cmd/config.go Fixed
Comment thread cmd/config.go Fixed
Comment thread cmd/config.go Fixed
@arjunk820

arjunk820 commented Mar 15, 2026

Copy link
Copy Markdown
Contributor Author

@Winston-Hsiao if none of the keys are set, there's an error thrown. if old key is set, then openrouter is used with deprecation warning / migration notice.

sk-or- prefix is required for the old api key, otherwise it will result in an error. i hope this clarifies your doubts

@Winston-Hsiao Winston-Hsiao left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hey good work, but not quite the fully flexible multi-provider (Cerebras or OpenRouter) implementation we want. We want to allow users to switch between them—which currently is not the case.

Also we need to make sure to validate when using the new `cfg.Provider = "cerebras" config that is set, that you check if the model is a valid/eligible model to use through Cerebras directly.

Also make sure to avoid just using the bare "cerebras" string, can set a typed enum in a types file somewhere and export that package/enum'd string type

Comment thread cmd/root.go
Comment on lines +124 to +129
// Validate that at least one API key is configured
if !cfg.HasAnyAPIKey() {
fmt.Fprintln(os.Stderr, "Error: No API key configured. Set one of:")
fmt.Fprintln(os.Stderr, " CEREBRAS_API_KEY - for direct Cerebras inference")
fmt.Fprintln(os.Stderr, " OPENROUTER_API_KEY - for OpenRouter multi-provider access")
fmt.Fprintln(os.Stderr, " Or run: raypaste config set cerebras-api-key <key>")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

"Or run: ..." language seems a bit odd here
maybe "Set key by running: " instead? (and give both example commands)

Comment on lines +86 to +106
{
name: "cerebras model with cerebras key goes direct",
cfg: Config{CerebrasAPIKey: "csk-123", Models: map[string]Model{}},
modelAlias: "cerebras-llama-8b",
wantProvider: "cerebras",
wantKey: "csk-123",
},
{
name: "cerebras model with only openrouter key falls back to openrouter",
cfg: Config{OpenRouterAPIKey: "sk-or-abc", Models: map[string]Model{}},
modelAlias: "cerebras-llama-8b",
wantProvider: "openrouter",
wantKey: "sk-or-abc",
},
{
name: "cerebras model with both keys prefers direct",
cfg: Config{CerebrasAPIKey: "csk-123", OpenRouterAPIKey: "sk-or-abc", Models: map[string]Model{}},
modelAlias: "cerebras-llama-8b",
wantProvider: "cerebras",
wantKey: "csk-123",
},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I envisioned we would not have a default/fallback behavior here but instead the user can set their config to choose specifically if they want to use direct to Cerebras OR direct to OpenRouter.

i.e. your current implemented behavior constrains to "cerebras model with both keys prefers direct"
given both keys are set Config{CerebrasAPIKey: "csk-123", OpenRouterAPIKey: "sk-or-abc",

  • but what if the user:
    1. doesn't want to clear their Cerebras API key to route through OpenRouter
    2. does want to switch back and forth, keeping both keys set and specifically wants to route to one or the other.

Comment thread cmd/config.go
` + output.Green("default-model") + ` - Default model alias or OpenRouter ID
` + output.Green("default-length") + ` - Default output length (short|medium|long)
` + output.Green("disable-copy") + ` - Disable auto-copy to clipboard (true|false)
` + output.Green("temperature") + ` - Sampling temperature (0.0-2.0)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

based on one of the other comments regarding switching between available provider options by choice instead of forced default to cerebras openrouter/old fallback

we would likely want a "provider" key representing the selected/active provider that stores either "cerebras" or "openrouter", it can start out initialized as nil for new users, they will be prompted to set it with:
raypaste config set provider if it's nil

for quality of life new users when setting their key initially with raypaste config set cerebras-api-key [key...] can have the provider value set to "cerebras" automatically, likewise goes for if openrouter-api-key is set first instead

We want a flexible (no annoying config) lock-in implementation—which is currently not the case

Comment thread cmd/config.go
switch key {
case "openrouter-api-key":
cfg.OpenRouterAPIKey = value
if err := cfg.SaveTo(cfgFile); err != nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

can add the cfg.Provider = ... here in these setter case blocks

Comment thread cmd/config.go
}
} else {
fmt.Println("not set")
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Avoid excessively nested if, else statements (for easier readability of logical flow).

can clean this up to be more idiomatic go example below:

case "openrouter-api-key":
	key := cfg.GetOpenRouterAPIKey()
	if key == "" {
		fmt.Println("not set")
		break
	}
	masked := maskSecret(key)
	if cfg.OpenRouterAPIKey == "" {
		fmt.Println(masked + " (from environment)")
	} else {
		fmt.Println(masked)
	}

Somewhat confusing/annoying distinction worth calling out is that we support api key setting in both config (the .yaml file OR reading from the env) seen by following the logic in your new functions which is why we check for "" twice once on the overall resolved key OR on the one set by config which allows the distinctive printing of where the key is sourced from (config or env)

You added these in internal/config/config.go

// GetOpenRouterAPIKey retrieves the OpenRouter API key from config or environment
func (c *Config) GetOpenRouterAPIKey() string {
	if c.OpenRouterAPIKey != "" {
		return c.OpenRouterAPIKey
	}
	return os.Getenv("OPENROUTER_API_KEY")
}

// GetCerebrasAPIKey retrieves the Cerebras API key from config or environment
func (c *Config) GetCerebrasAPIKey() string {
	if c.CerebrasAPIKey != "" {
		return c.CerebrasAPIKey
	}
	return os.Getenv("CEREBRAS_API_KEY")
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants