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
- Send json.Marshal(pluginapi.HostModelStreamCloseRequest{StreamID: id}) output unchanged.
- Rebuild the plugin against the host's pluginapi version.
- 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
- Always close streams you opened (defer the close call).
- Marshal the typed struct.
- Treat close failures as warnings — the bridge also cleans up via callback cleanup.
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
- auth file not found
- realtime_client_secret_capacity_exhausted
- decode host log request: %w
- decode host auth list request: %w
- decode host auth get request: %w
AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15).
Data as JSON: /api/errors/4e1a9d0a187137a7.
Report an issue: GitHub.