Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 34 additions & 3 deletions internal/harnesses/claude.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ import (

type claudeSettings struct {
Env struct {
AnthropicBaseURL string `json:"ANTHROPIC_BASE_URL"`
AnthropicAuthToken string `json:"ANTHROPIC_AUTH_TOKEN"`
AnthropicModel string `json:"ANTHROPIC_MODEL"`
AnthropicBaseURL string `json:"ANTHROPIC_BASE_URL"`
AnthropicAuthToken string `json:"ANTHROPIC_AUTH_TOKEN"`
AnthropicModel string `json:"ANTHROPIC_MODEL"`
AnthropicCustomHeaders string `json:"ANTHROPIC_CUSTOM_HEADERS,omitempty"`
} `json:"env"`
}

Expand Down Expand Up @@ -120,6 +121,13 @@ func (c *ClaudeHarness) configureMerge(opts ConfigureOptions) error {
return fmt.Errorf("failed to merge settings file: %w", err)
}

env, ok := settings["env"].(map[string]any)
if !ok {
return fmt.Errorf("failed to merge settings file: env is not an object")
}
existingHeaders, _ := env["ANTHROPIC_CUSTOM_HEADERS"].(string)
env["ANTHROPIC_CUSTOM_HEADERS"] = withRequestyClientHeaders(existingHeaders)

if err := backupAndWriteConfigFileAsJSON(settingsPath, &settings); err != nil {
return fmt.Errorf("failed to write settings file: %w", err)
}
Expand All @@ -132,6 +140,7 @@ func (c *ClaudeHarness) configureOverwrite(opts ConfigureOptions) error {
settings.Env.AnthropicBaseURL = c.config.RouterBaseURL
settings.Env.AnthropicAuthToken = c.config.APIKey
settings.Env.AnthropicModel = opts.Model
settings.Env.AnthropicCustomHeaders = withRequestyClientHeaders("")

if err := backupAndWriteConfigFileAsJSON(c.settingsPath(), &settings); err != nil {
return fmt.Errorf("failed to write settings file: %w", err)
Expand Down Expand Up @@ -189,6 +198,7 @@ func (c *ClaudeHarness) Launch(opts LaunchOptions) error {
env["ANTHROPIC_BASE_URL"] = c.config.RouterBaseURL
env["ANTHROPIC_API_KEY"] = c.config.APIKey
env["REQUESTY_API_KEY"] = c.config.APIKey
env["ANTHROPIC_CUSTOM_HEADERS"] = withRequestyClientHeaders(env["ANTHROPIC_CUSTOM_HEADERS"])
delete(env, "ANTHROPIC_AUTH_TOKEN")
// The fast-mode check calls an Anthropic-only org endpoint that a gateway
// cannot answer; skipping it avoids a startup warning.
Expand Down Expand Up @@ -256,3 +266,24 @@ func hasAnyFlag(args []string, flags []string) bool {

return false
}

// withRequestyClientHeaders adds the Requesty client headers to headers, a
// newline-separated list of `Name: Value` lines as ANTHROPIC_CUSTOM_HEADERS
// expects, replacing any earlier values and keeping the user's other headers.
func withRequestyClientHeaders(headers string) string {
lines := []string{}
for _, line := range strings.Split(headers, "\n") {
name, _, _ := strings.Cut(line, ":")
name = strings.TrimSpace(name)
if strings.TrimSpace(line) == "" || strings.EqualFold(name, requestyClientHeader) || strings.EqualFold(name, requestyClientVersionHeader) {
continue
}
lines = append(lines, line)
}
lines = append(lines,
requestyClientHeader+": "+requestyClientName,
requestyClientVersionHeader+": "+requestyClientVersion(),
)

return strings.Join(lines, "\n")
}
31 changes: 30 additions & 1 deletion internal/harnesses/claude_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,36 @@ func TestClaudeHarnessConfigureCreatesMissingConfig(t *testing.T) {
"env": {
"ANTHROPIC_BASE_URL": "https://router.requesty.ai",
"ANTHROPIC_AUTH_TOKEN": "my-api-key",
"ANTHROPIC_MODEL": "anthropic/claude-fable-5"
"ANTHROPIC_MODEL": "anthropic/claude-fable-5",
"ANTHROPIC_CUSTOM_HEADERS": "X-Requesty-Client: requesty-cli\nX-Requesty-Client-Version: dev"
}
}`, string(settings))
}

func TestClaudeHarnessConfigureKeepsCustomHeaders(t *testing.T) {
config := config.Config{
RouterBaseURL: "https://router.requesty.ai",
APIKey: "my-api-key",
}
configDir := t.TempDir()
settingsPath := filepath.Join(configDir, "settings.json")
require.NoError(t, os.WriteFile(settingsPath, []byte(`{
"env": {"ANTHROPIC_CUSTOM_HEADERS": "X-Team: platform\nX-Requesty-Client-Version: 0.0.1\nX-Requesty-Client: requesty-cli"}
}`), 0o600))
harness := newClaudeHarness(config, configDir)

require.NoError(t, harness.Configure(ConfigureOptions{
Model: "anthropic/claude-fable-5",
}))

settings, err := os.ReadFile(settingsPath)
require.NoError(t, err)
assert.JSONEq(t, `{
"env": {
"ANTHROPIC_BASE_URL": "https://router.requesty.ai",
"ANTHROPIC_AUTH_TOKEN": "my-api-key",
"ANTHROPIC_MODEL": "anthropic/claude-fable-5",
"ANTHROPIC_CUSTOM_HEADERS": "X-Team: platform\nX-Requesty-Client: requesty-cli\nX-Requesty-Client-Version: dev"
}
}`, string(settings))
}
10 changes: 8 additions & 2 deletions internal/harnesses/codex.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,9 @@ func (c *CodexHarness) configureMerge(opts ConfigureOptions) error {
"name": "Requesty",
"base_url": fmt.Sprintf("%s/v1", c.config.RouterBaseURL),
"http_headers": map[string]any{
"X-Title": "OpenAI Codex",
"X-Title": "OpenAI Codex",
requestyClientHeader: requestyClientName,
requestyClientVersionHeader: requestyClientVersion(),
},
"auth": map[string]any{
"command": "requesty",
Expand Down Expand Up @@ -183,7 +185,9 @@ func (c *CodexHarness) configureOverwrite(opts ConfigureOptions) error {
Name: "Requesty",
BaseURL: fmt.Sprintf("%s/v1", c.config.RouterBaseURL),
HTTPHeaders: map[string]string{
"X-Title": "OpenAI Codex",
"X-Title": "OpenAI Codex",
requestyClientHeader: requestyClientName,
requestyClientVersionHeader: requestyClientVersion(),
},
Auth: codexProviderAuth{
Command: "requesty",
Expand Down Expand Up @@ -232,6 +236,8 @@ func (c *CodexHarness) Launch(opts LaunchOptions) error {
"-c", provider + ".name=" + tomlString("Requesty"),
"-c", provider + ".base_url=" + tomlString(c.config.RouterBaseURL+"/v1"),
"-c", provider + ".http_headers.X-Title=" + tomlString("OpenAI Codex"),
"-c", provider + ".http_headers." + requestyClientHeader + "=" + tomlString(requestyClientName),
"-c", provider + ".http_headers." + requestyClientVersionHeader + "=" + tomlString(requestyClientVersion()),
"-c", provider + ".auth.command=" + tomlString(requestyExecutable()),
"-c", provider + ".auth.args=[" + tomlString("auth") + "," + tomlString("token") + "," + tomlString("--profile") + "," + tomlString(c.config.Name) + "]",
"-c", "model_supports_reasoning_summaries=false",
Expand Down
4 changes: 3 additions & 1 deletion internal/harnesses/codex_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,9 @@ custom_provider_setting = "keep-me"
"base_url": "https://router.requesty.ai/v1",
"custom_provider_setting": "keep-me",
"http_headers": map[string]any{
"X-Title": "OpenAI Codex",
"X-Title": "OpenAI Codex",
"X-Requesty-Client": "requesty-cli",
"X-Requesty-Client-Version": "dev",
},
"auth": map[string]any{
"command": "requesty",
Expand Down
12 changes: 8 additions & 4 deletions internal/harnesses/deepseek.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,10 @@ func (d *DeepSeekHarness) configureMerge(opts ConfigureOptions) error {
"defaultContextWindow": deepseekContextWindow,
"defaultMaxTokens": deepseekMaxTokens,
"headers": map[string]any{
"HTTP-Referer": "https://requesty.ai",
"X-Title": "DeepSeek Harness",
"HTTP-Referer": "https://requesty.ai",
"X-Title": "DeepSeek Harness",
requestyClientHeader: requestyClientName,
requestyClientVersionHeader: requestyClientVersion(),
},
"models": models,
},
Expand Down Expand Up @@ -233,8 +235,10 @@ func (d *DeepSeekHarness) configureOverwrite(opts ConfigureOptions) error {
DefaultContextWindow: deepseekContextWindow,
DefaultMaxTokens: deepseekMaxTokens,
Headers: map[string]string{
"HTTP-Referer": "https://requesty.ai",
"X-Title": "DeepSeek Harness",
"HTTP-Referer": "https://requesty.ai",
"X-Title": "DeepSeek Harness",
requestyClientHeader: requestyClientName,
requestyClientVersionHeader: requestyClientVersion(),
},
Models: []deepseekModelConfig{
{ID: opts.Model},
Expand Down
12 changes: 8 additions & 4 deletions internal/harnesses/deepseek_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,10 @@ llm-deepseek:
"defaultContextWindow": 200000,
"defaultMaxTokens": 8192,
"headers": map[string]any{
"HTTP-Referer": "https://requesty.ai",
"X-Title": "DeepSeek Harness",
"HTTP-Referer": "https://requesty.ai",
"X-Title": "DeepSeek Harness",
"X-Requesty-Client": "requesty-cli",
"X-Requesty-Client-Version": "dev",
},
"models": []any{
map[string]any{"id": "deepseek/deepseek-v4-pro-0813"},
Expand Down Expand Up @@ -127,8 +129,10 @@ func TestDeepSeekHarnessConfigureCreatesMissingFiles(t *testing.T) {
DefaultContextWindow: 200000,
DefaultMaxTokens: 8192,
Headers: map[string]string{
"HTTP-Referer": "https://requesty.ai",
"X-Title": "DeepSeek Harness",
"HTTP-Referer": "https://requesty.ai",
"X-Title": "DeepSeek Harness",
"X-Requesty-Client": "requesty-cli",
"X-Requesty-Client-Version": "dev",
},
Models: []deepseekModelConfig{
{ID: "anthropic/claude-fable-5"},
Expand Down
16 changes: 16 additions & 0 deletions internal/harnesses/harnesses.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
package harnesses

import (
"strings"

"github.com/requestyai/cli/internal/config"
"github.com/requestyai/cli/internal/version"
)

// requestyClientHeader and requestyClientVersionHeader are sent on every
// request from a harness the CLI set up or launched, so the router can tell
// CLI traffic apart from harnesses configured by hand.
const (
requestyClientHeader = "X-Requesty-Client"
requestyClientVersionHeader = "X-Requesty-Client-Version"
requestyClientName = "requesty-cli"
)

func requestyClientVersion() string {
return strings.TrimPrefix(version.Version, "v")
}

type Status struct {
Files []string
Executable bool
Expand Down
58 changes: 35 additions & 23 deletions internal/harnesses/hermes.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ const (
// hermesAPIMode is the native Anthropic Messages format, which lets Requesty
// apply automatic prompt caching to Hermes' large system prompt.
hermesAPIMode = "anthropic_messages"

hermesReferer = "https://hermes-agent.nousresearch.com"
hermesTitle = "Hermes Agent"
)

type hermesConfig struct {
Expand All @@ -26,16 +29,16 @@ type hermesConfig struct {
}

type hermesModel struct {
Default string `yaml:"default"`
Provider string `yaml:"provider"`
DefaultHeaders map[string]string `yaml:"default_headers,omitempty"`
Default string `yaml:"default"`
Provider string `yaml:"provider"`
}

type hermesCustomProvider struct {
Name string `yaml:"name"`
BaseURL string `yaml:"base_url"`
APIKey string `yaml:"api_key"`
APIMode string `yaml:"api_mode"`
Name string `yaml:"name"`
BaseURL string `yaml:"base_url"`
APIKey string `yaml:"api_key"`
APIMode string `yaml:"api_mode"`
ExtraHeaders map[string]string `yaml:"extra_headers,omitempty"`
}

type HermesHarness struct {
Expand Down Expand Up @@ -243,10 +246,6 @@ func (h *HermesHarness) configureMerge(opts ConfigureOptions) error {
"model": map[string]any{
"default": opts.Model,
"provider": hermesProvider,
"default_headers": map[string]any{
"HTTP-Referer": "https://hermes-agent.nousresearch.com",
"X-Origin-Title": "Hermes",
},
},
})
if err != nil {
Expand All @@ -273,17 +272,14 @@ func (h *HermesHarness) configureOverwrite(opts ConfigureOptions) error {
Model: hermesModel{
Default: opts.Model,
Provider: hermesProvider,
DefaultHeaders: map[string]string{
"HTTP-Referer": "https://hermes-agent.nousresearch.com",
"X-Origin-Title": "Hermes",
},
},
CustomProviders: []hermesCustomProvider{
{
Name: hermesProvider,
BaseURL: h.config.RouterBaseURL,
APIKey: h.config.APIKey,
APIMode: hermesAPIMode,
Name: hermesProvider,
BaseURL: h.config.RouterBaseURL,
APIKey: h.config.APIKey,
APIMode: hermesAPIMode,
ExtraHeaders: hermesExtraHeaders(),
},
},
}
Expand All @@ -298,11 +294,16 @@ func (h *HermesHarness) configureOverwrite(opts ConfigureOptions) error {
// upsertCustomProvider replaces the Requesty entry in providers, appending it
// when the user has not configured Requesty yet.
func (h *HermesHarness) upsertCustomProvider(providers []any) []any {
extraHeaders := map[string]any{}
for key, value := range hermesExtraHeaders() {
extraHeaders[key] = value
}
provider := map[string]any{
"name": hermesProvider,
"base_url": h.config.RouterBaseURL,
"api_key": h.config.APIKey,
"api_mode": hermesAPIMode,
"name": hermesProvider,
"base_url": h.config.RouterBaseURL,
"api_key": h.config.APIKey,
"api_mode": hermesAPIMode,
"extra_headers": extraHeaders,
}

for i, existing := range providers {
Expand Down Expand Up @@ -343,3 +344,14 @@ func hermesCustomProviders(settings map[string]any) ([]any, error) {
func (h *HermesHarness) configPath() string {
return filepath.Join(h.configDir, "config.yaml")
}

// hermesExtraHeaders go on the provider entry rather than in
// model.default_headers, which Hermes ignores in anthropic_messages mode.
func hermesExtraHeaders() map[string]string {
return map[string]string{
"HTTP-Referer": hermesReferer,
"X-Title": hermesTitle,
requestyClientHeader: requestyClientName,
requestyClientVersionHeader: requestyClientVersion(),
}
}
28 changes: 19 additions & 9 deletions internal/harnesses/hermes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,6 @@ custom_providers:
"model": map[string]any{
"default": "anthropic/claude-fable-5",
"provider": "requesty",
"default_headers": map[string]any{
"HTTP-Referer": "https://hermes-agent.nousresearch.com",
"X-Origin-Title": "Hermes",
},
},
"custom_providers": []any{
map[string]any{
Expand All @@ -72,6 +68,12 @@ custom_providers:
"base_url": "https://router.requesty.ai",
"api_key": "my-api-key",
"api_mode": "anthropic_messages",
"extra_headers": map[string]any{
"HTTP-Referer": "https://hermes-agent.nousresearch.com",
"X-Title": "Hermes Agent",
"X-Requesty-Client": "requesty-cli",
"X-Requesty-Client-Version": "dev",
},
},
},
}, settings)
Expand Down Expand Up @@ -107,7 +109,13 @@ func TestHermesHarnessUpdatesExistingProvider(t *testing.T) {
"base_url": "https://router.eu.requesty.ai",
"api_key": "my-new-api-key",
"api_mode": "anthropic_messages",
"timeout": 300,
"extra_headers": map[string]any{
"HTTP-Referer": "https://hermes-agent.nousresearch.com",
"X-Title": "Hermes Agent",
"X-Requesty-Client": "requesty-cli",
"X-Requesty-Client-Version": "dev",
},
"timeout": 300,
},
}, settings["custom_providers"])

Expand Down Expand Up @@ -139,17 +147,19 @@ func TestHermesHarnessConfigureCreatesMissingConfig(t *testing.T) {
"model": map[string]any{
"default": "anthropic/claude-fable-5",
"provider": "requesty",
"default_headers": map[string]any{
"HTTP-Referer": "https://hermes-agent.nousresearch.com",
"X-Origin-Title": "Hermes",
},
},
"custom_providers": []any{
map[string]any{
"name": "requesty",
"base_url": "https://router.requesty.ai",
"api_key": "my-api-key",
"api_mode": "anthropic_messages",
"extra_headers": map[string]any{
"HTTP-Referer": "https://hermes-agent.nousresearch.com",
"X-Title": "Hermes Agent",
"X-Requesty-Client": "requesty-cli",
"X-Requesty-Client-Version": "dev",
},
},
},
}, settings)
Expand Down
Loading
Loading