package pluginabi import ( "encoding/json" "strings" ) const ( // ABIVersion tracks the native C ABI shape (native plugin exports). ABIVersion uint32 = 1 // SchemaVersion tracks the RPC JSON contract exchanged at plugin.register. // Version 2 adds request lifecycle completion and active request termination. // Version 3 omits OriginalRequest/RequestBody on payload stream chunks // (ChunkIndex >= 0); those fields remain on StreamChunkHeaderInitIndex only. // Plugins that still need per-chunk request bodies should keep schema_version < 3. // Version 4 adds upstream WebSocket response event observation. // Version 5 omits HistoryChunks on payload stream chunks (ChunkIndex >= 0); // those fields remain on StreamChunkHeaderInitIndex only. Plugins that still need // per-chunk history chunks should keep schema_version < 5. // Version 6 preserves raw JSON bodies for plugin management responses. // Plugins that still require HTML entity escaping on JSON response strings // should keep schema_version < 6. // Version 7 lets request interceptors opt out of receiving the request body // per phase via request_interceptor_omit_body_before/after capabilities and // lets frontend auth providers opt out via frontend_auth_provider_omit_body / // frontend_auth_omit_body; the host then omits the request body instead of // reading, cloning, and Base64-encoding the full payload. Plugins that declare // the flags on schema_version < 7 (or do not declare them) still receive it. // Version 8 lets request interceptors opt into raw request body delivery and // localized byte-range patching per phase via request_interceptor_patch_body_before/after // (or request_body_patcher_before/after) capabilities and the // request.patch_body_before / request.patch_body_after methods; the host passes // raw request body bytes directly across the C ABI without JSON/Base64 wrapping // and splices validated byte-range edits in a single pass. // Version 9 lets ModelRegistrar.RegisterModels and ModelProvider.StaticModels // publish model catalogs for multiple providers in one response via Registrations. // Version 10 lets a plugin expose multiple OAuth/auth provider identifiers and // per-entry display metadata via auth.identifier (identifiers/entries) or // Capabilities.AuthProviders / MultiAuthProvider / AuthProviderEntries. SchemaVersion uint32 = 10 // SchemaVersionStreamChunkOmitRequestBody is the first schema version that omits // request bodies on payload stream-chunk interceptor calls. SchemaVersionStreamChunkOmitRequestBody uint32 = 3 // SchemaVersionWebSocketResponseObserver is the first schema version that supports // upstream WebSocket response event observation. SchemaVersionWebSocketResponseObserver uint32 = 4 // SchemaVersionStreamChunkOmitHistory is the first schema version that omits // history chunks on payload stream-chunk interceptor calls. SchemaVersionStreamChunkOmitHistory uint32 = 5 // SchemaVersionRawManagementResponse is the first schema version where plugin // management JSON responses are preserved without HTML-escaping strings. SchemaVersionRawManagementResponse uint32 = 6 // SchemaVersionRequestInterceptorOmitBody is the first schema version where // request interceptors may opt out of receiving the request body per phase. SchemaVersionRequestInterceptorOmitBody uint32 = 7 // SchemaVersionFrontendAuthOmitBody is the first schema version where // frontend auth providers may opt out of receiving the request body. SchemaVersionFrontendAuthOmitBody uint32 = 7 // SchemaVersionRequestInterceptorBodyPatch is the first schema version where // request interceptors may opt into raw request body delivery and byte-range patching. SchemaVersionRequestInterceptorBodyPatch uint32 = 8 // SchemaVersionMultiProviderModels is the first schema version where model // registration responses may publish models for multiple providers via Registrations. SchemaVersionMultiProviderModels uint32 = 9 // SchemaVersionMultiAuthProviders is the first schema version where a plugin // may register multiple auth provider identifiers. SchemaVersionMultiAuthProviders uint32 = 10 ) const ( MethodPluginRegister = "plugin.register" MethodPluginQuiesce = "plugin.quiesce" MethodPluginReconfigure = "plugin.reconfigure" MethodPluginShutdown = "plugin.shutdown" MethodModelRegister = "model.register" MethodModelStatic = "model.static" MethodModelForAuth = "model.for_auth" MethodAuthIdentifier = "auth.identifier" MethodAuthParse = "auth.parse" MethodAuthLoginStart = "auth.login.start" MethodAuthLoginPoll = "auth.login.poll" MethodAuthRefresh = "auth.refresh" MethodFrontendAuthIdentifier = "frontend_auth.identifier" MethodFrontendAuthAuthenticate = "frontend_auth.authenticate" // MethodSchedulerPick asks a scheduler plugin to select an auth candidate. MethodSchedulerPick = "scheduler.pick" // MethodModelRoute asks a router plugin to select a plugin executor for a matching request. MethodModelRoute = "model.route" MethodExecutorIdentifier = "executor.identifier" MethodExecutorExecute = "executor.execute" MethodExecutorExecuteStream = "executor.execute_stream" MethodExecutorCountTokens = "executor.count_tokens" MethodExecutorHTTPRequest = "executor.http_request" MethodRequestTranslate = "request.translate" MethodRequestNormalize = "request.normalize" MethodRequestInterceptBefore = "request.intercept_before" MethodRequestInterceptAfter = "request.intercept_after" MethodRequestPatchBodyBefore = "request.patch_body_before" MethodRequestPatchBodyAfter = "request.patch_body_after" MethodRequestComplete = "request.complete" MethodResponseTranslate = "response.translate" MethodResponseNormalizeBefore = "response.normalize_before" MethodResponseNormalizeAfter = "response.normalize_after" MethodResponseInterceptAfter = "response.intercept_after" MethodResponseInterceptStreamChunk = "response.intercept_stream_chunk" MethodWebSocketResponseEvent = "websocket.response_event" MethodThinkingIdentifier = "thinking.identifier" MethodThinkingApply = "thinking.apply" MethodUsageHandle = "usage.handle" MethodCommandLineRegister = "command_line.register" MethodCommandLineExecute = "command_line.execute" MethodManagementRegister = "management.register" MethodManagementHandle = "management.handle" MethodQuotaIdentifier = "quota.identifier" MethodQuotaDescribe = "quota.describe" MethodQuotaFetch = "quota.fetch" MethodQuotaReset = "quota.reset" MethodHostHTTPDo = "host.http.do" MethodHostHTTPDoStream = "host.http.do_stream" MethodHostHTTPOperationOpen = "host.http.operation_open" MethodHostHTTPCancel = "host.http.cancel" MethodHostHTTPStreamRead = "host.http.stream_read" MethodHostHTTPStreamClose = "host.http.stream_close" MethodHostModelExecute = "host.model.execute" MethodHostModelExecuteStream = "host.model.execute_stream" MethodHostModelStreamRead = "host.model.stream_read" MethodHostModelStreamClose = "host.model.stream_close" MethodHostStreamEmit = "host.stream.emit" MethodHostStreamClose = "host.stream.close" MethodHostLog = "host.log" MethodHostAuthList = "host.auth.list" MethodHostAuthGet = "host.auth.get" MethodHostAuthGetRuntime = "host.auth.get_runtime" MethodHostAuthSave = "host.auth.save" MethodHostAuthModels = "host.auth.models" MethodHostAffinityLookup = "host.affinity.lookup" MethodHostRoutingResetCooldown = "host.routing.reset_cooldown" ) // ErrorCode constants define the machine-readable Error.Code values recognized // by CPA's routing and cooldown pipeline. const ( // ErrorCodeCredentialQuota marks a 429 refusal as exhausting or limiting the // entire credential across all of its models (sets Result.CredentialScope and // records quota reason "credential_quota"). Plugins may also set // Error.CredentialScoped = true while keeping a provider-specific Code. ErrorCodeCredentialQuota = "credential_quota" // ErrorCodeTransientRateLimit marks a 429 refusal as a transient per-model // capacity/admission limit that cools only the reporting model until // RetryAfterSeconds without recording quota exhaustion or advancing backoff. ErrorCodeTransientRateLimit = "transient_rate_limit" // ErrorCodeRequestScoped marks a failure as caused by the request itself: // CPA skips credential/model cooldown and stops credential rotation. ErrorCodeRequestScoped = "request_scoped" // ErrorCodeForceCooldown forces cooldown scheduling even when cooling is // disabled for the credential, falling back to the default transient cooldown // when no status-specific deadline applies. ErrorCodeForceCooldown = "force_cooldown" // ErrorCodeStreamTruncated marks an accepted upstream stream that terminated // without a completion frame: CPA skips credential cooldown and does not // replay the request on another credential. ErrorCodeStreamTruncated = "stream_truncated" // ErrorCodeConnectionLifecycle marks a client disconnect or session-close // failure that must skip credential cooldown. ErrorCodeConnectionLifecycle = "connection_lifecycle" // ErrorCodeTransientTransport marks a pre-HTTP dial/TLS/DNS/reset failure // that skips credential cooldown and remains eligible for request retries. ErrorCodeTransientTransport = "transient_transport" // ErrorCodeModelNotFound marks the requested model as unavailable on the // selected credential so CPA cools only that model for 12h (or RetryAfterSeconds). ErrorCodeModelNotFound = "model_not_found" // ErrorCodeNotImplemented marks an optional capability (such as count_tokens) // as unsupported so CPA keeps the credential and model available. ErrorCodeNotImplemented = "not_implemented" ) type Envelope struct { OK bool `json:"ok"` Result json.RawMessage `json:"result,omitempty"` Error *Error `json:"error,omitempty"` } // Error carries a structured plugin failure across RPC envelopes and stream // error_detail callbacks (host.stream.emit and host.stream.close). // // Routing and cooldown behavior is controlled by HTTPStatus, RetryAfterSeconds, // CredentialScoped, and the recognized ErrorCode* values in Code: // - CredentialScoped: true (or Code: ErrorCodeCredentialQuota) on a 429 marks // the entire credential as rate-limited across all of its models // (reason "credential_quota") until RetryAfterSeconds or the quota backoff. // - Code: ErrorCodeTransientRateLimit on a 429 cools only the reporting model // until RetryAfterSeconds without marking quota exceeded. // - Code: ErrorCodeRequestScoped, ErrorCodeStreamTruncated, // ErrorCodeConnectionLifecycle, or ErrorCodeTransientTransport skips // credential/model cooldown. // - Code: ErrorCodeForceCooldown enforces cooldown even when cooling is disabled. // - Code: ErrorCodeModelNotFound cools only the requested model as unsupported. // - Code: ErrorCodeNotImplemented keeps count_tokens failures availability-neutral. type Error struct { Code string `json:"code"` Message string `json:"message"` Retryable bool `json:"retryable,omitempty"` // HTTPStatus is the HTTP status code (e.g. 401, 403, 429) to surface to the client. // When omitted or 0, CPA defaults to HTTP 500 (internal_server_error). HTTPStatus int `json:"http_status,omitempty"` // RetryAfterSeconds carries upstream retry guidance for cooldown and response headers. // It is an optional hint. Schema-version agreement does not negotiate support for // this field, and older hosts silently ignore it, so plugins must not depend on it // for correctness. The same pair is accepted as error_detail on host.stream.emit // and host.stream.close. RetryAfterSeconds int `json:"retry_after_seconds,omitempty"` // CredentialScoped reports whether the failure applies to the whole credential // across all models (for example, an account-wide 429 quota or spend limit) // rather than only the requested model. CredentialScoped bool `json:"credential_scoped,omitempty"` } // Error implements the error interface for Error. func (e *Error) Error() string { if e == nil { return "" } return e.Message } // ErrorCode returns the machine-readable error code embedded in the Error. func (e *Error) ErrorCode() string { if e == nil { return "" } return e.Code } // StatusCode returns the HTTP status code embedded in the Error, or 0 if unset. func (e *Error) StatusCode() int { if e == nil { return 0 } return e.HTTPStatus } // IsCredentialScoped reports whether the Error applies to the entire credential. func (e *Error) IsCredentialScoped() bool { if e == nil { return false } return e.CredentialScoped || strings.EqualFold(strings.TrimSpace(e.Code), ErrorCodeCredentialQuota) } // IsRequestScoped reports whether the Error is tied to the request rather than the credential. func (e *Error) IsRequestScoped() bool { if e == nil || e.IsCredentialScoped() { return false } return strings.EqualFold(strings.TrimSpace(e.Code), ErrorCodeRequestScoped) } // IsTransientRateLimit reports whether the Error is a transient per-model capacity 429. func (e *Error) IsTransientRateLimit() bool { if e == nil || e.IsCredentialScoped() { return false } return strings.EqualFold(strings.TrimSpace(e.Code), ErrorCodeTransientRateLimit) } // IsStreamTruncated reports whether the Error represents a truncated accepted upstream stream. func (e *Error) IsStreamTruncated() bool { if e == nil || e.IsCredentialScoped() { return false } return strings.EqualFold(strings.TrimSpace(e.Code), ErrorCodeStreamTruncated) } // NewError creates an Error instance with an optional HTTP status code. func NewError(code, message string, httpStatus ...int) *Error { status := 0 if len(httpStatus) > 0 { status = httpStatus[0] } return &Error{ Code: code, Message: message, HTTPStatus: status, } } // NewErrorEnvelope serializes a failed RPC Envelope containing an Error with an optional HTTP status code. func NewErrorEnvelope(code, message string, httpStatus ...int) ([]byte, error) { return json.Marshal(Envelope{ OK: false, Error: NewError(code, message, httpStatus...), }) }