router-for-me/CLIProxyAPI · error

failed to merge metadata: %w

Error message

failed to merge metadata: %w

What it means

Returned when misc.MergeMetadata fails while serializing the Kimi token storage together with its Metadata map before writing the JSON auth file (token.go:92-95). MergeMetadata merges the struct and its metadata into a single JSON-serializable value, so failure almost always means the Metadata map contains a value type that json.Marshal cannot encode (e.g. channels, funcs, cycles).

Source

Thrown at internal/auth/kimi/token.go:94

	// ExpiresIn is the number of seconds until the device code expires.
	ExpiresIn int `json:"expires_in"`
	// Interval is the minimum number of seconds to wait between polling requests.
	Interval int `json:"interval"`
}

// SaveTokenToFile serializes the Kimi token storage to a JSON file.
func (ts *KimiTokenStorage) SaveTokenToFile(authFilePath string) error {
	misc.LogSavingCredentials(authFilePath)
	ts.Type = "kimi"

	if err := os.MkdirAll(filepath.Dir(authFilePath), 0700); err != nil {
		return fmt.Errorf("failed to create directory: %v", err)
	}

	// Merge metadata using helper
	data, errMerge := misc.MergeMetadata(ts, ts.Metadata)
	if errMerge != nil {
		return fmt.Errorf("failed to merge metadata: %w", errMerge)
	}

	f, err := os.Create(authFilePath)
	if err != nil {
		return fmt.Errorf("failed to create token file: %w", err)
	}
	defer func() {
		if errClose := f.Close(); errClose != nil {
			log.Errorf("kimi token storage: close token file error: %v", errClose)
		}
	}()

	encoder := json.NewEncoder(f)
	encoder.SetIndent("", "  ")
	if err = encoder.Encode(data); err != nil {
		return fmt.Errorf("failed to write token to file: %w", err)
	}
	return nil

View on GitHub (pinned to 78f0c4079e)

Solutions

  1. Inspect what keys/values are placed into KimiTokenStorage.Metadata and restrict them to plain JSON types (string, number, bool, nested maps/slices)
  2. If embedding the SDK, JSON-encode exotic values to strings before storing them as metadata
  3. Reproduce with a minimal SaveTokenToFile call and check the wrapped %w cause for the exact marshal error

Example fix

// before
ts.Metadata = map[string]any{"done": make(chan struct{})}
// after
ts.Metadata = map[string]any{"done": "closed"}
Defensive patterns

Strategy: validation

Validate before calling

for k, v := range ts.Metadata {
    if !isJSONScalarOrContainer(v) {
        return fmt.Errorf("metadata key %q holds a non-serializable value", k)
    }
}

Try / catch

if err := ts.SaveTokenToFile(path); err != nil && strings.Contains(err.Error(), "failed to merge metadata") {
    log.Errorf("unserializable kimi metadata; dropping metadata and retrying")
    ts.Metadata = nil
    err = ts.SaveTokenToFile(path)
}

Prevention

When it happens

Trigger: KimiTokenStorage.Metadata populated with non-serializable values (func, chan, complex, or cyclic pointer structures) before SaveTokenToFile is called; extremely large metadata causing a marshaler panic captured as an error.

Common situations: Custom code embedding the SDK (sdk/cliproxy) that stuffs arbitrary Go values into Metadata; upstream code attaching an unserializable context-like object; virtually never happens with CLIProxyAPI's own metadata usage.

Related errors


AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15). Data as JSON: /api/errors/c92ed611f8e01c0f. Report an issue: GitHub.