fix(config): accept singular api_key in v2 model_list JSON
ModelConfig only declared APIKeys (json:"api_keys"). The singular "api_key" field used in v2 configs was silently dropped by the JSON decoder, leaving APIKeys empty and causing startup to fail with: failed to create provider: api_key or api_base is required for HTTP-based protocol "openrouter" Add ModelConfig.UnmarshalJSON (alias trick to avoid recursion) that reads an optional "api_key" field and prepends it to APIKeys. Guarded by five regression tests in model_list_sushi30_test.go covering literal keys, env:// refs, plural-only, both fields, and a full model-list array. Note: SecureString.IsZero() always returns true outside the yaml library (by design), so the presence check uses String() != "" instead. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8eec398c9f
commit
09a9a091f4
2 changed files with 138 additions and 0 deletions
|
|
@ -678,6 +678,27 @@ type ModelConfig struct {
|
|||
isVirtual bool
|
||||
}
|
||||
|
||||
// UnmarshalJSON accepts both "api_key" (singular SecureString) and "api_keys"
|
||||
// (plural SecureStrings). When "api_key" is present it is prepended to
|
||||
// APIKeys so that ModelConfig.APIKey() returns the resolved value.
|
||||
func (c *ModelConfig) UnmarshalJSON(data []byte) error {
|
||||
// Use a local alias to avoid infinite recursion through this method.
|
||||
type modelConfigAlias ModelConfig
|
||||
type wire struct {
|
||||
modelConfigAlias
|
||||
APIKey *SecureString `json:"api_key,omitempty"`
|
||||
}
|
||||
var w wire
|
||||
if err := json.Unmarshal(data, &w); err != nil {
|
||||
return err
|
||||
}
|
||||
*c = ModelConfig(w.modelConfigAlias)
|
||||
if w.APIKey != nil && w.APIKey.String() != "" {
|
||||
c.APIKeys = append(SecureStrings{w.APIKey}, c.APIKeys...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// APIKey returns the first API key from apiKeys
|
||||
func (c *ModelConfig) APIKey() string {
|
||||
if len(c.APIKeys) > 0 {
|
||||
|
|
|
|||
117
pkg/config/model_list_sushi30_test.go
Normal file
117
pkg/config/model_list_sushi30_test.go
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
// model_list_sushi30_test.go — sushi30 fork regression tests for ModelConfig
|
||||
// JSON unmarshalling of the singular "api_key" field.
|
||||
//
|
||||
// Background: ModelConfig only declares APIKeys (plural, json:"api_keys").
|
||||
// The singular "api_key" field used in v2 configs was silently dropped by the
|
||||
// standard JSON unmarshaller, leaving APIKeys empty and causing:
|
||||
// failed to create provider: api_key or api_base is required for HTTP-based protocol "openrouter"
|
||||
//
|
||||
// Fix: ModelConfig.UnmarshalJSON in pkg/config/config.go now accepts both
|
||||
// "api_key" (singular) and "api_keys" (plural) and merges them into APIKeys.
|
||||
// These tests guard against that regression being re-introduced by a future
|
||||
// upstream merge or rebase.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestModelConfig_SingularAPIKey_JSON verifies that a literal api_key value in
|
||||
// a model_list entry is unmarshalled into APIKey().
|
||||
func TestModelConfig_SingularAPIKey_JSON(t *testing.T) {
|
||||
data := `{
|
||||
"model_name": "test-model",
|
||||
"model": "openrouter/test/model",
|
||||
"api_key": "sk-literal-key"
|
||||
}`
|
||||
|
||||
var cfg ModelConfig
|
||||
require.NoError(t, json.Unmarshal([]byte(data), &cfg))
|
||||
assert.Equal(t, "sk-literal-key", cfg.APIKey(),
|
||||
"singular api_key must be available via APIKey()")
|
||||
}
|
||||
|
||||
// TestModelConfig_SingularAPIKey_EnvRef verifies that an env:// reference in
|
||||
// the singular api_key field is resolved to the environment variable value.
|
||||
func TestModelConfig_SingularAPIKey_EnvRef(t *testing.T) {
|
||||
t.Setenv("PICOCLAW_TEST_MODEL_KEY", "sk-from-env")
|
||||
|
||||
data := `{
|
||||
"model_name": "test-model",
|
||||
"model": "openrouter/test/model",
|
||||
"api_key": "env://PICOCLAW_TEST_MODEL_KEY"
|
||||
}`
|
||||
|
||||
var cfg ModelConfig
|
||||
require.NoError(t, json.Unmarshal([]byte(data), &cfg))
|
||||
assert.Equal(t, "sk-from-env", cfg.APIKey(),
|
||||
"env:// api_key must resolve to the environment variable value")
|
||||
}
|
||||
|
||||
// TestModelConfig_PluralAPIKeys_StillWork verifies that the existing api_keys
|
||||
// (plural) field continues to work after the UnmarshalJSON change.
|
||||
func TestModelConfig_PluralAPIKeys_StillWork(t *testing.T) {
|
||||
data := `{
|
||||
"model_name": "test-model",
|
||||
"model": "openrouter/test/model",
|
||||
"api_keys": ["sk-a", "sk-b"]
|
||||
}`
|
||||
|
||||
var cfg ModelConfig
|
||||
require.NoError(t, json.Unmarshal([]byte(data), &cfg))
|
||||
assert.Equal(t, "sk-a", cfg.APIKey(),
|
||||
"first entry of api_keys must be returned by APIKey()")
|
||||
assert.Len(t, cfg.APIKeys, 2)
|
||||
}
|
||||
|
||||
// TestModelConfig_BothAPIKeyFields_SingularWins verifies that when both
|
||||
// api_key and api_keys are present, the singular api_key ends up at index 0.
|
||||
func TestModelConfig_BothAPIKeyFields_SingularWins(t *testing.T) {
|
||||
data := `{
|
||||
"model_name": "test-model",
|
||||
"model": "openrouter/test/model",
|
||||
"api_key": "sk-primary",
|
||||
"api_keys": ["sk-fallback"]
|
||||
}`
|
||||
|
||||
var cfg ModelConfig
|
||||
require.NoError(t, json.Unmarshal([]byte(data), &cfg))
|
||||
assert.Equal(t, "sk-primary", cfg.APIKey(),
|
||||
"singular api_key must take slot 0 when both fields are present")
|
||||
assert.Len(t, cfg.APIKeys, 2,
|
||||
"api_keys entries must be preserved after singular api_key is prepended")
|
||||
}
|
||||
|
||||
// TestModelList_EnvRef_ResolvedByGateway mimics how a real v2 config.json
|
||||
// model_list array is loaded and checks that APIKey() returns the resolved
|
||||
// env var value, not an empty string.
|
||||
func TestModelList_EnvRef_ResolvedByGateway(t *testing.T) {
|
||||
t.Setenv("OPENROUTER_API_KEY", "sk-openrouter-test")
|
||||
|
||||
data := `[
|
||||
{
|
||||
"model_name": "glm-4.5",
|
||||
"model": "openrouter/z-ai/glm-4.5",
|
||||
"api_key": "env://OPENROUTER_API_KEY"
|
||||
},
|
||||
{
|
||||
"model_name": "gpt-4o",
|
||||
"model": "openrouter/openai/gpt-4o",
|
||||
"api_key": "env://OPENROUTER_API_KEY"
|
||||
}
|
||||
]`
|
||||
|
||||
var list SecureModelList
|
||||
require.NoError(t, json.Unmarshal([]byte(data), &list))
|
||||
require.Len(t, list, 2)
|
||||
|
||||
for _, m := range list {
|
||||
assert.Equal(t, "sk-openrouter-test", m.APIKey(),
|
||||
"model %q: api_key env:// must resolve, not return empty string", m.ModelName)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue