@apifuse/provider-sdk 2.2.0-beta.15 → 2.2.0-beta.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AUTHORING.md CHANGED
@@ -411,12 +411,14 @@ Provider-server failures use a stable public envelope:
411
411
 
412
412
  `retryable` is always present on responses emitted by the current SDK. Set
413
413
  `retryable` in the `ProviderError` options when the provider knows the answer;
414
- an explicit `true` or `false` wins over SDK derivation. When it is omitted, the
415
- SDK derives the value for its known error classes and otherwise defaults to
416
- `false`. During stateful rolling upgrades, the forwarding client also accepts
417
- an older owner response that omits `retryable` and treats it as `false` without
418
- loosening the emitted response contract. Existing optional `fix` guidance is
419
- also preserved when a `ProviderError` supplies it.
414
+ an explicit `true` or `false` wins over the matching operation declaration and
415
+ SDK derivation. When it is omitted, `operations.<id>.docs.errorCodes[].retryable`
416
+ is used for a matching provider-owned code, followed by SDK derivation (which
417
+ defaults ordinary `ProviderError` values to `false`). During stateful rolling
418
+ upgrades, the forwarding client also accepts an older owner response that omits
419
+ `retryable` and treats it as `false` without loosening the emitted response
420
+ contract. Existing optional `fix` guidance is also preserved when a
421
+ `ProviderError` supplies it.
420
422
 
421
423
  `details` belongs exclusively to the provider. The server passes
422
424
  `ProviderError.options.details` through verbatim, including strings and arrays,
@@ -435,8 +437,41 @@ Treat this header as telemetry, not as provider-controlled public error detail.
435
437
  Its category, taxonomy version, retryability, and optional upstream status match
436
438
  the structured `provider_request_failed` log event.
437
439
 
438
- Registered error-code mappings take precedence for every `ProviderError`,
439
- including `ValidationError`:
440
+ Declare provider-owned operation failures next to their documentation. The
441
+ server builds a lookup once at startup and applies it to failures from that
442
+ operation:
443
+
444
+ ```ts
445
+ docs: {
446
+ errorCodes: [{
447
+ code: "UPSTREAM_SCHEMA_ERROR",
448
+ status: 502,
449
+ retryable: true,
450
+ description: "The upstream response no longer matches its schema.",
451
+ }],
452
+ },
453
+ handler: async () => {
454
+ throw new ProviderError("Upstream schema changed", {
455
+ code: "UPSTREAM_SCHEMA_ERROR",
456
+ });
457
+ },
458
+ ```
459
+
460
+ `defineProvider` accepts only statuses the server can emit: 400, 401, 404, 429,
461
+ 500, 502, 503, and 504. Invalid declared statuses fail provider definition,
462
+ not a live request. Status selection uses this order:
463
+
464
+ 1. SDK-owned errors retain SDK status semantics. Operation declarations cannot
465
+ override SDK-owned codes, stateful-forwarding failures, Zod/deadline errors,
466
+ or `TransportError` values.
467
+ 2. A matching operation `errorCodes` entry with `status` supplies the status.
468
+ This slot applies to `ValidationError` as well as ordinary `ProviderError`.
469
+ 3. The registered mappings below apply.
470
+ 4. Existing fallbacks apply: `TransportError` 502/504, unregistered input
471
+ `ValidationError` 400 (output validation 500), and other unregistered
472
+ `ProviderError` values 500.
473
+
474
+ The registered mappings are:
440
475
 
441
476
  | Error code or fallback | HTTP status |
442
477
  | --- | ---: |
@@ -451,11 +486,15 @@ including `ValidationError`:
451
486
 
452
487
  An unregistered non-validation `ProviderError` code returns HTTP 500 and emits
453
488
  the greppable `unregistered_provider_error_code` signal with the code in the
454
- structured failure log. Before publishing a new code, register its status in
455
- the SDK mapping and add it to the mapping tests; declaring it only in operation
456
- documentation does not change runtime status selection. The HTTP 400
457
- `ValidationError` behavior is only the fallback for unregistered input
458
- validation codes; a registered code such as `NOT_FOUND` retains its mapped 404.
489
+ structured failure log. A matching operation declaration, including one that
490
+ omits `status`, makes the code registered for this signal and may independently
491
+ supply `retryable`. The HTTP 400 `ValidationError` behavior is only the fallback
492
+ when neither an operation status nor a registered mapping applies.
493
+
494
+ Throw the domain `ProviderError` directly. Subclassing or wrapping it as a
495
+ `TransportError` solely to preserve a 5xx response is obsolete; declare the
496
+ domain code's `status` instead. Genuine `TransportError` values remain
497
+ SDK-owned and keep their 502/504 mapping.
459
498
 
460
499
  ### Declared secrets are SDK-enforced
461
500
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @apifuse/provider-sdk Changelog
2
2
 
3
+ ## 2.2.0-beta.16
4
+
5
+ - Release candidate for main commit c5deb2bc31a4a4a236d27a2ce3d214b533ed358f.
6
+
3
7
  ## 2.2.0-beta.15
4
8
 
5
9
  - Release candidate for main commit b5ebd25e48f6502e4ddb775d4e0a25f5c8276712.
@@ -90,6 +94,7 @@
90
94
 
91
95
  ## Unreleased
92
96
 
97
+ - Honor operation `docs.errorCodes` at runtime: declared provider-owned statuses and retryability now drive the HTTP envelope, observability header, and structured log; invalid statuses fail `defineProvider`, declared codes no longer emit the unregistered-code signal, and `TransportError` status-preservation workarounds are obsolete.
93
98
  - Add an opt-in same-origin redirect hop policy to `ctx.http`, with bounded manual following and typed failures before a refused target is requested.
94
99
  - Enforce provider-declared native TCP/TLS egress before proxy or socket setup, with revocable and expiring dynamic grants plus typed authorization failures; providers without a native egress declaration retain legacy behavior.
95
100
  - **Breaking:** Provider error `details` is now passed through verbatim; SDK observability fields (`category`, `taxonomyVersion`, `upstreamStatus`, and derived `retryable`) are no longer merged into the public body. Emitted error envelopes now require top-level `retryable`, while inbound stateful forwarding tolerates an older owner response that omits it and defaults it to `false`. The removed observability metadata is available in the new `X-ApiFuse-Error-Observability` response header.
package/dist/define.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import ms from "ms";
2
+ import { SDK_RUNTIME_OWNED_ERROR_CODES } from "./error-resolution.js";
2
3
  import { ProviderError, ValidationError } from "./errors.js";
3
4
  import { NativeEgressPolicyValidationError, validateNativeProviderConfig, } from "./native-egress-policy.js";
4
5
  import { safeParseSchemaSync } from "./schema.js";
5
6
  import { resolveHealthCheckInputDateTokens } from "./server/self-test-input-tokens.js";
6
- import { HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX, HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MIN, HEALTH_CHECK_TIMEOUT_MS_MAX, HEALTH_CHECK_TIMEOUT_MS_MIN, OPERATION_TIMEOUT_MS_MAX, OPERATION_TIMEOUT_MS_MIN, STREAM_CHUNK_BYTES_MAX, STREAM_CHUNK_BYTES_MIN, STREAM_HEARTBEAT_MS_MAX, STREAM_HEARTBEAT_MS_MIN, STREAM_IDLE_TIMEOUT_MS_MAX, STREAM_IDLE_TIMEOUT_MS_MIN, STREAM_MAX_DURATION_MS_MAX, STREAM_MAX_DURATION_MS_MIN, } from "./types.js";
7
+ import { HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX, HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MIN, HEALTH_CHECK_TIMEOUT_MS_MAX, HEALTH_CHECK_TIMEOUT_MS_MIN, OPERATION_TIMEOUT_MS_MAX, OPERATION_TIMEOUT_MS_MIN, STREAM_CHUNK_BYTES_MAX, STREAM_CHUNK_BYTES_MIN, STREAM_HEARTBEAT_MS_MAX, STREAM_HEARTBEAT_MS_MIN, STREAM_IDLE_TIMEOUT_MS_MAX, STREAM_IDLE_TIMEOUT_MS_MIN, STREAM_MAX_DURATION_MS_MAX, STREAM_MAX_DURATION_MS_MIN, VALID_OPERATION_ERROR_STATUSES, } from "./types.js";
7
8
  const CONNECTOR_ID_REGEX = /^[a-z][a-z0-9]*(-[a-z][a-z0-9]*)*$/;
8
9
  const OPERATION_ID_REGEX = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/;
9
10
  const VALID_RUNTIMES = ["standard", "shared", "browser"];
@@ -410,6 +411,23 @@ function validateOperationObservability(providerId, operations) {
410
411
  }
411
412
  }
412
413
  }
414
+ function validateOperationErrorCodes(providerId, operations) {
415
+ for (const [operationName, operation] of Object.entries(operations)) {
416
+ for (const [index, errorCode] of (operation.docs?.errorCodes ?? []).entries()) {
417
+ if (errorCode.status !== undefined &&
418
+ !VALID_OPERATION_ERROR_STATUSES.some((status) => status === errorCode.status)) {
419
+ const field = `operations.${operationName}.docs.errorCodes[${index}].status`;
420
+ throw new ValidationError(`Provider "${providerId}" has invalid ${field}: ${String(errorCode.status)} is not an emittable provider error status.`, {
421
+ fix: `Set ${field} to one of ${VALID_OPERATION_ERROR_STATUSES.join(", ")}, or omit it.`,
422
+ });
423
+ }
424
+ if (errorCode.status !== undefined &&
425
+ SDK_RUNTIME_OWNED_ERROR_CODES.has(errorCode.code)) {
426
+ console.warn(`[provider-sdk] Provider "${providerId}" operation "${operationName}" declares status ${errorCode.status} for SDK-owned error code "${errorCode.code}"; the declared status is documentation-only and will be ignored at runtime.`);
427
+ }
428
+ }
429
+ }
430
+ }
413
431
  const JSON_TRANSPORT_FIELDS = new Set(["kind"]);
