fix(opencode): preserve same-name model metadata
Treat provider.olpx.models as an alias set during sync so OpenCode-only fields survive round-trips unchanged.
This commit is contained in:
parent
71524ce10e
commit
14061b020f
66
examples/opencode_olpx.jsonc
Normal file
66
examples/opencode_olpx.jsonc
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
// provider.olpx.models entries may include OpenCode-only metadata.
|
||||||
|
// olpx sync preserves same-name model objects and only manages the alias set.
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"model": "olpx/gpt-5.4",
|
||||||
|
"small_model": "olpx/gpt-5.4-mini",
|
||||||
|
"provider": {
|
||||||
|
"olpx": {
|
||||||
|
"models": {
|
||||||
|
"gpt-5.4": {
|
||||||
|
"name": "gpt-5.4",
|
||||||
|
"limit": {
|
||||||
|
"context": 272000,
|
||||||
|
"output": 128000
|
||||||
|
},
|
||||||
|
"cost": {
|
||||||
|
"input": 1.75,
|
||||||
|
"output": 14,
|
||||||
|
"cache_read": 0.175,
|
||||||
|
"cache_write": 1.75
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"serviceTier": "priority",
|
||||||
|
"include": [
|
||||||
|
"reasoning.encrypted_content"
|
||||||
|
],
|
||||||
|
"parallel_tool_calls": true,
|
||||||
|
"store": false
|
||||||
|
},
|
||||||
|
"variants": {
|
||||||
|
"xhigh": {
|
||||||
|
"reasoningEffort": "xhigh",
|
||||||
|
"reasoningSummary": "auto"
|
||||||
|
},
|
||||||
|
"high": {
|
||||||
|
"reasoningEffort": "high",
|
||||||
|
"reasoningSummary": "auto"
|
||||||
|
},
|
||||||
|
"medium": {
|
||||||
|
"reasoningEffort": "medium",
|
||||||
|
"reasoningSummary": "auto"
|
||||||
|
},
|
||||||
|
"low": {
|
||||||
|
"reasoningEffort": "low",
|
||||||
|
"reasoningSummary": "auto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gpt-5.4-mini": {
|
||||||
|
"name": "gpt-5.4-mini"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "OpenCode LocalProxy CLI",
|
||||||
|
"npm": "@ai-sdk/openai",
|
||||||
|
"options": {
|
||||||
|
"apiKey": "olpx-local",
|
||||||
|
"baseURL": "http://127.0.0.1:9982/v1",
|
||||||
|
"setCacheKey": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"plugin": [
|
||||||
|
"@tarquinen/opencode-dcp@latest",
|
||||||
|
"oh-my-opencode-slim@latest"
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -411,8 +411,10 @@ func lineIndent(data []byte, pos int) string {
|
|||||||
|
|
||||||
// EnsureOLPXProvider updates (or creates) the provider.olpx entry with the given
|
// EnsureOLPXProvider updates (or creates) the provider.olpx entry with the given
|
||||||
// local base URL, local api key and alias set. Existing keys on provider.olpx
|
// local base URL, local api key and alias set. Existing keys on provider.olpx
|
||||||
// are preserved unless they conflict with the sync intent. Returns true if the
|
// are preserved unless they conflict with the sync intent. For model entries,
|
||||||
// file would actually change.
|
// sync owns only the alias set: same-name model objects are left untouched so
|
||||||
|
// OpenCode-only metadata survives round-trips. Returns true if the file would
|
||||||
|
// actually change.
|
||||||
func EnsureOLPXProvider(raw Raw, baseURL, apiKey string, aliases []string) bool {
|
func EnsureOLPXProvider(raw Raw, baseURL, apiKey string, aliases []string) bool {
|
||||||
changed := false
|
changed := false
|
||||||
if _, ok := raw["$schema"]; !ok {
|
if _, ok := raw["$schema"]; !ok {
|
||||||
@ -452,18 +454,14 @@ func EnsureOLPXProvider(raw Raw, baseURL, apiKey string, aliases []string) bool
|
|||||||
if setIfDiff(opts, "setCacheKey", true) {
|
if setIfDiff(opts, "setCacheKey", true) {
|
||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
// Build models map from alias list. Preserve any existing per-model extras
|
// Build models map from alias list. Preserve any existing per-model objects
|
||||||
// if the alias key matches; drop aliases removed locally.
|
// verbatim if the alias key matches; drop aliases removed locally.
|
||||||
existingModels, _ := olpxRaw["models"].(map[string]any)
|
existingModels, _ := olpxRaw["models"].(map[string]any)
|
||||||
newModels := map[string]any{}
|
newModels := map[string]any{}
|
||||||
aliasSet := map[string]bool{}
|
aliasSet := map[string]bool{}
|
||||||
for _, a := range aliases {
|
for _, a := range aliases {
|
||||||
aliasSet[a] = true
|
aliasSet[a] = true
|
||||||
if existing, ok := existingModels[a].(map[string]any); ok {
|
if existing, ok := existingModels[a].(map[string]any); ok {
|
||||||
// make sure "name" stays consistent with alias key
|
|
||||||
if setIfDiff(existing, "name", a) {
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
newModels[a] = existing
|
newModels[a] = existing
|
||||||
} else {
|
} else {
|
||||||
newModels[a] = map[string]any{"name": a}
|
newModels[a] = map[string]any{"name": a}
|
||||||
@ -523,9 +521,6 @@ func ValidateOLPXProvider(raw Raw, baseURL, apiKey string, aliases []string) err
|
|||||||
if modelCfg == nil {
|
if modelCfg == nil {
|
||||||
return fmt.Errorf("provider.olpx.models.%s must be an object", alias)
|
return fmt.Errorf("provider.olpx.models.%s must be an object", alias)
|
||||||
}
|
}
|
||||||
if got, _ := modelCfg["name"].(string); got != alias {
|
|
||||||
return fmt.Errorf("provider.olpx.models.%s.name mismatch", alias)
|
|
||||||
}
|
|
||||||
actual = append(actual, alias)
|
actual = append(actual, alias)
|
||||||
}
|
}
|
||||||
sort.Strings(actual)
|
sort.Strings(actual)
|
||||||
|
|||||||
@ -83,6 +83,91 @@ func TestValidateOLPXProvider(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEnsureOLPXProviderPreservesExistingModelMetadata(t *testing.T) {
|
||||||
|
raw := Raw{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"provider": map[string]any{
|
||||||
|
"olpx": map[string]any{
|
||||||
|
"npm": "@ai-sdk/openai",
|
||||||
|
"name": "OpenCode LocalProxy CLI",
|
||||||
|
"options": map[string]any{
|
||||||
|
"baseURL": "http://127.0.0.1:9982/v1",
|
||||||
|
"apiKey": "olpx-local",
|
||||||
|
"setCacheKey": true,
|
||||||
|
},
|
||||||
|
"models": map[string]any{
|
||||||
|
"gpt-5.4": map[string]any{
|
||||||
|
"name": "custom-display-name",
|
||||||
|
"limit": map[string]any{
|
||||||
|
"context": float64(272000),
|
||||||
|
"output": float64(128000),
|
||||||
|
},
|
||||||
|
"cost": map[string]any{
|
||||||
|
"input": float64(1.75),
|
||||||
|
"output": float64(14),
|
||||||
|
},
|
||||||
|
"variants": map[string]any{
|
||||||
|
"high": map[string]any{"reasoningEffort": "high"},
|
||||||
|
},
|
||||||
|
"options": map[string]any{"serviceTier": "priority"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
changed := EnsureOLPXProvider(raw, "http://127.0.0.1:9982/v1", "olpx-local", []string{"gpt-5.4"})
|
||||||
|
if changed {
|
||||||
|
t.Fatal("EnsureOLPXProvider() reported change for unchanged same-name alias")
|
||||||
|
}
|
||||||
|
|
||||||
|
providerRaw := raw["provider"].(map[string]any)
|
||||||
|
olpxRaw := providerRaw["olpx"].(map[string]any)
|
||||||
|
models := olpxRaw["models"].(map[string]any)
|
||||||
|
model := models["gpt-5.4"].(map[string]any)
|
||||||
|
if got := model["name"]; got != "custom-display-name" {
|
||||||
|
t.Fatalf("model name = %#v, want custom-display-name preserved", got)
|
||||||
|
}
|
||||||
|
if _, ok := model["limit"].(map[string]any); !ok {
|
||||||
|
t.Fatalf("model limit metadata missing: %#v", model["limit"])
|
||||||
|
}
|
||||||
|
if _, ok := model["cost"].(map[string]any); !ok {
|
||||||
|
t.Fatalf("model cost metadata missing: %#v", model["cost"])
|
||||||
|
}
|
||||||
|
if _, ok := model["variants"].(map[string]any); !ok {
|
||||||
|
t.Fatalf("model variants metadata missing: %#v", model["variants"])
|
||||||
|
}
|
||||||
|
if _, ok := model["options"].(map[string]any); !ok {
|
||||||
|
t.Fatalf("model options metadata missing: %#v", model["options"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateOLPXProviderAllowsCustomModelMetadata(t *testing.T) {
|
||||||
|
raw := Raw{
|
||||||
|
"provider": map[string]any{
|
||||||
|
"olpx": map[string]any{
|
||||||
|
"npm": "@ai-sdk/openai",
|
||||||
|
"name": "OpenCode LocalProxy CLI",
|
||||||
|
"options": map[string]any{
|
||||||
|
"baseURL": "http://127.0.0.1:9982/v1",
|
||||||
|
"apiKey": "olpx-local",
|
||||||
|
"setCacheKey": true,
|
||||||
|
},
|
||||||
|
"models": map[string]any{
|
||||||
|
"gpt-5.4": map[string]any{
|
||||||
|
"name": "custom-display-name",
|
||||||
|
"options": map[string]any{"serviceTier": "priority"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ValidateOLPXProvider(raw, "http://127.0.0.1:9982/v1", "olpx-local", []string{"gpt-5.4"}); err != nil {
|
||||||
|
t.Fatalf("ValidateOLPXProvider() unexpected error for custom metadata: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRenderSaveDataReplacesExistingProviderOLPXOnly(t *testing.T) {
|
func TestRenderSaveDataReplacesExistingProviderOLPXOnly(t *testing.T) {
|
||||||
raw := Raw{
|
raw := Raw{
|
||||||
"$schema": "https://opencode.ai/config.json",
|
"$schema": "https://opencode.ai/config.json",
|
||||||
@ -271,6 +356,44 @@ func TestRenderSaveDataWritesValidJSONToDisk(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSavePreservesExistingModelMetadataForSameAlias(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "opencode.jsonc")
|
||||||
|
seed := []byte("{\n \"$schema\": \"https://opencode.ai/config.json\",\n \"provider\": {\n \"olpx\": {\n \"npm\": \"@ai-sdk/openai\",\n \"name\": \"OpenCode LocalProxy CLI\",\n \"options\": {\n \"baseURL\": \"http://127.0.0.1:9982/v1\",\n \"apiKey\": \"olpx-local\",\n \"setCacheKey\": true\n },\n \"models\": {\n \"gpt-5.4\": {\n \"name\": \"custom-display-name\",\n \"limit\": {\n \"context\": 272000,\n \"output\": 128000\n },\n \"options\": {\n \"serviceTier\": \"priority\"\n }\n }\n }\n }\n }\n}\n")
|
||||||
|
if err := os.WriteFile(path, seed, 0o600); err != nil {
|
||||||
|
t.Fatalf("write seed config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, err := Load(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load(seed) error: %v", err)
|
||||||
|
}
|
||||||
|
changed := EnsureOLPXProvider(raw, "http://127.0.0.1:9982/v1", "olpx-local", []string{"gpt-5.4"})
|
||||||
|
if changed {
|
||||||
|
t.Fatal("EnsureOLPXProvider() reported change for preserved same-name alias metadata")
|
||||||
|
}
|
||||||
|
if err := Save(path, raw); err != nil {
|
||||||
|
t.Fatalf("Save() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
loaded, err := Load(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load(saved) error: %v", err)
|
||||||
|
}
|
||||||
|
providerRaw := loaded["provider"].(map[string]any)
|
||||||
|
olpxRaw := providerRaw["olpx"].(map[string]any)
|
||||||
|
models := olpxRaw["models"].(map[string]any)
|
||||||
|
model := models["gpt-5.4"].(map[string]any)
|
||||||
|
if got := model["name"]; got != "custom-display-name" {
|
||||||
|
t.Fatalf("saved model name = %#v, want custom-display-name preserved", got)
|
||||||
|
}
|
||||||
|
if _, ok := model["limit"].(map[string]any); !ok {
|
||||||
|
t.Fatalf("saved limit metadata missing: %#v", model["limit"])
|
||||||
|
}
|
||||||
|
if _, ok := model["options"].(map[string]any); !ok {
|
||||||
|
t.Fatalf("saved options metadata missing: %#v", model["options"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func assertValidJSON(t *testing.T, data []byte) {
|
func assertValidJSON(t *testing.T, data []byte) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
if !json.Valid(data) {
|
if !json.Valid(data) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user