router-for-me/CLIProxyAPI · error

invalid_session

invalid_session

Error message

session must be a valid JSON object

What it means

Thrown when the payload for host.model.stream_close fails to unmarshal into pluginapi.HostModelStreamCloseRequest (field: StreamID string). Wraps the encoding/json error with %w.

Source

Thrown at internal/client/codex/live/client_secret.go:330

	if expiresAfter.Anchor != "" && expiresAfter.Anchor != "created_at" {
		return 0, errors.New("expires_after.anchor must be created_at")
	}
	minimumSeconds := int64(clientSecretMinimumLifetime / time.Second)
	maximumSeconds := int64(clientSecretMaximumLifetime / time.Second)
	if expiresAfter.Seconds < minimumSeconds || expiresAfter.Seconds > maximumSeconds {
		return 0, fmt.Errorf("expires_after.seconds must be between %d and %d", minimumSeconds, maximumSeconds)
	}
	return time.Duration(expiresAfter.Seconds) * time.Second, nil
}

func normalizeClientSecretSession(session json.RawMessage) (json.RawMessage, json.RawMessage, error) {
	trimmedSession := strings.TrimSpace(string(session))
	if trimmedSession == "" || trimmedSession == "null" {
		session = json.RawMessage(`{"type":"realtime","model":"gpt-realtime"}`)
	}
	var clientSession map[string]any
	if errUnmarshal := json.Unmarshal(session, &clientSession); errUnmarshal != nil || clientSession == nil {
		return nil, nil, errors.New("session must be a valid JSON object")
	}
	sessionType, _ := clientSession["type"].(string)
	if strings.TrimSpace(sessionType) == "" {
		sessionType = "realtime"
		clientSession["type"] = sessionType
	}
	if sessionType != "realtime" {
		return nil, nil, fmt.Errorf("%w by the Codex OAuth upstream: %q", errUnsupportedSessionType, sessionType)
	}
	model, _ := clientSession["model"].(string)
	if strings.TrimSpace(model) == "" {
		model = "gpt-realtime"
		clientSession["model"] = model
	}
	clientEncoded, errMarshal := json.Marshal(clientSession)
	if errMarshal != nil {
		return nil, nil, fmt.Errorf("encode Realtime session: %w", errMarshal)
	}

View on GitHub (pinned to 78f0c4079e)

Solutions

  1. Send json.Marshal(pluginapi.HostModelStreamCloseRequest{StreamID: id}) output unchanged.
  2. Rebuild the plugin against the host's pluginapi version.
  3. Inspect the raw bytes at the RPC boundary if the cause is unclear.

Example fix

// after
raw, _ := json.Marshal(pluginapi.HostModelStreamCloseRequest{StreamID: streamID})
host.Call(ctx, "host.model.stream_close", raw)
Defensive patterns

Strategy: try-catch

Validate before calling

raw, err := json.Marshal(pluginapi.HostModelStreamCloseRequest{StreamID: id})
if err != nil {
    return err
}

Try / catch

if _, err := host.Call(ctx, "host.model.stream_close", raw); err != nil {
    log.Warnf("stream close failed for %s: %v", id, err) // close is best-effort; log and continue
}

Prevention

When it happens

Trigger: Plugin sends malformed JSON to host.model.stream_close: wrong StreamID type, truncated body, or garbage bytes from the C buffer boundary.

Common situations: Plugin SDK version mismatch; serialization bug; reusing a buffer after free in a non-Go plugin.

Related errors


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