414
432
  const SSE_TRANSPORT_FIELDS = new Set([
415
433
  "kind",
@@ -1396,6 +1414,7 @@ export function defineProvider(config) {
1396
1414
  validateOperationIds(config.id, config.operations);
1397
1415
  validateOperationAnnotations(config.id, config.operations);
1398
1416
  validateOperationObservability(config.id, config.operations);
1417
+ validateOperationErrorCodes(config.id, config.operations);
1399
1418
  validateOperationTransports(config.id, config.operations);
1400
1419
  validateOperationContracts(config.id, config.operations);
1401
1420
  validateToolRouterMetadata(config.id, config.operations);
@@ -0,0 +1,2 @@
1
+ export declare const SDK_OWNED_PROVIDER_ERROR_CODES: Set<string>;
2
+ export declare const SDK_RUNTIME_OWNED_ERROR_CODES: Set<string>;
@@ -0,0 +1,90 @@
1
+ // This set suppresses the unregistered-provider-error-code signal for codes
2
+ // intentionally emitted by SDK paths. It is not the complete authority for
3
+ // runtime error resolution: branded errors and additional canonical SDK codes
4
+ // must also remain immune to provider-declared status/retryability overrides.
5
+ export const SDK_OWNED_PROVIDER_ERROR_CODES = new Set([
6
+ "MISSING_SECRET",
7
+ "AUTH_PROMPT_UNAVAILABLE",
8
+ "BROWSER_CDP_POOL_REQUIRED",
9
+ "BROWSER_RUNTIME_UNSUPPORTED",
10
+ "STEALTH_RUNTIME_UNSUPPORTED",
11
+ "SSE_EVENT_UNDECLARED",
12
+ "STREAM_EVENT_TOO_LARGE",
13
+ "STREAM_CHUNK_TOO_LARGE",
14
+ "SSE_RESULT_UNSUPPORTED",
15
+ "STREAM_RESULT_UNSUPPORTED",
16
+ "AUTH_FLOW_NOT_CONFIGURED",
17
+ "refresh_not_supported",
18
+ "RUNTIME_UNSUPPORTED",
19
+ "PROVIDER_STATE_UNSUPPORTED",
20
+ "CHOICE_TOKEN_MASTER_SECRET_NOT_CONFIGURED",
21
+ "CHOICE_STATE_PAYLOAD_TOO_LARGE",
22
+ "CHOICE_STATE_UNAVAILABLE",
23
+ "CHOICE_CONTEXT_REQUIRED",
24
+ "unsupported_stealth_cookie_store_version",
25
+ "provider_secret_error",
26
+ "credential_key_error",
27
+ "credential_mode_error",
28
+ "flow_expired",
29
+ "turn_validation_error",
30
+ "context_access_error",
31
+ "UNSUPPORTED_STT_OPTION",
32
+ "INVALID_STT_AUDIO",
33
+ "STT_AUDIO_TOO_LARGE",
34
+ "STT_UPSTREAM_FAILED",
35
+ "INVALID_STT_VERIFICATION_CODE_OPTIONS",
36
+ "NO_CODE_FOUND",
37
+ "AMBIGUOUS_CODE",
38
+ "retry_invalid_policy",
39
+ "retry_unsafe_method",
40
+ "stealth_cookie_store_serialize_failed",
41
+ "response_too_large",
42
+ "transport_stream_unavailable",
43
+ "transport_invalid_method",
44
+ "http_transport_override_unsupported",
45
+ "http_redirect_policy_invalid",
46
+ "http_redirect_stopped",
47
+ "http_redirect_max_hops",
48
+ "http_redirect_missing_location",
49
+ "http_redirect_loop",
50
+ "transport_invalid_url",
51
+ "retry_exhausted",
52
+ "auth_abort_unsafe_data",
53
+ "credentials_auth_missing_credential_keys",
54
+ "credentials_auth_missing_credential",
55
+ "credentials_auth_invalid_login_result",
56
+ "credentials_auth_unknown_challenge",
57
+ "credentials_auth_unknown_pending_challenge",
58
+ "STATEFUL_FORWARDING_NOT_CONFIGURED",
59
+ "STATEFUL_FORWARDING_SIGNATURE_MISSING",
60
+ "STATEFUL_FORWARDING_NONCE_INVALID",
61
+ "STATEFUL_FORWARDING_TIMESTAMP_INVALID",
62
+ "STATEFUL_FORWARDING_SIGNATURE_INVALID",
63
+ "STATEFUL_FORWARDING_REPLAY_DETECTED",
64
+ "STATEFUL_FORWARDING_REPLAY_CACHE_FULL",
65
+ "STATEFUL_FORWARDING_ENVELOPE_INVALID",
66
+ "STATEFUL_FORWARDING_PROVIDER_MISMATCH",
67
+ "STATEFUL_FORWARDING_SOURCE_POD_MISMATCH",
68
+ "STATEFUL_FORWARDING_OWNER_FENCE_INVALID",
69
+ "STATEFUL_FORWARDING_REQUEST_FAILED",
70
+ "STATEFUL_FORWARDING_CONTEXT_MISSING",
71
+ "STATEFUL_FORWARDING_BAD_RESPONSE",
72
+ "STATEFUL_INTERNAL_EXECUTOR_NOT_CONFIGURED",
73
+ "STATEFUL_FILE_FORWARDING_UNSUPPORTED",
74
+ "STATEFUL_CONTROL_PLANE_OPERATION_AMBIGUOUS",
75
+ "STATEFUL_CONTROL_PLANE_REQUEST_FAILED",
76
+ "STATEFUL_CONTROL_PLANE_HTTP_ERROR",
77
+ "STATEFUL_CONTROL_PLANE_INVALID_RESPONSE",
78
+ ]);
79
+ // Complete code authority for provider-declared runtime resolution. Keep this
80
+ // separate from signal suppression: declarations may document these codes, but
81
+ // their status and retryability can never override the SDK's canonical result.
82
+ export const SDK_RUNTIME_OWNED_ERROR_CODES = new Set([
83
+ ...SDK_OWNED_PROVIDER_ERROR_CODES,
84
+ "reauth_required",
85
+ "STT_UNAVAILABLE",
86
+ "UNSUPPORTED_STT_BACKEND",
87
+ "OUTPUT_VALIDATION_FAILED",
88
+ "NOT_FOUND",
89
+ "not_found",
90
+ ]);
package/dist/index.d.ts CHANGED
@@ -37,7 +37,7 @@ export { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_REDACTION_MARKER, APIFUSE_SEN
37
37
  export { createServerApp, ERROR_OBSERVABILITY_HEADER, type ServeOptions, serve, } from "./server/index.js";
38
38
  export { getStealthProfile, listStealthProfiles } from "./stealth/profiles.js";
39
39
  export * from "./stream.js";
40
- export type { ApiFuseResponse, AuthConfig, AuthContext, AuthFlowDefinition, AuthFlowInputHandler, AuthFlowStartHandler, AuthMode, AuthTurn, Bcp47Locale, BrowserEngine, BrowserOptions, BrowserResourceBody, BrowserResourceDecision, BrowserResourceMethod, BrowserResourcePolicy, BrowserResourceRequest, BrowserResourceRoute, ConnectionMode, ContextDeclaration, CookieJar, ContextScratchpad, CredentialContext, CredentialDeclaration, E164PhoneNumber, EnvContext, FlowContext, FlowContextStore, HealthCheckAssertionContext, HealthCheckCase, HealthCheckCaseResult, HealthCheckSuite, HealthCheckUnsupported, HealthJourneyDefinition, HealthJourneyEventContext, HealthJourneyGatewayContext, HealthJourneyJournalContext, HealthJourneyManualTriggerPolicy, HealthJourneyRunContext, HealthJourneyRunResult, HealthJourneySchedule, HealthScheduleRandomization, HealthJourneySmsContext, HealthJourneyStep, HttpClient, HttpMethod, HttpRedirectFailureReason, HttpRedirectPolicy, HttpRedirectPolicyMode, HttpResponse, HttpRetryOptions, HttpRetrySummary, HttpStreamResponse, IanaTimeZone, InferSchemaOutput, Iso3166Alpha2CountryCode, Iso4217CurrencyCode, Iso8601Duration, NativeContext, NativeNetworkClient, NativeNetworkCloseReason, NativeNetworkConnection, NativeNetworkConnectInput, NativeNetworkConnectOptions, NativeNetworkDynamicGrantOptions, NativeNetworkEgressGrant, NativeProviderConfig, NativeProviderContext, NativeProxyDrainHandler, NativeProxyEgressInfo, NativeProxyExpiringEvent, NativeProxyExpiringReason, NativeTcpDynamicEgressRule, NativeTcpEgressGrant, NativeTcpEgressRule, NativeTcpPortRange, NativeTcpTlsMode, NativeTlsConnectOptions, OperationAnnotations, OperationApprovalPolicy, OperationContractMetadata, OperationDefinition, OperationDeprecationMetadata, OperationDocMeta, OperationErrorCode, OperationHandlerResult, OperationInputExample, OperationLifecycle, OperationObservabilityConfig, OperationObservabilitySensitiveConfig, OperationRelationships, OperationRiskClass, OperationSensitivePath, OperationToolRouterMetadata, OperationTransport, OperationTransportKind, ProbeInterval, ProviderAccessConfig, ProviderAccessVisibility, ProviderCache, ProviderCacheGetOrSetOptions, ProviderCacheKeyOptions, ProviderCacheLookupMeta, ProviderCacheResponseMeta, ProviderCacheResult, ProviderChoiceBindingOptions, ProviderChoiceContext, ProviderChoiceIssueOptions, ProviderChoiceParseOptions, ProviderContext, ProviderDefinition, ProviderDeploymentOverrides, ProviderFileRef, ProviderFilesContext, ProviderHealthMonitorConfig, ProviderHealthProbeConfig, ProviderLocale, ProviderLocaleKey, ProviderLocaleKeyInput, ProviderLogoProfile, ProviderLogoSource, ProviderMeta, ProviderProxyConfig, ProviderProxyMode, ProviderProxyPolicy, ProviderProxyProvider, ProviderProxySessionAffinity, ProviderPublicConnectionMode, ProviderPublicProfile, ProviderReviewed, ProviderResolvedFile, ProviderRuntimeState, ProviderSecretDeclaration, ProviderStateDurationString, ProviderStateNamespace, ProviderStreamEvent, ProviderSttConfig, ProviderSttMode, ProviderSupportLevel, RequestOptions, RedirectRunReason, Rfc3339Instant, SchemaLike, SmsOrigin, SmsOtpExtractionPattern, SmsOtpMatcherDefinition, SseMessage, StandardSchemaV1, StateCasResult, StateNamespaceOptions, StateValue, StateWriteOptions, StealthClient, StealthCookieStore, StealthCookieStoreV1, StealthFetchOptions, StealthPlatform, StealthProfile, StealthRedirectHop, StealthRedirectRunOptions, StealthRedirectRunResult, StealthResponse, StealthSession, StealthSessionCookies, SttAudioInput, SttContext, SttPromptPolicy, SttSegment, SttTranscribeMode, SttTranscribeRequest, SttTranscript, SttUnsupportedOptionPolicy, SttUsage, SttVerificationCodeOptions, SttWarning, TraceConfig, TraceSpan, VerificationCodeCandidate, VerificationCodeCandidateSource, VerificationCodeExtractionResult, } from "./types.js";
40
+ export type { ApiFuseResponse, AuthConfig, AuthContext, AuthFlowDefinition, AuthFlowInputHandler, AuthFlowStartHandler, AuthMode, AuthTurn, Bcp47Locale, BrowserEngine, BrowserOptions, BrowserResourceBody, BrowserResourceDecision, BrowserResourceMethod, BrowserResourcePolicy, BrowserResourceRequest, BrowserResourceRoute, ConnectionMode, ContextDeclaration, CookieJar, ContextScratchpad, CredentialContext, CredentialDeclaration, E164PhoneNumber, EnvContext, FlowContext, FlowContextStore, HealthCheckAssertionContext, HealthCheckCase, HealthCheckCaseResult, HealthCheckSuite, HealthCheckUnsupported, HealthJourneyDefinition, HealthJourneyEventContext, HealthJourneyGatewayContext, HealthJourneyJournalContext, HealthJourneyManualTriggerPolicy, HealthJourneyRunContext, HealthJourneyRunResult, HealthJourneySchedule, HealthScheduleRandomization, HealthJourneySmsContext, HealthJourneyStep, HttpClient, HttpMethod, HttpRedirectFailureReason, HttpRedirectPolicy, HttpRedirectPolicyMode, HttpResponse, HttpRetryOptions, HttpRetrySummary, HttpStreamResponse, IanaTimeZone, InferSchemaOutput, Iso3166Alpha2CountryCode, Iso4217CurrencyCode, Iso8601Duration, NativeContext, NativeNetworkClient, NativeNetworkCloseReason, NativeNetworkConnection, NativeNetworkConnectInput, NativeNetworkConnectOptions, NativeNetworkDynamicGrantOptions, NativeNetworkEgressGrant, NativeProviderConfig, NativeProviderContext, NativeProxyDrainHandler, NativeProxyEgressInfo, NativeProxyExpiringEvent, NativeProxyExpiringReason, NativeTcpDynamicEgressRule, NativeTcpEgressGrant, NativeTcpEgressRule, NativeTcpPortRange, NativeTcpTlsMode, NativeTlsConnectOptions, OperationAnnotations, OperationApprovalPolicy, OperationContractMetadata, OperationDefinition, OperationDeprecationMetadata, OperationDocMeta, OperationErrorCode, ProviderErrorStatus, OperationHandlerResult, OperationInputExample, OperationLifecycle, OperationObservabilityConfig, OperationObservabilitySensitiveConfig, OperationRelationships, OperationRiskClass, OperationSensitivePath, OperationToolRouterMetadata, OperationTransport, OperationTransportKind, ProbeInterval, ProviderAccessConfig, ProviderAccessVisibility, ProviderCache, ProviderCacheGetOrSetOptions, ProviderCacheKeyOptions, ProviderCacheLookupMeta, ProviderCacheResponseMeta, ProviderCacheResult, ProviderChoiceBindingOptions, ProviderChoiceContext, ProviderChoiceIssueOptions, ProviderChoiceParseOptions, ProviderContext, ProviderDefinition, ProviderDeploymentOverrides, ProviderFileRef, ProviderFilesContext, ProviderHealthMonitorConfig, ProviderHealthProbeConfig, ProviderLocale, ProviderLocaleKey, ProviderLocaleKeyInput, ProviderLogoProfile, ProviderLogoSource, ProviderMeta, ProviderProxyConfig, ProviderProxyMode, ProviderProxyPolicy, ProviderProxyProvider, ProviderProxySessionAffinity, ProviderPublicConnectionMode, ProviderPublicProfile, ProviderReviewed, ProviderResolvedFile, ProviderRuntimeState, ProviderSecretDeclaration, ProviderStateDurationString, ProviderStateNamespace, ProviderStreamEvent, ProviderSttConfig, ProviderSttMode, ProviderSupportLevel, RequestOptions, RedirectRunReason, Rfc3339Instant, SchemaLike, SmsOrigin, SmsOtpExtractionPattern, SmsOtpMatcherDefinition, SseMessage, StandardSchemaV1, StateCasResult, StateNamespaceOptions, StateValue, StateWriteOptions, StealthClient, StealthCookieStore, StealthCookieStoreV1, StealthFetchOptions, StealthPlatform, StealthProfile, StealthRedirectHop, StealthRedirectRunOptions, StealthRedirectRunResult, StealthResponse, StealthSession, StealthSessionCookies, SttAudioInput, SttContext, SttPromptPolicy, SttSegment, SttTranscribeMode, SttTranscribeRequest, SttTranscript, SttUnsupportedOptionPolicy, SttUsage, SttVerificationCodeOptions, SttWarning, TraceConfig, TraceSpan, VerificationCodeCandidate, VerificationCodeCandidateSource, VerificationCodeExtractionResult, } from "./types.js";
41
41
  export { DEFAULT_OPERATION_TRANSPORT, HttpRetryAfterPolicy, HttpRetryDelayStrategy, HttpRetryJitter, HttpRetryPreset, HttpRetryUnsafeMethodPolicy, PROBE_INTERVALS, STREAM_CHUNK_BYTES_MAX, STREAM_CHUNK_BYTES_MIN, STREAM_HEARTBEAT_MS_MAX, STREAM_HEARTBEAT_MS_MIN, STREAM_IDLE_TIMEOUT_MS_MAX, STREAM_IDLE_TIMEOUT_MS_MIN, STREAM_MAX_DURATION_MS_MAX, STREAM_MAX_DURATION_MS_MIN, } from "./types.js";
42
42
  export * from "./utils/date.js";
43
43
  export * from "./utils/parse.js";
@@ -7,6 +7,6 @@ export { AuthError, HttpRedirectError, isProviderError, isSessionExpiredError, i
7
7
  export { getProviderLocalePath, providerLocaleKey, qualifyProviderLocaleKey, } from "./i18n/index.js";
8
8
  export { type CreateProviderChoiceContextOptions, createProviderChoiceContext, createTestProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "./runtime/choice.js";
9
9
  export { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_REDACTION_MARKER, APIFUSE_SENSITIVE_KIND_META_KEY, APIFUSE_SENSITIVE_META_KEY, collectSensitivePaths, describeKey, field, fields, isSensitiveSchema, redactPayload, type SensitiveFieldKind, type SensitiveFieldOptions, type SensitivePath, sensitive, z, } from "./schema.js";
10
- export type { AuthAbortData, AuthAbortRetry, AuthFlowTerminalContext, AuthMode, AuthSafeData, AuthSafeJson, FlowContext, HealthCheckAssertionContext, HealthCheckCase, HealthCheckSuite, HealthCheckUnsupported, HealthJourneyDefinition, HealthJourneyEventContext, HealthJourneyManualTriggerPolicy, HealthJourneyRunContext, HealthJourneyRunResult, HealthScheduleRandomization, HttpRedirectFailureReason, HttpRedirectPolicy, HttpRedirectPolicyMode, HttpRetryOptions, HttpRetrySummary, InferSchemaOutput, NativeContext, NativeNetworkClient, NativeNetworkCloseReason, NativeNetworkConnection, NativeNetworkConnectInput, NativeNetworkConnectOptions, NativeNetworkDynamicGrantOptions, NativeNetworkEgressGrant, NativeProviderConfig, NativeProviderContext, NativeProxyDrainHandler, NativeProxyEgressInfo, NativeProxyExpiringEvent, NativeProxyExpiringReason, NativeTcpDynamicEgressRule, NativeTcpEgressGrant, NativeTcpEgressRule, NativeTcpPortRange, NativeTcpTlsMode, NativeTlsConnectOptions, OperationApprovalPolicy, OperationContractMetadata, OperationDefinition, OperationDocMeta, OperationErrorCode, OperationInputExample, OperationLifecycle, OperationObservabilityConfig, OperationObservabilitySensitiveConfig, OperationRelationships, OperationRiskClass, OperationSensitivePath, OperationToolRouterMetadata, OperationTransport, ProviderAccessVisibility, ProviderChoiceBindingOptions, ProviderChoiceContext, ProviderChoiceIssueOptions, ProviderChoiceParseOptions, ProviderContext, ProviderDefinition, ProviderDeploymentOverrides, ProviderFileRef, ProviderFilesContext, ProviderLocale, ProviderLocaleKey, ProviderLocaleKeyInput, ProviderLogoProfile, ProviderProxyPolicy, ProviderPublicConnectionMode, ProviderPublicProfile, ProviderResolvedFile, ProviderRuntimeState, ProviderStateDurationString, ProviderStateNamespace, ProviderSupportLevel, RedirectRunReason, SchemaLike, SmsOtpMatcherDefinition, StandardSchemaV1, StateCasResult, StateNamespaceOptions, StateValue, StateWriteOptions, } from "./types.js";
10
+ export type { AuthAbortData, AuthAbortRetry, AuthFlowTerminalContext, AuthMode, AuthSafeData, AuthSafeJson, FlowContext, HealthCheckAssertionContext, HealthCheckCase, HealthCheckSuite, HealthCheckUnsupported, HealthJourneyDefinition, HealthJourneyEventContext, HealthJourneyManualTriggerPolicy, HealthJourneyRunContext, HealthJourneyRunResult, HealthScheduleRandomization, HttpRedirectFailureReason, HttpRedirectPolicy, HttpRedirectPolicyMode, HttpRetryOptions, HttpRetrySummary, InferSchemaOutput, NativeContext, NativeNetworkClient, NativeNetworkCloseReason, NativeNetworkConnection, NativeNetworkConnectInput, NativeNetworkConnectOptions, NativeNetworkDynamicGrantOptions, NativeNetworkEgressGrant, NativeProviderConfig, NativeProviderContext, NativeProxyDrainHandler, NativeProxyEgressInfo, NativeProxyExpiringEvent, NativeProxyExpiringReason, NativeTcpDynamicEgressRule, NativeTcpEgressGrant, NativeTcpEgressRule, NativeTcpPortRange, NativeTcpTlsMode, NativeTlsConnectOptions, OperationApprovalPolicy, OperationContractMetadata, OperationDefinition, OperationDocMeta, OperationErrorCode, ProviderErrorStatus, OperationInputExample, OperationLifecycle, OperationObservabilityConfig, OperationObservabilitySensitiveConfig, OperationRelationships, OperationRiskClass, OperationSensitivePath, OperationToolRouterMetadata, OperationTransport, ProviderAccessVisibility, ProviderChoiceBindingOptions, ProviderChoiceContext, ProviderChoiceIssueOptions, ProviderChoiceParseOptions, ProviderContext, ProviderDefinition, ProviderDeploymentOverrides, ProviderFileRef, ProviderFilesContext, ProviderLocale, ProviderLocaleKey, ProviderLocaleKeyInput, ProviderLogoProfile, ProviderProxyPolicy, ProviderPublicConnectionMode, ProviderPublicProfile, ProviderResolvedFile, ProviderRuntimeState, ProviderStateDurationString, ProviderStateNamespace, ProviderSupportLevel, RedirectRunReason, SchemaLike, SmsOtpMatcherDefinition, StandardSchemaV1, StateCasResult, StateNamespaceOptions, StateValue, StateWriteOptions, } from "./types.js";
11
11
  export { createNativeNetworkClient, deriveNativeCredentialAffinityKey, NativeEgressGrantExpiredError, NativeEgressNotDeclaredError, NativeIdleTimeoutError, NativeNetworkError, NativeProxyExpiredError, resolveNativeGatewayProxy, type NativeGatewayProxy, type NativeGatewayProxyResolutionInput, type NativeGatewayProxySynthesizer, type NativeGatewayProxySynthesisInput, type NativeNetworkClientOptions, type NativeNetworkErrorCode, } from "./runtime/native-network.js";
12
12
  export { HttpRetryAfterPolicy, HttpRetryDelayStrategy, HttpRetryJitter, HttpRetryPreset, HttpRetryUnsafeMethodPolicy, } from "./types.js";
@@ -3,6 +3,7 @@ import { join } from "node:path";
3
3
  import { Hono } from "hono";
4
4
  import { z } from "zod";
5
5
  import { AuthAbortError, createAuthFlowHelpers } from "../auth.js";
6
+ import { SDK_OWNED_PROVIDER_ERROR_CODES, SDK_RUNTIME_OWNED_ERROR_CODES, } from "../error-resolution.js";
6
7
  import { AuthError, isProviderError, isSessionExpiredError, isTransportError, isValidationError, ProviderError, } from "../errors.js";
7
8
  import { loadProviderLocaleCatalogs, localizeAuthTurn, } from "../i18n/catalog.js";
8
9
  import { categoryForStatus, isRetryableCategory, PROVIDER_OBSERVABILITY_TAXONOMY_VERSION, } from "../observability.js";
@@ -29,6 +30,7 @@ import { STATEFUL_NONCE_HEADER as STATEFUL_FORWARDING_NONCE_HEADER, STATEFUL_SIG
29
30
  import { StatefulRoutingDeadlineError } from "../stateful/stateful-provider-session-routing.js";
30
31
  import { getStealthProfile } from "../stealth/profiles.js";
31
32
  import { APIFUSE_STREAM_DONE_EVENT, APIFUSE_STREAM_ERROR_EVENT, encodeSseEvent, error as streamError, } from "../stream.js";
33
+ import { VALID_OPERATION_ERROR_STATUSES } from "../types.js";
32
34
  import { createSelfTestApp, createSelfTestAuthFlowInvoke, createSelfTestInvoke, resolveSelfTestPort, } from "./self-test.js";
33
35
  import { resolveSelfTestMasterSecrets } from "./self-test-token.js";
34
36
  import { AuthFlowRequestSchema, OperationConnectionSchema, OperationRequestSchema, } from "./types.js";
@@ -353,8 +355,8 @@ function zodDetails(error) {
353
355
  message: issue.message,
354
356
  }));
355
357
  }
356
- function toErrorResponse(error, requestId) {
357
- const observability = errorObservabilityDetails(error);
358
+ function toErrorResponse(error, requestId, declaredErrorCode) {
359
+ const observability = errorObservabilityDetails(error, declaredErrorCode);
358
360
  if (error instanceof StatefulRoutingDeadlineError) {
359
361
  return {
360
362
  error: {
@@ -414,7 +416,10 @@ function toErrorResponse(error, requestId) {
414
416
  // narrowing from a ProviderError-typed value would collapse the negative branch
415
417
  // to `never`. Narrowing from unknown avoids that while still recognizing errors
416
418
  // from a duplicate SDK module instance.
417
- function providerObservabilityDetails(error) {
419
+ function providerObservabilityDetails(error, declaredErrorCode) {
420
+ const declaredRetryable = sdkOwnsErrorResolution(error)
421
+ ? undefined
422
+ : declaredErrorCode?.retryable;
418
423
  // Session-expiry surfaces the credential_expired category + the opt-in
419
424
  // retryable signal so Gateway/Credential Service can refresh and re-drive the
420
425
  // operation (see design.md §4.3 D3). Without this branch the auth error would
@@ -424,7 +429,7 @@ function providerObservabilityDetails(error) {
424
429
  return {
425
430
  category: error.options?.category ?? "credential_expired",
426
431
  taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
427
- retryable: error.options?.retryable ?? false,
432
+ retryable: error.options?.retryable ?? declaredRetryable ?? false,
428
433
  };
429
434
  }
430
435
  // Missing-secret errors carry the canonical credential_unavailable category
@@ -436,7 +441,7 @@ function providerObservabilityDetails(error) {
436
441
  return {
437
442
  category: error.options?.category ?? "credential_unavailable",
438
443
  taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
439
- retryable: error.options?.retryable ?? false,
444
+ retryable: error.options?.retryable ?? declaredRetryable ?? false,
440
445
  };
441
446
  }
442
447
  if (!isTransportError(error)) {
@@ -467,17 +472,23 @@ function providerObservabilityDetails(error) {
467
472
  ...(error.upstreamStatus ? { upstreamStatus: error.upstreamStatus } : {}),
468
473
  };
469
474
  }
470
- function errorObservabilityDetails(error) {
471
- const providerDetails = providerObservabilityDetails(error);
475
+ function errorObservabilityDetails(error, declaredErrorCode) {
476
+ const effectiveDeclaration = sdkOwnsErrorResolution(error) ? undefined : declaredErrorCode;
477
+ const providerDetails = providerObservabilityDetails(error, effectiveDeclaration);
472
478
  if (providerDetails)
473
479
  return providerDetails;
474
480
  if (error instanceof z.ZodError || isValidationError(error)) {
481
+ const declaredStatus = effectiveDeclaration?.status;
475
482
  return {
476
483
  category: isProviderError(error) && error.options?.category
477
484
  ? error.options.category
478
- : "input_validation",
485
+ : isEmittableErrorStatus(declaredStatus) && declaredStatus >= 500
486
+ ? "provider_error"
487
+ : "input_validation",
479
488
  taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
480
- retryable: isProviderError(error) ? (error.options?.retryable ?? false) : false,
489
+ retryable: isProviderError(error)
490
+ ? (error.options?.retryable ?? effectiveDeclaration?.retryable ?? false)
491
+ : false,
481
492
  };
482
493
  }
483
494
  if (error instanceof StatefulRoutingDeadlineError) {
@@ -491,7 +502,7 @@ function errorObservabilityDetails(error) {
491
502
  return {
492
503
  category: error.options?.category ?? "provider_error",
493
504
  taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
494
- retryable: error.options?.retryable ?? false,
505
+ retryable: error.options?.retryable ?? effectiveDeclaration?.retryable ?? false,
495
506
  };
496
507
  }
497
508
  return {
@@ -500,9 +511,9 @@ function errorObservabilityDetails(error) {
500
511
  retryable: false,
501
512
  };
502
513
  }
503
- function responseWithErrorObservability(response, error) {
514
+ function responseWithErrorObservability(response, error, declaredErrorCode) {
504
515
  const headers = new Headers(response.headers);
505
- headers.set(ERROR_OBSERVABILITY_HEADER, JSON.stringify(errorObservabilityDetails(error)));
516
+ headers.set(ERROR_OBSERVABILITY_HEADER, JSON.stringify(errorObservabilityDetails(error, declaredErrorCode)));
506
517
  return new Response(response.body, {
507
518
  status: response.status,
508
519
  statusText: response.statusText,
@@ -534,7 +545,11 @@ function publicProviderErrorMessage(error) {
534
545
  }
535
546
  return error.message;
536
547
  }
537
- function toStatusCode(error) {
548
+ function isEmittableErrorStatus(value) {
549
+ return (typeof value === "number" &&
550
+ VALID_OPERATION_ERROR_STATUSES.some((status) => status === value));
551
+ }
552
+ function toStatusCode(error, declaredErrorCode) {
538
553
  if (error instanceof z.ZodError) {
539
554
  return 400;
540
555
  }
@@ -542,6 +557,10 @@ function toStatusCode(error) {
542
557
  return 504;
543
558
  }
544
559
  if (isProviderError(error)) {
560
+ if (!sdkOwnsErrorResolution(error) &&
561
+ isEmittableErrorStatus(declaredErrorCode?.status)) {
562
+ return declaredErrorCode.status;
563
+ }
545
564
  switch (error.code) {
546
565
  case "AUTH_REQUIRED":
547
566
  case "reauth_required":
@@ -576,82 +595,32 @@ function toStatusCode(error) {
576
595
  }
577
596
  return 500;
578
597
  }
579
- // Codes emitted by SDK-owned paths must never be attributed to provider
580
- // authors by the unregistered-code signal, even when their intentional status
581
- // is 500. Provider-authored codes not in this registry retain the signal.
582
- const SDK_OWNED_PROVIDER_ERROR_CODES = new Set([
583
- "AUTH_PROMPT_UNAVAILABLE",
584
- "BROWSER_CDP_POOL_REQUIRED",
585
- "BROWSER_RUNTIME_UNSUPPORTED",
586
- "STEALTH_RUNTIME_UNSUPPORTED",
587
- "SSE_EVENT_UNDECLARED",
588
- "STREAM_EVENT_TOO_LARGE",
589
- "STREAM_CHUNK_TOO_LARGE",
590
- "SSE_RESULT_UNSUPPORTED",
591
- "STREAM_RESULT_UNSUPPORTED",
592
- "AUTH_FLOW_NOT_CONFIGURED",
593
- "refresh_not_supported",
594
- "RUNTIME_UNSUPPORTED",
595
- "PROVIDER_STATE_UNSUPPORTED",
596
- "CHOICE_TOKEN_MASTER_SECRET_NOT_CONFIGURED",
597
- "CHOICE_STATE_PAYLOAD_TOO_LARGE",
598
- "CHOICE_STATE_UNAVAILABLE",
599
- "CHOICE_CONTEXT_REQUIRED",
600
- "unsupported_stealth_cookie_store_version",
601
- "provider_secret_error",
602
- "credential_key_error",
603
- "credential_mode_error",
604
- "flow_expired",
605
- "turn_validation_error",
606
- "context_access_error",
607
- "UNSUPPORTED_STT_OPTION",
608
- "INVALID_STT_AUDIO",
609
- "STT_AUDIO_TOO_LARGE",
610
- "STT_UPSTREAM_FAILED",
611
- "INVALID_STT_VERIFICATION_CODE_OPTIONS",
612
- "NO_CODE_FOUND",
613
- "AMBIGUOUS_CODE",
614
- "retry_invalid_policy",
615
- "retry_unsafe_method",
616
- "stealth_cookie_store_serialize_failed",
617
- "response_too_large",
618
- "transport_stream_unavailable",
619
- "transport_invalid_method",
620
- "http_transport_override_unsupported",
621
- "http_redirect_policy_invalid",
622
- "http_redirect_stopped",
623
- "http_redirect_max_hops",
624
- "http_redirect_missing_location",
625
- "http_redirect_loop",
626
- "transport_invalid_url",
627
- "retry_exhausted",
628
- "auth_abort_unsafe_data",
629
- "credentials_auth_missing_credential_keys",
630
- "credentials_auth_missing_credential",
631
- "credentials_auth_invalid_login_result",
632
- "credentials_auth_unknown_challenge",
633
- "credentials_auth_unknown_pending_challenge",
634
- "STATEFUL_FORWARDING_NOT_CONFIGURED",
635
- "STATEFUL_FORWARDING_SIGNATURE_MISSING",
636
- "STATEFUL_FORWARDING_NONCE_INVALID",
637
- "STATEFUL_FORWARDING_TIMESTAMP_INVALID",
638
- "STATEFUL_FORWARDING_SIGNATURE_INVALID",
639
- "STATEFUL_FORWARDING_REPLAY_DETECTED",
640
- "STATEFUL_FORWARDING_REPLAY_CACHE_FULL",
641
- "STATEFUL_FORWARDING_ENVELOPE_INVALID",
642
- "STATEFUL_FORWARDING_PROVIDER_MISMATCH",
643
- "STATEFUL_FORWARDING_SOURCE_POD_MISMATCH",
644
- "STATEFUL_FORWARDING_OWNER_FENCE_INVALID",
645
- "STATEFUL_FORWARDING_REQUEST_FAILED",
646
- "STATEFUL_FORWARDING_CONTEXT_MISSING",
647
- "STATEFUL_FORWARDING_BAD_RESPONSE",
648
- "STATEFUL_INTERNAL_EXECUTOR_NOT_CONFIGURED",
649
- "STATEFUL_FILE_FORWARDING_UNSUPPORTED",
650
- "STATEFUL_CONTROL_PLANE_OPERATION_AMBIGUOUS",
651
- "STATEFUL_CONTROL_PLANE_REQUEST_FAILED",
652
- "STATEFUL_CONTROL_PLANE_HTTP_ERROR",
653
- "STATEFUL_CONTROL_PLANE_INVALID_RESPONSE",
654
- ]);
598
+ function sdkOwnsErrorResolution(error) {
599
+ if (isSessionExpiredError(error))
600
+ return true;
601
+ if (isTransportError(error))
602
+ return true;
603
+ if (error instanceof z.ZodError)
604
+ return true;
605
+ if (error instanceof StatefulRoutingDeadlineError)
606
+ return true;
607
+ return (isProviderError(error) &&
608
+ typeof error.code === "string" &&
609
+ SDK_RUNTIME_OWNED_ERROR_CODES.has(error.code));
610
+ }
611
+ function buildOperationErrorCodeLookup(provider) {
612
+ return new Map(Object.entries(provider.operations).flatMap(([operationId, operation]) => {
613
+ const errorCodes = operation.docs?.errorCodes;
614
+ return errorCodes?.length
615
+ ? [[operationId, new Map(errorCodes.map((entry) => [entry.code, entry]))]]
616
+ : [];
617
+ }));
618
+ }
619
+ function declaredErrorCodeFor(error, operationId, lookup) {
620
+ if (!operationId || !isProviderError(error) || typeof error.code !== "string")
621
+ return undefined;
622
+ return lookup.get(operationId)?.get(error.code);
623
+ }
655
624
  function extractRequestId(raw) {
656
625
  if (!raw || typeof raw !== "object") {
657
626
  return undefined;
@@ -659,7 +628,7 @@ function extractRequestId(raw) {
659
628
  const value = Object.getOwnPropertyDescriptor(raw, "requestId")?.value;
660
629
  return typeof value === "string" ? value : undefined;
661
630
  }
662
- function logProviderError(logger, provider, kind, route, requestId, error, status, cost) {
631
+ function logProviderError(logger, provider, kind, route, requestId, error, status, cost, declaredErrorCode) {
663
632
  const code = isProviderError(error)
664
633
  ? (error.code ?? "provider_error")
665
634
  : error instanceof z.ZodError
@@ -669,12 +638,13 @@ function logProviderError(logger, provider, kind, route, requestId, error, statu
669
638
  : "internal_error";
670
639
  const errorClass = error instanceof Error ? error.name : typeof error;
671
640
  const message = error instanceof Error ? error.message : String(error);
672
- const details = errorObservabilityDetails(error);
641
+ const details = errorObservabilityDetails(error, declaredErrorCode);
673
642
  const isUnregisteredProviderErrorCode = status === 500 &&
674
643
  isProviderError(error) &&
675
644
  !isValidationError(error) &&
676
645
  typeof error.code === "string" &&
677
- !SDK_OWNED_PROVIDER_ERROR_CODES.has(error.code);
646
+ !SDK_OWNED_PROVIDER_ERROR_CODES.has(error.code) &&
647
+ declaredErrorCode === undefined;
678
648
  const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
679
649
  emit({
680
650
  level: status >= 500 ? "error" : "warn",
@@ -1270,6 +1240,7 @@ export function createServerApp(provider, options = {}) {
1270
1240
  validateStatefulServerConfig(options);
1271
1241
  const app = new Hono();
1272
1242
  const logger = options.logger ?? defaultProviderServerLogger;
1243
+ const operationErrorCodes = buildOperationErrorCodeLookup(provider);
1273
1244
  const statefulForwardingReplayCache = new StatefulForwardingReplayCache(options.statefulForwarding?.replayCacheMaxEntries ??
1274
1245
  DEFAULT_STATEFUL_FORWARDING_REPLAY_CACHE_MAX_ENTRIES);
1275
1246
  const state = options.state ??
@@ -1303,6 +1274,7 @@ export function createServerApp(provider, options = {}) {
1303
1274
  app.post(STATEFUL_INTERNAL_OPERATIONS_ROUTE, async (c) => {
1304
1275
  let rawBodyText = "";
1305
1276
  let rawBody;
1277
+ let operationId;
1306
1278
  const operation = "stateful-internal";
1307
1279
  const requestCost = startRequestCost();
1308
1280
  try {
@@ -1388,7 +1360,7 @@ export function createServerApp(provider, options = {}) {
1388
1360
  });
1389
1361
  }
1390
1362
  const request = operationRequestFromForwardingEnvelope(envelope);
1391
- const operationId = envelope.operationId;
1363
+ operationId = envelope.operationId;
1392
1364
  const ctx = createProviderContext(provider, request, operationId, options, state);
1393
1365
  if (deadlineAtMs !== undefined && deadlineAtMs <= Date.now()) {
1394
1366
  throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt);
@@ -1405,13 +1377,14 @@ export function createServerApp(provider, options = {}) {
1405
1377
  return c.json({ data: output });
1406
1378
  }
1407
1379
  catch (error) {
1408
- const status = toStatusCode(error);
1380
+ const declaredErrorCode = declaredErrorCodeFor(error, operationId, operationErrorCodes);
1381
+ const status = toStatusCode(error, declaredErrorCode);
1409
1382
  if (isProviderError(error) && error.code === "STATEFUL_FORWARDING_REPLAY_CACHE_FULL") {
1410
1383
  c.header("Retry-After", String(STATEFUL_FORWARDING_REPLAY_RETRY_AFTER_SECONDS));
1411
1384
  }
1412
1385
  const requestId = extractRequestId(rawBody);
1413
- logProviderError(logger, provider, "operation", operation, requestId, error, status, finishRequestCost(requestCost));
1414
- return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
1386
+ logProviderError(logger, provider, "operation", operationId || operation, requestId, error, status, finishRequestCost(requestCost), declaredErrorCode);
1387
+ return responseWithErrorObservability(c.json(toErrorResponse(error, requestId, declaredErrorCode), status), error, declaredErrorCode);
1415
1388
  }
1416
1389
  });
1417
1390
  app.post("/v1/:operation", async (c) => {
@@ -1439,13 +1412,14 @@ export function createServerApp(provider, options = {}) {
1439
1412
  return c.json(response);
1440
1413
  }
1441
1414
  catch (error) {
1442
- const status = toStatusCode(error);
1415
+ const declaredErrorCode = declaredErrorCodeFor(error, operation, operationErrorCodes);
1416
+ const status = toStatusCode(error, declaredErrorCode);
1443
1417
  const requestId = extractRequestId(rawBody);
1444
- logProviderError(logger, provider, "operation", operation, requestId, error, status, finishRequestCost(requestCost));
1418
+ logProviderError(logger, provider, "operation", operation, requestId, error, status, finishRequestCost(requestCost), declaredErrorCode);
1445
1419
  const telemetryHeader = proxyTelemetry.toHeaderValue();
1446
1420
  if (telemetryHeader)
1447
1421
  c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
1448
- return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
1422
+ return responseWithErrorObservability(c.json(toErrorResponse(error, requestId, declaredErrorCode), status), error, declaredErrorCode);
1449
1423
  }
1450
1424
  });
1451
1425
  app.post("/auth/start", async (c) => {
package/dist/types.d.ts CHANGED
@@ -610,9 +610,11 @@ export interface HealthMonitorProbeOverride {
610
610
  /** Optional degraded threshold override for generated registry probes. */
611
611
  degradedThresholdMs?: number;
612
612
  }
613
+ export declare const VALID_OPERATION_ERROR_STATUSES: readonly [400, 401, 404, 429, 500, 502, 503, 504];
614
+ export type ProviderErrorStatus = (typeof VALID_OPERATION_ERROR_STATUSES)[number];
613
615
  export interface OperationErrorCode {
614
616
  code: string;
615
- status?: number;
617
+ status?: ProviderErrorStatus;
616
618
  description: string;
617
619
  retryable?: boolean;
618
620
  }
package/dist/types.js CHANGED
@@ -32,6 +32,7 @@ export const HEALTH_CHECK_TIMEOUT_MS_MIN = 1;
32
32
  export const HEALTH_CHECK_TIMEOUT_MS_MAX = 60_000;
33
33
  export const HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MIN = 1;
34
34
  export const HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX = 60_000;
35
+ export const VALID_OPERATION_ERROR_STATUSES = [400, 401, 404, 429, 500, 502, 503, 504];
35
36
  export const HttpRetryPreset = {
36
37
  Off: "off",
37
38
  TransportTransient: "transport_transient",
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.2.0-beta.15",
2
+ "version": "2.2.0-beta.16",
3
3
  "name": "@apifuse/provider-sdk",
4
4
  "private": false,
5
5
  "type": "module",
package/src/define.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import ms from "ms";
2
2
 
3
+ import { SDK_RUNTIME_OWNED_ERROR_CODES } from "./error-resolution.js";
3
4
  import { ProviderError, ValidationError } from "./errors.js";
4
5
  import {
5
6
  NativeEgressPolicyValidationError,
@@ -56,6 +57,7 @@ import {
56
57
  STREAM_IDLE_TIMEOUT_MS_MIN,
57
58
  STREAM_MAX_DURATION_MS_MAX,
58
59
  STREAM_MAX_DURATION_MS_MIN,
60
+ VALID_OPERATION_ERROR_STATUSES,
59
61
  } from "./types.js";
60
62
 
61
63
  type ProviderImplementationSourceAccess =
@@ -800,6 +802,36 @@ function validateOperationObservability(
800
802
  }
801
803
  }
802
804
 
805
+ function validateOperationErrorCodes(
806
+ providerId: string,
807
+ operations: Record<string, ProviderOperation>,
808
+ ): void {
809
+ for (const [operationName, operation] of Object.entries(operations)) {
810
+ for (const [index, errorCode] of (operation.docs?.errorCodes ?? []).entries()) {
811
+ if (
812
+ errorCode.status !== undefined &&
813
+ !VALID_OPERATION_ERROR_STATUSES.some((status) => status === errorCode.status)
814
+ ) {
815
+ const field = `operations.${operationName}.docs.errorCodes[${index}].status`;
816
+ throw new ValidationError(
817
+ `Provider "${providerId}" has invalid ${field}: ${String(errorCode.status)} is not an emittable provider error status.`,
818
+ {
819
+ fix: `Set ${field} to one of ${VALID_OPERATION_ERROR_STATUSES.join(", ")}, or omit it.`,
820
+ },
821
+ );
822
+ }
823
+ if (
824
+ errorCode.status !== undefined &&
825
+ SDK_RUNTIME_OWNED_ERROR_CODES.has(errorCode.code)
826
+ ) {
827
+ console.warn(
828
+ `[provider-sdk] Provider "${providerId}" operation "${operationName}" declares status ${errorCode.status} for SDK-owned error code "${errorCode.code}"; the declared status is documentation-only and will be ignored at runtime.`,
829
+ );
830
+ }
831
+ }
832
+ }
833
+ }
834
+
803
835
  const JSON_TRANSPORT_FIELDS = new Set(["kind"]);
804
836
  const SSE_TRANSPORT_FIELDS = new Set([
805
837
  "kind",
@@ -2185,6 +2217,7 @@ export function defineProvider<
2185
2217
  validateOperationIds(config.id, config.operations);
2186
2218
  validateOperationAnnotations(config.id, config.operations);
2187
2219
  validateOperationObservability(config.id, config.operations);
2220
+ validateOperationErrorCodes(config.id, config.operations);
2188
2221
  validateOperationTransports(config.id, config.operations);
2189
2222
  validateOperationContracts(config.id, config.operations);
2190
2223
  validateToolRouterMetadata(config.id, config.operations);
@@ -0,0 +1,91 @@
1
+ // This set suppresses the unregistered-provider-error-code signal for codes
2
+ // intentionally emitted by SDK paths. It is not the complete authority for
3
+ // runtime error resolution: branded errors and additional canonical SDK codes
4
+ // must also remain immune to provider-declared status/retryability overrides.
5
+ export const SDK_OWNED_PROVIDER_ERROR_CODES = new Set([
6
+ "MISSING_SECRET",
7
+ "AUTH_PROMPT_UNAVAILABLE",
8
+ "BROWSER_CDP_POOL_REQUIRED",
9
+ "BROWSER_RUNTIME_UNSUPPORTED",
10
+ "STEALTH_RUNTIME_UNSUPPORTED",
11
+ "SSE_EVENT_UNDECLARED",
12
+ "STREAM_EVENT_TOO_LARGE",
13
+ "STREAM_CHUNK_TOO_LARGE",
14
+ "SSE_RESULT_UNSUPPORTED",
15
+ "STREAM_RESULT_UNSUPPORTED",
16
+ "AUTH_FLOW_NOT_CONFIGURED",
17
+ "refresh_not_supported",
18
+ "RUNTIME_UNSUPPORTED",
19
+ "PROVIDER_STATE_UNSUPPORTED",
20
+ "CHOICE_TOKEN_MASTER_SECRET_NOT_CONFIGURED",
21
+ "CHOICE_STATE_PAYLOAD_TOO_LARGE",
22
+ "CHOICE_STATE_UNAVAILABLE",
23
+ "CHOICE_CONTEXT_REQUIRED",
24
+ "unsupported_stealth_cookie_store_version",
25
+ "provider_secret_error",
26
+ "credential_key_error",
27
+ "credential_mode_error",
28
+ "flow_expired",
29
+ "turn_validation_error",
30
+ "context_access_error",
31
+ "UNSUPPORTED_STT_OPTION",
32
+ "INVALID_STT_AUDIO",
33
+ "STT_AUDIO_TOO_LARGE",
34
+ "STT_UPSTREAM_FAILED",
35
+ "INVALID_STT_VERIFICATION_CODE_OPTIONS",
36
+ "NO_CODE_FOUND",
37
+ "AMBIGUOUS_CODE",
38
+ "retry_invalid_policy",
39
+ "retry_unsafe_method",
40
+ "stealth_cookie_store_serialize_failed",
41
+ "response_too_large",
42
+ "transport_stream_unavailable",
43
+ "transport_invalid_method",
44
+ "http_transport_override_unsupported",
45
+ "http_redirect_policy_invalid",
46
+ "http_redirect_stopped",
47
+ "http_redirect_max_hops",
48
+ "http_redirect_missing_location",
49
+ "http_redirect_loop",
50
+ "transport_invalid_url",
51
+ "retry_exhausted",
52
+ "auth_abort_unsafe_data",
53
+ "credentials_auth_missing_credential_keys",
54
+ "credentials_auth_missing_credential",
55
+ "credentials_auth_invalid_login_result",
56
+ "credentials_auth_unknown_challenge",
57
+ "credentials_auth_unknown_pending_challenge",
58
+ "STATEFUL_FORWARDING_NOT_CONFIGURED",
59
+ "STATEFUL_FORWARDING_SIGNATURE_MISSING",
60
+ "STATEFUL_FORWARDING_NONCE_INVALID",
61
+ "STATEFUL_FORWARDING_TIMESTAMP_INVALID",
62
+ "STATEFUL_FORWARDING_SIGNATURE_INVALID",
63
+ "STATEFUL_FORWARDING_REPLAY_DETECTED",
64
+ "STATEFUL_FORWARDING_REPLAY_CACHE_FULL",
65
+ "STATEFUL_FORWARDING_ENVELOPE_INVALID",
66
+ "STATEFUL_FORWARDING_PROVIDER_MISMATCH",
67
+ "STATEFUL_FORWARDING_SOURCE_POD_MISMATCH",
68
+ "STATEFUL_FORWARDING_OWNER_FENCE_INVALID",
69
+ "STATEFUL_FORWARDING_REQUEST_FAILED",
70
+ "STATEFUL_FORWARDING_CONTEXT_MISSING",
71
+ "STATEFUL_FORWARDING_BAD_RESPONSE",
72
+ "STATEFUL_INTERNAL_EXECUTOR_NOT_CONFIGURED",
73
+ "STATEFUL_FILE_FORWARDING_UNSUPPORTED",
74
+ "STATEFUL_CONTROL_PLANE_OPERATION_AMBIGUOUS",
75
+ "STATEFUL_CONTROL_PLANE_REQUEST_FAILED",
76
+ "STATEFUL_CONTROL_PLANE_HTTP_ERROR",
77
+ "STATEFUL_CONTROL_PLANE_INVALID_RESPONSE",
78
+ ]);
79
+
80
+ // Complete code authority for provider-declared runtime resolution. Keep this
81
+ // separate from signal suppression: declarations may document these codes, but
82
+ // their status and retryability can never override the SDK's canonical result.
83
+ export const SDK_RUNTIME_OWNED_ERROR_CODES = new Set([
84
+ ...SDK_OWNED_PROVIDER_ERROR_CODES,
85
+ "reauth_required",
86
+ "STT_UNAVAILABLE",
87
+ "UNSUPPORTED_STT_BACKEND",
88
+ "OUTPUT_VALIDATION_FAILED",
89
+ "NOT_FOUND",
90
+ "not_found",
91
+ ]);
package/src/index.ts CHANGED
@@ -230,6 +230,7 @@ export type {
230
230
  OperationDeprecationMetadata,
231
231
  OperationDocMeta,
232
232
  OperationErrorCode,
233
+ ProviderErrorStatus,
233
234
  OperationHandlerResult,
234
235
  OperationInputExample,
235
236
  OperationLifecycle,
package/src/provider.ts CHANGED
@@ -123,6 +123,7 @@ export type {
123
123
  OperationDefinition,
124
124
  OperationDocMeta,
125
125
  OperationErrorCode,
126
+ ProviderErrorStatus,
126
127
  OperationInputExample,
127
128
  OperationLifecycle,
128
129
  OperationObservabilityConfig,
@@ -4,6 +4,10 @@ import { join } from "node:path";
4
4
  import { Hono } from "hono";
5
5
  import { z } from "zod";
6
6
  import { AuthAbortError, createAuthFlowHelpers } from "../auth.js";
7
+ import {
8
+ SDK_OWNED_PROVIDER_ERROR_CODES,
9
+ SDK_RUNTIME_OWNED_ERROR_CODES,
10
+ } from "../error-resolution.js";
7
11
  import {
8
12
  AuthError,
9
13
  isProviderError,
@@ -79,8 +83,10 @@ import type {
79
83
  FlowContextStore,
80
84
  HttpRetrySummary,
81
85
  OperationDefinition,
86
+ OperationErrorCode,
82
87
  OperationHttpStreamTransport,
83
88
  OperationSseTransport,
89
+ ProviderErrorStatus,
84
90
  ProviderContext,
85
91
  ProviderDefinition,
86
92
  ProviderProxyPolicy,
@@ -89,6 +95,7 @@ import type {
89
95
  StealthClient,
90
96
  SttContext,
91
97
  } from "../types.js";
98
+ import { VALID_OPERATION_ERROR_STATUSES } from "../types.js";
92
99
  import {
93
100
  createSelfTestApp,
94
101
  createSelfTestAuthFlowInvoke,
@@ -644,8 +651,12 @@ function zodDetails(error: z.ZodError): Array<{
644
651
  }));
645
652
  }
646
653
 
647
- function toErrorResponse(error: unknown, requestId?: string): OperationErrorResponse {
648
- const observability = errorObservabilityDetails(error);
654
+ function toErrorResponse(
655
+ error: unknown,
656
+ requestId?: string,
657
+ declaredErrorCode?: OperationErrorCode,
658
+ ): OperationErrorResponse {
659
+ const observability = errorObservabilityDetails(error, declaredErrorCode);
649
660
  if (error instanceof StatefulRoutingDeadlineError) {
650
661
  return {
651
662
  error: {
@@ -709,7 +720,13 @@ function toErrorResponse(error: unknown, requestId?: string): OperationErrorResp
709
720
  // narrowing from a ProviderError-typed value would collapse the negative branch
710
721
  // to `never`. Narrowing from unknown avoids that while still recognizing errors
711
722
  // from a duplicate SDK module instance.
712
- function providerObservabilityDetails(error: unknown): ErrorObservabilityDetails | undefined {
723
+ function providerObservabilityDetails(
724
+ error: unknown,
725
+ declaredErrorCode?: OperationErrorCode,
726
+ ): ErrorObservabilityDetails | undefined {
727
+ const declaredRetryable = sdkOwnsErrorResolution(error)
728
+ ? undefined
729
+ : declaredErrorCode?.retryable;
713
730
  // Session-expiry surfaces the credential_expired category + the opt-in
714
731
  // retryable signal so Gateway/Credential Service can refresh and re-drive the
715
732
  // operation (see design.md §4.3 D3). Without this branch the auth error would
@@ -719,7 +736,7 @@ function providerObservabilityDetails(error: unknown): ErrorObservabilityDetails
719
736
  return {
720
737
  category: error.options?.category ?? "credential_expired",
721
738
  taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
722
- retryable: error.options?.retryable ?? false,
739
+ retryable: error.options?.retryable ?? declaredRetryable ?? false,
723
740
  };
724
741
  }
725
742
  // Missing-secret errors carry the canonical credential_unavailable category
@@ -731,7 +748,7 @@ function providerObservabilityDetails(error: unknown): ErrorObservabilityDetails
731
748
  return {
732
749
  category: error.options?.category ?? "credential_unavailable",
733
750
  taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
734
- retryable: error.options?.retryable ?? false,
751
+ retryable: error.options?.retryable ?? declaredRetryable ?? false,
735
752
  };
736
753
  }
737
754
  if (!isTransportError(error)) {
@@ -766,18 +783,27 @@ function providerObservabilityDetails(error: unknown): ErrorObservabilityDetails
766
783
  };
767
784
  }
768
785
 
769
- function errorObservabilityDetails(error: unknown): ErrorObservabilityDetails {
770
- const providerDetails = providerObservabilityDetails(error);
786
+ function errorObservabilityDetails(
787
+ error: unknown,
788
+ declaredErrorCode?: OperationErrorCode,
789
+ ): ErrorObservabilityDetails {
790
+ const effectiveDeclaration = sdkOwnsErrorResolution(error) ? undefined : declaredErrorCode;
791
+ const providerDetails = providerObservabilityDetails(error, effectiveDeclaration);
771
792
  if (providerDetails) return providerDetails;
772
793
 
773
794
  if (error instanceof z.ZodError || isValidationError(error)) {
795
+ const declaredStatus = effectiveDeclaration?.status;
774
796
  return {
775
797
  category:
776
798
  isProviderError(error) && error.options?.category
777
799
  ? error.options.category
778
- : "input_validation",
800
+ : isEmittableErrorStatus(declaredStatus) && declaredStatus >= 500
801
+ ? "provider_error"
802
+ : "input_validation",
779
803
  taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
780
- retryable: isProviderError(error) ? (error.options?.retryable ?? false) : false,
804
+ retryable: isProviderError(error)
805
+ ? (error.options?.retryable ?? effectiveDeclaration?.retryable ?? false)
806
+ : false,
781
807
  };
782
808
  }
783
809
 
@@ -793,7 +819,7 @@ function errorObservabilityDetails(error: unknown): ErrorObservabilityDetails {
793
819
  return {
794
820
  category: error.options?.category ?? "provider_error",
795
821
  taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
796
- retryable: error.options?.retryable ?? false,
822
+ retryable: error.options?.retryable ?? effectiveDeclaration?.retryable ?? false,
797
823
  };
798
824
  }
799
825
 
@@ -804,9 +830,16 @@ function errorObservabilityDetails(error: unknown): ErrorObservabilityDetails {
804
830
  };
805
831
  }
806
832
 
807
- function responseWithErrorObservability(response: Response, error: unknown): Response {
833
+ function responseWithErrorObservability(
834
+ response: Response,
835
+ error: unknown,
836
+ declaredErrorCode?: OperationErrorCode,
837
+ ): Response {
808
838
  const headers = new Headers(response.headers);
809
- headers.set(ERROR_OBSERVABILITY_HEADER, JSON.stringify(errorObservabilityDetails(error)));
839
+ headers.set(
840
+ ERROR_OBSERVABILITY_HEADER,
841
+ JSON.stringify(errorObservabilityDetails(error, declaredErrorCode)),
842
+ );
810
843
  return new Response(response.body, {
811
844
  status: response.status,
812
845
  statusText: response.statusText,
@@ -838,7 +871,14 @@ function publicProviderErrorMessage(error: ProviderError): string {
838
871
  return error.message;
839
872
  }
840
873
 
841
- function toStatusCode(error: unknown): 400 | 401 | 404 | 429 | 500 | 502 | 503 | 504 {
874
+ function isEmittableErrorStatus(value: unknown): value is ProviderErrorStatus {
875
+ return (
876
+ typeof value === "number" &&
877
+ VALID_OPERATION_ERROR_STATUSES.some((status) => status === value)
878
+ );
879
+ }
880
+
881
+ function toStatusCode(error: unknown, declaredErrorCode?: OperationErrorCode): ProviderErrorStatus {
842
882
  if (error instanceof z.ZodError) {
843
883
  return 400;
844
884
  }
@@ -846,6 +886,12 @@ function toStatusCode(error: unknown): 400 | 401 | 404 | 429 | 500 | 502 | 503 |
846
886
  return 504;
847
887
  }
848
888
  if (isProviderError(error)) {
889
+ if (
890
+ !sdkOwnsErrorResolution(error) &&
891
+ isEmittableErrorStatus(declaredErrorCode?.status)
892
+ ) {
893
+ return declaredErrorCode.status;
894
+ }
849
895
  switch (error.code) {
850
896
  case "AUTH_REQUIRED":
851
897
  case "reauth_required":
@@ -883,82 +929,39 @@ function toStatusCode(error: unknown): 400 | 401 | 404 | 429 | 500 | 502 | 503 |
883
929
  return 500;
884
930
  }
885
931
 
886
- // Codes emitted by SDK-owned paths must never be attributed to provider
887
- // authors by the unregistered-code signal, even when their intentional status
888
- // is 500. Provider-authored codes not in this registry retain the signal.
889
- const SDK_OWNED_PROVIDER_ERROR_CODES = new Set([
890
- "AUTH_PROMPT_UNAVAILABLE",
891
- "BROWSER_CDP_POOL_REQUIRED",
892
- "BROWSER_RUNTIME_UNSUPPORTED",
893
- "STEALTH_RUNTIME_UNSUPPORTED",
894
- "SSE_EVENT_UNDECLARED",
895
- "STREAM_EVENT_TOO_LARGE",
896
- "STREAM_CHUNK_TOO_LARGE",
897
- "SSE_RESULT_UNSUPPORTED",
898
- "STREAM_RESULT_UNSUPPORTED",
899
- "AUTH_FLOW_NOT_CONFIGURED",
900
- "refresh_not_supported",
901
- "RUNTIME_UNSUPPORTED",
902
- "PROVIDER_STATE_UNSUPPORTED",
903
- "CHOICE_TOKEN_MASTER_SECRET_NOT_CONFIGURED",
904
- "CHOICE_STATE_PAYLOAD_TOO_LARGE",
905
- "CHOICE_STATE_UNAVAILABLE",
906
- "CHOICE_CONTEXT_REQUIRED",
907
- "unsupported_stealth_cookie_store_version",
908
- "provider_secret_error",
909
- "credential_key_error",
910
- "credential_mode_error",
911
- "flow_expired",
912
- "turn_validation_error",
913
- "context_access_error",
914
- "UNSUPPORTED_STT_OPTION",
915
- "INVALID_STT_AUDIO",
916
- "STT_AUDIO_TOO_LARGE",
917
- "STT_UPSTREAM_FAILED",
918
- "INVALID_STT_VERIFICATION_CODE_OPTIONS",
919
- "NO_CODE_FOUND",
920
- "AMBIGUOUS_CODE",
921
- "retry_invalid_policy",
922
- "retry_unsafe_method",
923
- "stealth_cookie_store_serialize_failed",
924
- "response_too_large",
925
- "transport_stream_unavailable",
926
- "transport_invalid_method",
927
- "http_transport_override_unsupported",
928
- "http_redirect_policy_invalid",
929
- "http_redirect_stopped",
930
- "http_redirect_max_hops",
931
- "http_redirect_missing_location",
932
- "http_redirect_loop",
933
- "transport_invalid_url",
934
- "retry_exhausted",
935
- "auth_abort_unsafe_data",
936
- "credentials_auth_missing_credential_keys",
937
- "credentials_auth_missing_credential",
938
- "credentials_auth_invalid_login_result",
939
- "credentials_auth_unknown_challenge",
940
- "credentials_auth_unknown_pending_challenge",
941
- "STATEFUL_FORWARDING_NOT_CONFIGURED",
942
- "STATEFUL_FORWARDING_SIGNATURE_MISSING",
943
- "STATEFUL_FORWARDING_NONCE_INVALID",
944
- "STATEFUL_FORWARDING_TIMESTAMP_INVALID",
945
- "STATEFUL_FORWARDING_SIGNATURE_INVALID",
946
- "STATEFUL_FORWARDING_REPLAY_DETECTED",
947
- "STATEFUL_FORWARDING_REPLAY_CACHE_FULL",
948
- "STATEFUL_FORWARDING_ENVELOPE_INVALID",
949
- "STATEFUL_FORWARDING_PROVIDER_MISMATCH",
950
- "STATEFUL_FORWARDING_SOURCE_POD_MISMATCH",
951
- "STATEFUL_FORWARDING_OWNER_FENCE_INVALID",
952
- "STATEFUL_FORWARDING_REQUEST_FAILED",
953
- "STATEFUL_FORWARDING_CONTEXT_MISSING",
954
- "STATEFUL_FORWARDING_BAD_RESPONSE",
955
- "STATEFUL_INTERNAL_EXECUTOR_NOT_CONFIGURED",
956
- "STATEFUL_FILE_FORWARDING_UNSUPPORTED",
957
- "STATEFUL_CONTROL_PLANE_OPERATION_AMBIGUOUS",
958
- "STATEFUL_CONTROL_PLANE_REQUEST_FAILED",
959
- "STATEFUL_CONTROL_PLANE_HTTP_ERROR",
960
- "STATEFUL_CONTROL_PLANE_INVALID_RESPONSE",
961
- ]);
932
+ function sdkOwnsErrorResolution(error: unknown): boolean {
933
+ if (isSessionExpiredError(error)) return true;
934
+ if (isTransportError(error)) return true;
935
+ if (error instanceof z.ZodError) return true;
936
+ if (error instanceof StatefulRoutingDeadlineError) return true;
937
+ return (
938
+ isProviderError(error) &&
939
+ typeof error.code === "string" &&
940
+ SDK_RUNTIME_OWNED_ERROR_CODES.has(error.code)
941
+ );
942
+ }
943
+
944
+ type OperationErrorCodeLookup = ReadonlyMap<string, ReadonlyMap<string, OperationErrorCode>>;
945
+
946
+ function buildOperationErrorCodeLookup(provider: ProviderDefinition): OperationErrorCodeLookup {
947
+ return new Map(
948
+ Object.entries(provider.operations).flatMap(([operationId, operation]) => {
949
+ const errorCodes = operation.docs?.errorCodes;
950
+ return errorCodes?.length
951
+ ? [[operationId, new Map(errorCodes.map((entry) => [entry.code, entry]))] as const]
952
+ : [];
953
+ }),
954
+ );
955
+ }
956
+
957
+ function declaredErrorCodeFor(
958
+ error: unknown,
959
+ operationId: string | undefined,
960
+ lookup: OperationErrorCodeLookup,
961
+ ): OperationErrorCode | undefined {
962
+ if (!operationId || !isProviderError(error) || typeof error.code !== "string") return undefined;
963
+ return lookup.get(operationId)?.get(error.code);
964
+ }
962
965
 
963
966
  function extractRequestId(raw: unknown): string | undefined {
964
967
  if (!raw || typeof raw !== "object") {
@@ -978,6 +981,7 @@ function logProviderError(
978
981
  error: unknown,
979
982
  status: number,
980
983
  cost: ProviderRequestCost,
984
+ declaredErrorCode?: OperationErrorCode,
981
985
  ): void {
982
986
  const code = isProviderError(error)
983
987
  ? (error.code ?? "provider_error")
@@ -988,13 +992,14 @@ function logProviderError(
988
992
  : "internal_error";
989
993
  const errorClass = error instanceof Error ? error.name : typeof error;
990
994
  const message = error instanceof Error ? error.message : String(error);
991
- const details = errorObservabilityDetails(error);
995
+ const details = errorObservabilityDetails(error, declaredErrorCode);
992
996
  const isUnregisteredProviderErrorCode =
993
997
  status === 500 &&
994
998
  isProviderError(error) &&
995
999
  !isValidationError(error) &&
996
1000
  typeof error.code === "string" &&
997
- !SDK_OWNED_PROVIDER_ERROR_CODES.has(error.code);
1001
+ !SDK_OWNED_PROVIDER_ERROR_CODES.has(error.code) &&
1002
+ declaredErrorCode === undefined;
998
1003
  const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
999
1004
  emit({
1000
1005
  level: status >= 500 ? "error" : "warn",
@@ -1729,6 +1734,7 @@ export function createServerApp(
1729
1734
  validateStatefulServerConfig(options);
1730
1735
  const app = new Hono();
1731
1736
  const logger = options.logger ?? defaultProviderServerLogger;
1737
+ const operationErrorCodes = buildOperationErrorCodeLookup(provider);
1732
1738
  const statefulForwardingReplayCache = new StatefulForwardingReplayCache(
1733
1739
  options.statefulForwarding?.replayCacheMaxEntries ??
1734
1740
  DEFAULT_STATEFUL_FORWARDING_REPLAY_CACHE_MAX_ENTRIES,
@@ -1774,6 +1780,7 @@ export function createServerApp(
1774
1780
  app.post(STATEFUL_INTERNAL_OPERATIONS_ROUTE, async (c) => {
1775
1781
  let rawBodyText = "";
1776
1782
  let rawBody: unknown;
1783
+ let operationId: string | undefined;
1777
1784
  const operation = "stateful-internal";
1778
1785
  const requestCost = startRequestCost();
1779
1786
  try {
@@ -1884,7 +1891,7 @@ export function createServerApp(
1884
1891
  });
1885
1892
  }
1886
1893
  const request = operationRequestFromForwardingEnvelope(envelope);
1887
- const operationId = envelope.operationId;
1894
+ operationId = envelope.operationId;
1888
1895
  const ctx = createProviderContext(provider, request, operationId, options, state);
1889
1896
  if (deadlineAtMs !== undefined && deadlineAtMs <= Date.now()) {
1890
1897
  throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt as string);
@@ -1908,7 +1915,8 @@ export function createServerApp(
1908
1915
  );
1909
1916
  return c.json({ data: output });
1910
1917
  } catch (error) {
1911
- const status = toStatusCode(error);
1918
+ const declaredErrorCode = declaredErrorCodeFor(error, operationId, operationErrorCodes);
1919
+ const status = toStatusCode(error, declaredErrorCode);
1912
1920
  if (isProviderError(error) && error.code === "STATEFUL_FORWARDING_REPLAY_CACHE_FULL") {
1913
1921
  c.header("Retry-After", String(STATEFUL_FORWARDING_REPLAY_RETRY_AFTER_SECONDS));
1914
1922
  }
@@ -1917,15 +1925,17 @@ export function createServerApp(
1917
1925
  logger,
1918
1926
  provider,
1919
1927
  "operation",
1920
- operation,
1928
+ operationId || operation,
1921
1929
  requestId,
1922
1930
  error,
1923
1931
  status,
1924
1932
  finishRequestCost(requestCost),
1933
+ declaredErrorCode,
1925
1934
  );
1926
1935
  return responseWithErrorObservability(
1927
- c.json(toErrorResponse(error, requestId), status),
1936
+ c.json(toErrorResponse(error, requestId, declaredErrorCode), status),
1928
1937
  error,
1938
+ declaredErrorCode,
1929
1939
  );
1930
1940
  }
1931
1941
  });
@@ -1976,7 +1986,8 @@ export function createServerApp(
1976
1986
  );
1977
1987
  return c.json(response);
1978
1988
  } catch (error) {
1979
- const status = toStatusCode(error);
1989
+ const declaredErrorCode = declaredErrorCodeFor(error, operation, operationErrorCodes);
1990
+ const status = toStatusCode(error, declaredErrorCode);
1980
1991
  const requestId = extractRequestId(rawBody);
1981
1992
  logProviderError(
1982
1993
  logger,
@@ -1987,12 +1998,14 @@ export function createServerApp(
1987
1998
  error,
1988
1999
  status,
1989
2000
  finishRequestCost(requestCost),
2001
+ declaredErrorCode,
1990
2002
  );
1991
2003
  const telemetryHeader = proxyTelemetry.toHeaderValue();
1992
2004
  if (telemetryHeader) c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
1993
2005
  return responseWithErrorObservability(
1994
- c.json(toErrorResponse(error, requestId), status),
2006
+ c.json(toErrorResponse(error, requestId, declaredErrorCode), status),
1995
2007
  error,
2008
+ declaredErrorCode,
1996
2009
  );
1997
2010
  }
1998
2011
  });
package/src/types.ts CHANGED
@@ -719,9 +719,13 @@ export interface HealthMonitorProbeOverride {
719
719
  degradedThresholdMs?: number;
720
720
  }
721
721
 
722
+ export const VALID_OPERATION_ERROR_STATUSES = [400, 401, 404, 429, 500, 502, 503, 504] as const;
723
+
724
+ export type ProviderErrorStatus = (typeof VALID_OPERATION_ERROR_STATUSES)[number];
725
+
722
726
  export interface OperationErrorCode {
723
727
  code: string;
724
- status?: number;
728
+ status?: ProviderErrorStatus;
725
729
  description: string;
726
730
  retryable?: boolean;
727
731
  }