feat(config): support `HYPER_API_KEY` for hyper auth (#2583)

Andrey Nering created

Use `HYPER_API_KEY` to configure Hyper for non-interactive workflows and
bypass OAuth-driven setup when the environment variable is present.

💘 Generated with Crush

Assisted-by: Kimi K2.5 via Crush <crush@charm.land>

Change summary

internal/config/load.go      | 14 ++++++
internal/config/load_test.go | 84 +++++++++++++++++++++++++++++++++++++
2 files changed, 98 insertions(+)

Detailed changes

internal/config/load.go 🔗

@@ -295,6 +295,20 @@ func (c *Config) configureProviders(store *ConfigStore, env env.Env, resolver Va
 					return fmt.Errorf("bedrock provider only supports anthropic models for now, found: %s", model.ID)
 				}
 			}
+		case catwalk.InferenceProvider("hyper"):
+			if apiKey := env.Get("HYPER_API_KEY"); apiKey != "" {
+				prepared.APIKey = apiKey
+				prepared.APIKeyTemplate = apiKey
+			} else {
+				v, err := resolver.ResolveValue(p.APIKey)
+				if v == "" || err != nil {
+					if configExists {
+						slog.Warn("Skipping Hyper provider due to missing API key", "provider", p.ID)
+						c.Providers.Del(string(p.ID))
+					}
+					continue
+				}
+			}
 		default:
 			// if the provider api or endpoint are missing we skip them
 			v, err := resolver.ResolveValue(p.APIKey)

internal/config/load_test.go 🔗

@@ -1505,3 +1505,87 @@ func TestConfig_configureSelectedModels(t *testing.T) {
 		require.Equal(t, int64(100), large.MaxTokens)
 	})
 }
+
+func TestConfig_configureProviders_HyperAPIKeyFromEnv(t *testing.T) {
+	// Test that HYPER_API_KEY environment variable works without config
+	knownProviders := []catwalk.Provider{
+		{
+			ID:                  "hyper",
+			APIKey:              "", // No API key in provider definition
+			DefaultLargeModelID: "large-model",
+			DefaultSmallModelID: "small-model",
+			Models: []catwalk.Model{
+				{
+					ID:               "large-model",
+					DefaultMaxTokens: 1000,
+				},
+				{
+					ID:               "small-model",
+					DefaultMaxTokens: 500,
+				},
+			},
+		},
+	}
+
+	cfg := &Config{}
+	cfg.setDefaults("/tmp", "")
+	env := env.NewFromMap(map[string]string{
+		"HYPER_API_KEY": "env-api-key",
+	})
+	resolver := NewEnvironmentVariableResolver(env)
+	err := cfg.configureProviders(testStore(cfg), env, resolver, knownProviders)
+	require.NoError(t, err)
+	require.Equal(t, 1, cfg.Providers.Len())
+
+	// Verify Hyper provider is configured with the env var API key
+	pc, ok := cfg.Providers.Get("hyper")
+	require.True(t, ok, "Hyper provider should be configured")
+	require.Equal(t, "env-api-key", pc.APIKey)
+	require.Equal(t, "env-api-key", pc.APIKeyTemplate)
+}
+
+func TestConfig_configureProviders_HyperAPIKeyFromConfigOverrides(t *testing.T) {
+	// Test that config API key takes precedence when HYPER_API_KEY is also set
+	knownProviders := []catwalk.Provider{
+		{
+			ID:                  "hyper",
+			APIKey:              "provider-api-key",
+			DefaultLargeModelID: "large-model",
+			DefaultSmallModelID: "small-model",
+			Models: []catwalk.Model{
+				{
+					ID:               "large-model",
+					DefaultMaxTokens: 1000,
+				},
+				{
+					ID:               "small-model",
+					DefaultMaxTokens: 500,
+				},
+			},
+		},
+	}
+
+	// User has Hyper configured with an API key
+	cfg := &Config{
+		Providers: csync.NewMapFrom(map[string]ProviderConfig{
+			"hyper": {
+				APIKey: "config-api-key",
+			},
+		}),
+	}
+	cfg.setDefaults("/tmp", "")
+
+	// But they also have HYPER_API_KEY set - env var should take precedence
+	env := env.NewFromMap(map[string]string{
+		"HYPER_API_KEY": "env-api-key",
+	})
+	resolver := NewEnvironmentVariableResolver(env)
+	err := cfg.configureProviders(testStore(cfg), env, resolver, knownProviders)
+	require.NoError(t, err)
+	require.Equal(t, 1, cfg.Providers.Len())
+
+	// Verify env var takes precedence (as per requirements)
+	pc, ok := cfg.Providers.Get("hyper")
+	require.True(t, ok, "Hyper provider should be configured")
+	require.Equal(t, "env-api-key", pc.APIKey)
+}