@apifuse/provider-sdk 2.2.0-beta.18 → 2.2.0-beta.20

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @apifuse/provider-sdk Changelog
2
2
 
3
+ ## 2.2.0-beta.20
4
+
5
+ - Release candidate for main commit 8043d2ef0047e431aace750d8abca2e1149ec1d6.
6
+
7
+ ## 2.2.0-beta.19
8
+
9
+ - Release candidate for main commit fd93ae68d0472fae68d908312249163373ec5d22.
10
+
3
11
  ## 2.2.0-beta.18
4
12
 
5
13
  - Release candidate for main commit cebaf4b994918d2641c0fe6681fbffd3a3284c5d.
@@ -102,6 +110,7 @@
102
110
 
103
111
  ## Unreleased
104
112
 
113
+ - **honest-provider-error-contract (phase 2):** `UPSTREAM_REJECTED` is a registered code family serving HTTP 409 with `retryable: false` — deterministic upstream business refusals are no longer 502s. Operation-declared `docs.errorCodes` may now use 409/410/422. Public error envelopes carry a `source` field (`client` | `upstream_rule` | `upstream_failure` | `apifuse`) derived from the observability category. Taxonomy version bumps to `2026-08-07` with the `upstream_rejected`, `dependency_unavailable`, `unsupported_transport`, and `client_cancelled` categories (409/410/422 map to `upstream_rejected`), matching the platform monorepo SoT. The `unregistered_provider_error_code` signal now carries a `signalFix` pointing at `docs.errorCodes` declaration.
105
114
  - **Breaking for custom native gateway adapters:** `NativeGatewayProxySynthesisInput` now includes an injected `credentials` resolver and selected `protocol`; synthesizers may return promises and structured skip reasons, and `resolveNativeGatewayProxy` is async. Default callers retain env-backed behavior. Native transport now supports both HTTP CONNECT and SOCKS5, defaults per vendor with an explicit runtime override, registers smartproxy allocation ahead of nodemaven when declared in that order, and reports every exhausted vendor reason without exposing proxy credentials.
106
115
  - 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.
107
116
  - 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.
package/dist/index.d.ts CHANGED
@@ -8,6 +8,7 @@ export { centered, delayed, defineHealthJourney, defineOperation, defineProvider
8
8
  export type { DevServerOptions } from "./dev.js";
9
9
  export { createDevServer, startDevServer } from "./dev.js";
10
10
  export * from "./errors.js";
11
+ export * from "./observability.js";
11
12
  export * from "./user-input.js";
12
13
  export * from "./i18n/index.js";
13
14
  export { type LintDiagnostic, lintOperation, lintProvider, } from "./lint.js";
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ export { canonicalJson, digestProviderContract, extractProviderContract, PROVIDE
7
7
  export { centered, delayed, defineHealthJourney, defineOperation, defineProvider, defineSmsOtpMatcher, defineStreamOperation, every, } from "./define.js";
8
8
  export { createDevServer, startDevServer } from "./dev.js";
9
9
  export * from "./errors.js";
10
+ export * from "./observability.js";
10
11
  export * from "./user-input.js";
11
12
  export * from "./i18n/index.js";
12
13
  export { lintOperation, lintProvider, } from "./lint.js";
@@ -1,5 +1,8 @@
1
- export declare const PROVIDER_OBSERVABILITY_TAXONOMY_VERSION = "2026-05-26";
2
- export declare const PROVIDER_ERROR_CATEGORIES: readonly ["ok", "timeout", "network", "upstream_http", "upstream_rate_limited", "upstream_auth", "upstream_schema_drift", "proxy_pool", "anti_bot_blocked", "credential_expired", "credential_unavailable", "input_validation", "output_validation", "provider_error", "internal_error", "unclassified"];
1
+ export declare const PROVIDER_OBSERVABILITY_TAXONOMY_VERSION = "2026-08-07";
2
+ export declare const PROVIDER_ERROR_CATEGORIES: readonly ["ok", "timeout", "network", "upstream_http", "upstream_rate_limited", "upstream_auth", "upstream_rejected", "upstream_schema_drift", "proxy_pool", "anti_bot_blocked", "credential_expired", "credential_unavailable", "input_validation", "output_validation", "provider_error", "internal_error", "dependency_unavailable", "unsupported_transport", "client_cancelled", "unclassified"];
3
3
  export type ProviderErrorCategory = (typeof PROVIDER_ERROR_CATEGORIES)[number];
4
4
  export declare function categoryForStatus(status: number): ProviderErrorCategory;
5
5
  export declare function isRetryableCategory(category: ProviderErrorCategory): boolean;
6
+ export declare const PROVIDER_ERROR_SOURCES: readonly ["client", "upstream_rule", "upstream_failure", "apifuse"];
7
+ export type ProviderErrorSource = (typeof PROVIDER_ERROR_SOURCES)[number];
8
+ export declare function sourceForCategory(category: ProviderErrorCategory): ProviderErrorSource;
@@ -1,4 +1,5 @@
1
- export const PROVIDER_OBSERVABILITY_TAXONOMY_VERSION = "2026-05-26";
1
+ // Mirrors packages/provider-observability in the platform monorepo (SoT).
2
+ export const PROVIDER_OBSERVABILITY_TAXONOMY_VERSION = "2026-08-07";
2
3
  export const PROVIDER_ERROR_CATEGORIES = [
3
4
  "ok",
4
5
  "timeout",
@@ -6,6 +7,7 @@ export const PROVIDER_ERROR_CATEGORIES = [
6
7
  "upstream_http",
7
8
  "upstream_rate_limited",
8
9
  "upstream_auth",
10
+ "upstream_rejected",
9
11
  "upstream_schema_drift",
10
12
  "proxy_pool",
11
13
  "anti_bot_blocked",
@@ -15,6 +17,9 @@ export const PROVIDER_ERROR_CATEGORIES = [
15
17
  "output_validation",
16
18
  "provider_error",
17
19
  "internal_error",
20
+ "dependency_unavailable",
21
+ "unsupported_transport",
22
+ "client_cancelled",
18
23
  "unclassified",
19
24
  ];
20
25
  export function categoryForStatus(status) {
@@ -26,6 +31,10 @@ export function categoryForStatus(status) {
26
31
  return "upstream_rate_limited";
27
32
  if (status === 401 || status === 403)
28
33
  return "upstream_auth";
34
+ // provider-error-contract: 409/410/422 are the deterministic
35
+ // upstream-rejection status class — not a retryable upstream failure.
36
+ if (status === 409 || status === 410 || status === 422)
37
+ return "upstream_rejected";
29
38
  if (status >= 400)
30
39
  return "upstream_http";
31
40
  return "unclassified";
@@ -37,3 +46,41 @@ export function isRetryableCategory(category) {
37
46
  category === "upstream_http" ||
38
47
  category === "proxy_pool");
39
48
  }
49
+ // Who stopped the request (honest-provider-error-contract). A deliberately
50
+ // small public projection of the internal taxonomy:
51
+ // - client: the request itself is fixable by the caller
52
+ // - upstream_rule: the upstream service refused it under its own business
53
+ // rules (deterministic) or rate limits
54
+ // - upstream_failure: the upstream service malfunctioned — retryable
55
+ // - apifuse: an APIFuse-side fault (provider bug, platform dependency)
56
+ export const PROVIDER_ERROR_SOURCES = [
57
+ "client",
58
+ "upstream_rule",
59
+ "upstream_failure",
60
+ "apifuse",
61
+ ];
62
+ export function sourceForCategory(category) {
63
+ switch (category) {
64
+ case "input_validation":
65
+ case "credential_expired":
66
+ case "credential_unavailable":
67
+ case "client_cancelled":
68
+ return "client";
69
+ case "upstream_rejected":
70
+ case "upstream_rate_limited":
71
+ return "upstream_rule";
72
+ case "timeout":
73
+ case "network":
74
+ case "upstream_http":
75
+ case "upstream_auth":
76
+ case "upstream_schema_drift":
77
+ case "anti_bot_blocked":
78
+ return "upstream_failure";
79
+ default:
80
+ // ok never reaches error serialization; provider_error,
81
+ // internal_error, output_validation, proxy_pool,
82
+ // dependency_unavailable, unsupported_transport, unclassified are
83
+ // APIFuse-side faults.
84
+ return "apifuse";
85
+ }
86
+ }
@@ -87,6 +87,7 @@ export type ProviderServerLogEvent = (ProviderServerLogEventBase & {
87
87
  taxonomyVersion?: string;
88
88
  retryable?: boolean;
89
89
  signal?: "unregistered_provider_error_code";
90
+ signalFix?: string;
90
91
  issues?: Array<{
91
92
  path: string;
92
93
  code: string;
@@ -6,7 +6,7 @@ import { AuthAbortError, createAuthFlowHelpers } from "../auth.js";
6
6
  import { SDK_OWNED_PROVIDER_ERROR_CODES, SDK_RUNTIME_OWNED_ERROR_CODES, } from "../error-resolution.js";
7
7
  import { AuthError, isProviderError, isSessionExpiredError, isTransportError, isValidationError, ProviderError, } from "../errors.js";
8
8
  import { loadProviderLocaleCatalogs, localizeAuthTurn, } from "../i18n/catalog.js";
9
- import { categoryForStatus, isRetryableCategory, PROVIDER_OBSERVABILITY_TAXONOMY_VERSION, } from "../observability.js";
9
+ import { categoryForStatus, sourceForCategory, isRetryableCategory, PROVIDER_OBSERVABILITY_TAXONOMY_VERSION, } from "../observability.js";
10
10
  import { createScratchpad } from "../runtime/auth-flow.js";
11
11
  import { createBrowserClient } from "../runtime/browser.js";
12
12
  import { createProviderCache } from "../runtime/cache.js";
@@ -357,8 +357,28 @@ function zodDetails(error) {
357
357
  message: issue.message,
358
358
  }));
359
359
  }
360
+ // Category-level projection with code-aware honesty overrides: a missing
361
+ // deployment secret is an APIFuse-side defect even though its category
362
+ // (credential_unavailable) usually means a caller credential problem, an
363
+ // internal stateful-routing deadline is APIFuse-owned despite its timeout
364
+ // category, and the built-in upstream failure families keep their upstream
365
+ // attribution even when the author left the category at the provider_error
366
+ // default.
367
+ function publicErrorSource(error, category) {
368
+ if (error instanceof StatefulRoutingDeadlineError)
369
+ return "apifuse";
370
+ if (isProviderError(error)) {
371
+ if (error.code === MISSING_SECRET_CODE)
372
+ return "apifuse";
373
+ if (error.code === "UPSTREAM_ERROR" || error.code === "BLOCKED") {
374
+ return "upstream_failure";
375
+ }
376
+ }
377
+ return sourceForCategory(category);
378
+ }
360
379
  function toErrorResponse(error, requestId, declaredErrorCode) {
361
380
  const observability = errorObservabilityDetails(error, declaredErrorCode);
381
+ const source = publicErrorSource(error, observability.category);
362
382
  if (error instanceof StatefulRoutingDeadlineError) {
363
383
  return {
364
384
  error: {
@@ -366,6 +386,7 @@ function toErrorResponse(error, requestId, declaredErrorCode) {
366
386
  message: "Stateful forwarding deadline expired.",
367
387
  ...(requestId ? { requestId } : {}),
368
388
  retryable: observability.retryable,
389
+ source,
369
390
  },
370
391
  };
371
392
  }
@@ -377,6 +398,7 @@ function toErrorResponse(error, requestId, declaredErrorCode) {
377
398
  message: publicProviderErrorMessage(error),
378
399
  ...(requestId ? { requestId } : {}),
379
400
  retryable: observability.retryable,
401
+ source,
380
402
  ...(error.fix ? { fix: error.fix } : {}),
381
403
  ...(details !== undefined ? { details } : {}),
382
404
  },
@@ -389,6 +411,7 @@ function toErrorResponse(error, requestId, declaredErrorCode) {
389
411
  message: "Invalid request body",
390
412
  ...(requestId ? { requestId } : {}),
391
413
  retryable: observability.retryable,
414
+ source,
392
415
  details: zodDetails(error),
393
416
  },
394
417
  };
@@ -405,6 +428,7 @@ function toErrorResponse(error, requestId, declaredErrorCode) {
405
428
  message: "Internal error",
406
429
  ...(requestId ? { requestId } : {}),
407
430
  retryable: observability.retryable,
431
+ source,
408
432
  details: {
409
433
  retryable: false,
410
434
  category: "internal_error",
@@ -484,9 +508,12 @@ function errorObservabilityDetails(error, declaredErrorCode) {
484
508
  return {
485
509
  category: isProviderError(error) && error.options?.category
486
510
  ? error.options.category
487
- : isEmittableErrorStatus(declaredStatus) && declaredStatus >= 500
488
- ? "provider_error"
489
- : "input_validation",
511
+ : isEmittableErrorStatus(declaredStatus) &&
512
+ categoryForStatus(declaredStatus) === "upstream_rejected"
513
+ ? "upstream_rejected"
514
+ : isEmittableErrorStatus(declaredStatus) && declaredStatus >= 500
515
+ ? "provider_error"
516
+ : "input_validation",
490
517
  taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
491
518
  retryable: isProviderError(error)
492
519
  ? (error.options?.retryable ?? effectiveDeclaration?.retryable ?? false)
@@ -501,8 +528,18 @@ function errorObservabilityDetails(error, declaredErrorCode) {
501
528
  };
502
529
  }
503
530
  if (isProviderError(error)) {
531
+ // Deterministic upstream refusals default to the rejection category:
532
+ // the UPSTREAM_REJECTED family and any operation-declared rejection
533
+ // status (409/410/422) classify as upstream_rejected unless the
534
+ // author set an explicit category.
535
+ const declaredStatus = effectiveDeclaration?.status;
536
+ const rejectionDefault = error.code === "UPSTREAM_REJECTED" ||
537
+ (isEmittableErrorStatus(declaredStatus) &&
538
+ categoryForStatus(declaredStatus) === "upstream_rejected")
539
+ ? "upstream_rejected"
540
+ : "provider_error";
504
541
  return {
505
- category: error.options?.category ?? "provider_error",
542
+ category: error.options?.category ?? rejectionDefault,
506
543
  taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
507
544
  retryable: error.options?.retryable ?? effectiveDeclaration?.retryable ?? false,
508
545
  };
@@ -579,6 +616,11 @@ function toStatusCode(error, declaredErrorCode) {
579
616
  case "UPSTREAM_RATE_LIMIT":
580
617
  case "LIMITED_NUMBER_OF_SERVICE_REQUESTS_EXCEEDS_ERROR":
581
618
  return 429;
619
+ // Deterministic upstream business refusal (honest-provider-error-
620
+ // contract): the upstream evaluated the request and said no under
621
+ // its own rules — a conflict with upstream state, never a 5xx.
622
+ case "UPSTREAM_REJECTED":
623
+ return 409;
582
624
  case "UPSTREAM_ERROR":
583
625
  case "BLOCKED":
584
626
  return 502;
@@ -665,7 +707,10 @@ function logProviderError(logger, provider, kind, route, requestId, error, statu
665
707
  taxonomyVersion: details.taxonomyVersion,
666
708
  retryable: details.retryable,
667
709
  ...(isUnregisteredProviderErrorCode
668
- ? { signal: "unregistered_provider_error_code" }
710
+ ? {
711
+ signal: "unregistered_provider_error_code",
712
+ signalFix: "Declare this code (with status and retryable) in the operation's docs.errorCodes so it serves its intended status instead of 500.",
713
+ }
669
714
  : {}),
670
715
  ...(error instanceof z.ZodError ? { issues: zodDetails(error) } : {}),
671
716
  });
@@ -43,6 +43,12 @@ export declare const ErrorEnvelopeSchema: z.ZodObject<{
43
43
  message: z.ZodString;
44
44
  requestId: z.ZodOptional<z.ZodString>;
45
45
  retryable: z.ZodBoolean;
46
+ source: z.ZodOptional<z.ZodEnum<{
47
+ client: "client";
48
+ upstream_rule: "upstream_rule";
49
+ upstream_failure: "upstream_failure";
50
+ apifuse: "apifuse";
51
+ }>>;
46
52
  fix: z.ZodOptional<z.ZodString>;
47
53
  details: z.ZodOptional<z.ZodUnknown>;
48
54
  }, z.core.$strip>;
@@ -86,6 +92,12 @@ export declare const OperationErrorResponseSchema: z.ZodObject<{
86
92
  message: z.ZodString;
87
93
  requestId: z.ZodOptional<z.ZodString>;
88
94
  retryable: z.ZodBoolean;
95
+ source: z.ZodOptional<z.ZodEnum<{
96
+ client: "client";
97
+ upstream_rule: "upstream_rule";
98
+ upstream_failure: "upstream_failure";
99
+ apifuse: "apifuse";
100
+ }>>;
89
101
  fix: z.ZodOptional<z.ZodString>;
90
102
  details: z.ZodOptional<z.ZodUnknown>;
91
103
  }, z.core.$strip>;
@@ -124,6 +136,12 @@ export declare const AuthFlowErrorResponseSchema: z.ZodObject<{
124
136
  message: z.ZodString;
125
137
  requestId: z.ZodOptional<z.ZodString>;
126
138
  retryable: z.ZodBoolean;
139
+ source: z.ZodOptional<z.ZodEnum<{
140
+ client: "client";
141
+ upstream_rule: "upstream_rule";
142
+ upstream_failure: "upstream_failure";
143
+ apifuse: "apifuse";
144
+ }>>;
127
145
  fix: z.ZodOptional<z.ZodString>;
128
146
  details: z.ZodOptional<z.ZodUnknown>;
129
147
  }, z.core.$strip>;
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { PROVIDER_ERROR_SOURCES } from "../observability.js";
2
3
  import { HttpRetryPreset } from "../types.js";
3
4
  export const ConnectionModeSchema = z.enum(["oauth2", "credentials", "platform-managed", "none"]);
4
5
  export const OperationConnectionSchema = z.object({
@@ -22,6 +23,10 @@ export const ErrorEnvelopeSchema = z.object({
22
23
  message: z.string(),
23
24
  requestId: z.string().optional(),
24
25
  retryable: z.boolean(),
26
+ // Who stopped the request (honest-provider-error-contract); optional so
27
+ // older peers (stateful forwarding owners) remain parseable. Derived from
28
+ // the PROVIDER_ERROR_SOURCES registry so the enum cannot drift.
29
+ source: z.enum(PROVIDER_ERROR_SOURCES).optional(),
25
30
  fix: z.string().optional(),
26
31
  details: z.unknown().optional(),
27
32
  });
package/dist/types.d.ts CHANGED
@@ -610,7 +610,7 @@ 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];
613
+ export declare const VALID_OPERATION_ERROR_STATUSES: readonly [400, 401, 404, 409, 410, 422, 429, 500, 502, 503, 504];
614
614
  export type ProviderErrorStatus = (typeof VALID_OPERATION_ERROR_STATUSES)[number];
615
615
  export interface OperationErrorCode {
616
616
  code: string;
package/dist/types.js CHANGED
@@ -32,7 +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
+ export const VALID_OPERATION_ERROR_STATUSES = [400, 401, 404, 409, 410, 422, 429, 500, 502, 503, 504];
36
36
  export const HttpRetryPreset = {
37
37
  Off: "off",
38
38
  TransportTransient: "transport_transient",
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.2.0-beta.18",
2
+ "version": "2.2.0-beta.20",
3
3
  "name": "@apifuse/provider-sdk",
4
4
  "private": false,
5
5
  "type": "module",
package/src/index.ts CHANGED
@@ -39,6 +39,7 @@ export {
39
39
  export type { DevServerOptions } from "./dev.js";
40
40
  export { createDevServer, startDevServer } from "./dev.js";
41
41
  export * from "./errors.js";
42
+ export * from "./observability.js";
42
43
  export * from "./user-input.js";
43
44
  export * from "./i18n/index.js";
44
45
  export {
@@ -1,4 +1,5 @@
1
- export const PROVIDER_OBSERVABILITY_TAXONOMY_VERSION = "2026-05-26";
1
+ // Mirrors packages/provider-observability in the platform monorepo (SoT).
2
+ export const PROVIDER_OBSERVABILITY_TAXONOMY_VERSION = "2026-08-07";
2
3
 
3
4
  export const PROVIDER_ERROR_CATEGORIES = [
4
5
  "ok",
@@ -7,6 +8,7 @@ export const PROVIDER_ERROR_CATEGORIES = [
7
8
  "upstream_http",
8
9
  "upstream_rate_limited",
9
10
  "upstream_auth",
11
+ "upstream_rejected",
10
12
  "upstream_schema_drift",
11
13
  "proxy_pool",
12
14
  "anti_bot_blocked",
@@ -16,6 +18,9 @@ export const PROVIDER_ERROR_CATEGORIES = [
16
18
  "output_validation",
17
19
  "provider_error",
18
20
  "internal_error",
21
+ "dependency_unavailable",
22
+ "unsupported_transport",
23
+ "client_cancelled",
19
24
  "unclassified",
20
25
  ] as const;
21
26
 
@@ -26,6 +31,9 @@ export function categoryForStatus(status: number): ProviderErrorCategory {
26
31
  if (status === 408 || status === 504) return "timeout";
27
32
  if (status === 429) return "upstream_rate_limited";
28
33
  if (status === 401 || status === 403) return "upstream_auth";
34
+ // provider-error-contract: 409/410/422 are the deterministic
35
+ // upstream-rejection status class — not a retryable upstream failure.
36
+ if (status === 409 || status === 410 || status === 422) return "upstream_rejected";
29
37
  if (status >= 400) return "upstream_http";
30
38
  return "unclassified";
31
39
  }
@@ -39,3 +47,45 @@ export function isRetryableCategory(category: ProviderErrorCategory): boolean {
39
47
  category === "proxy_pool"
40
48
  );
41
49
  }
50
+
51
+ // Who stopped the request (honest-provider-error-contract). A deliberately
52
+ // small public projection of the internal taxonomy:
53
+ // - client: the request itself is fixable by the caller
54
+ // - upstream_rule: the upstream service refused it under its own business
55
+ // rules (deterministic) or rate limits
56
+ // - upstream_failure: the upstream service malfunctioned — retryable
57
+ // - apifuse: an APIFuse-side fault (provider bug, platform dependency)
58
+ export const PROVIDER_ERROR_SOURCES = [
59
+ "client",
60
+ "upstream_rule",
61
+ "upstream_failure",
62
+ "apifuse",
63
+ ] as const;
64
+
65
+ export type ProviderErrorSource = (typeof PROVIDER_ERROR_SOURCES)[number];
66
+
67
+ export function sourceForCategory(category: ProviderErrorCategory): ProviderErrorSource {
68
+ switch (category) {
69
+ case "input_validation":
70
+ case "credential_expired":
71
+ case "credential_unavailable":
72
+ case "client_cancelled":
73
+ return "client";
74
+ case "upstream_rejected":
75
+ case "upstream_rate_limited":
76
+ return "upstream_rule";
77
+ case "timeout":
78
+ case "network":
79
+ case "upstream_http":
80
+ case "upstream_auth":
81
+ case "upstream_schema_drift":
82
+ case "anti_bot_blocked":
83
+ return "upstream_failure";
84
+ default:
85
+ // ok never reaches error serialization; provider_error,
86
+ // internal_error, output_validation, proxy_pool,
87
+ // dependency_unavailable, unsupported_transport, unclassified are
88
+ // APIFuse-side faults.
89
+ return "apifuse";
90
+ }
91
+ }
@@ -24,6 +24,8 @@ import {
24
24
  import type { ProviderLocale } from "../i18n/keys.js";
25
25
  import {
26
26
  categoryForStatus,
27
+ type ProviderErrorSource,
28
+ sourceForCategory,
27
29
  isRetryableCategory,
28
30
  PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
29
31
  type ProviderErrorCategory,
@@ -541,6 +543,7 @@ export type ProviderServerLogEvent =
541
543
  taxonomyVersion?: string;
542
544
  retryable?: boolean;
543
545
  signal?: "unregistered_provider_error_code";
546
+ signalFix?: string;
544
547
  issues?: Array<{ path: string; code: string; message: string }>;
545
548
  })
546
549
  | {
@@ -658,12 +661,34 @@ function zodDetails(error: z.ZodError): Array<{
658
661
  }));
659
662
  }
660
663
 
664
+ // Category-level projection with code-aware honesty overrides: a missing
665
+ // deployment secret is an APIFuse-side defect even though its category
666
+ // (credential_unavailable) usually means a caller credential problem, an
667
+ // internal stateful-routing deadline is APIFuse-owned despite its timeout
668
+ // category, and the built-in upstream failure families keep their upstream
669
+ // attribution even when the author left the category at the provider_error
670
+ // default.
671
+ function publicErrorSource(
672
+ error: unknown,
673
+ category: ProviderErrorCategory,
674
+ ): ProviderErrorSource {
675
+ if (error instanceof StatefulRoutingDeadlineError) return "apifuse";
676
+ if (isProviderError(error)) {
677
+ if (error.code === MISSING_SECRET_CODE) return "apifuse";
678
+ if (error.code === "UPSTREAM_ERROR" || error.code === "BLOCKED") {
679
+ return "upstream_failure";
680
+ }
681
+ }
682
+ return sourceForCategory(category);
683
+ }
684
+
661
685
  function toErrorResponse(
662
686
  error: unknown,
663
687
  requestId?: string,
664
688
  declaredErrorCode?: OperationErrorCode,
665
689
  ): OperationErrorResponse {
666
690
  const observability = errorObservabilityDetails(error, declaredErrorCode);
691
+ const source = publicErrorSource(error, observability.category);
667
692
  if (error instanceof StatefulRoutingDeadlineError) {
668
693
  return {
669
694
  error: {
@@ -671,6 +696,7 @@ function toErrorResponse(
671
696
  message: "Stateful forwarding deadline expired.",
672
697
  ...(requestId ? { requestId } : {}),
673
698
  retryable: observability.retryable,
699
+ source,
674
700
  },
675
701
  };
676
702
  }
@@ -683,6 +709,7 @@ function toErrorResponse(
683
709
  message: publicProviderErrorMessage(error),
684
710
  ...(requestId ? { requestId } : {}),
685
711
  retryable: observability.retryable,
712
+ source,
686
713
  ...(error.fix ? { fix: error.fix } : {}),
687
714
  ...(details !== undefined ? { details } : {}),
688
715
  },
@@ -696,6 +723,7 @@ function toErrorResponse(
696
723
  message: "Invalid request body",
697
724
  ...(requestId ? { requestId } : {}),
698
725
  retryable: observability.retryable,
726
+ source,
699
727
  details: zodDetails(error),
700
728
  },
701
729
  };
@@ -713,6 +741,7 @@ function toErrorResponse(
713
741
  message: "Internal error",
714
742
  ...(requestId ? { requestId } : {}),
715
743
  retryable: observability.retryable,
744
+ source,
716
745
  details: {
717
746
  retryable: false,
718
747
  category: "internal_error",
@@ -804,9 +833,12 @@ function errorObservabilityDetails(
804
833
  category:
805
834
  isProviderError(error) && error.options?.category
806
835
  ? error.options.category
807
- : isEmittableErrorStatus(declaredStatus) && declaredStatus >= 500
808
- ? "provider_error"
809
- : "input_validation",
836
+ : isEmittableErrorStatus(declaredStatus) &&
837
+ categoryForStatus(declaredStatus) === "upstream_rejected"
838
+ ? "upstream_rejected"
839
+ : isEmittableErrorStatus(declaredStatus) && declaredStatus >= 500
840
+ ? "provider_error"
841
+ : "input_validation",
810
842
  taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
811
843
  retryable: isProviderError(error)
812
844
  ? (error.options?.retryable ?? effectiveDeclaration?.retryable ?? false)
@@ -823,8 +855,19 @@ function errorObservabilityDetails(
823
855
  }
824
856
 
825
857
  if (isProviderError(error)) {
858
+ // Deterministic upstream refusals default to the rejection category:
859
+ // the UPSTREAM_REJECTED family and any operation-declared rejection
860
+ // status (409/410/422) classify as upstream_rejected unless the
861
+ // author set an explicit category.
862
+ const declaredStatus = effectiveDeclaration?.status;
863
+ const rejectionDefault =
864
+ error.code === "UPSTREAM_REJECTED" ||
865
+ (isEmittableErrorStatus(declaredStatus) &&
866
+ categoryForStatus(declaredStatus) === "upstream_rejected")
867
+ ? ("upstream_rejected" as const)
868
+ : ("provider_error" as const);
826
869
  return {
827
- category: error.options?.category ?? "provider_error",
870
+ category: error.options?.category ?? rejectionDefault,
828
871
  taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
829
872
  retryable: error.options?.retryable ?? effectiveDeclaration?.retryable ?? false,
830
873
  };
@@ -915,6 +958,11 @@ function toStatusCode(error: unknown, declaredErrorCode?: OperationErrorCode): P
915
958
  case "UPSTREAM_RATE_LIMIT":
916
959
  case "LIMITED_NUMBER_OF_SERVICE_REQUESTS_EXCEEDS_ERROR":
917
960
  return 429;
961
+ // Deterministic upstream business refusal (honest-provider-error-
962
+ // contract): the upstream evaluated the request and said no under
963
+ // its own rules — a conflict with upstream state, never a 5xx.
964
+ case "UPSTREAM_REJECTED":
965
+ return 409;
918
966
  case "UPSTREAM_ERROR":
919
967
  case "BLOCKED":
920
968
  return 502;
@@ -1025,7 +1073,11 @@ function logProviderError(
1025
1073
  taxonomyVersion: details.taxonomyVersion,
1026
1074
  retryable: details.retryable,
1027
1075
  ...(isUnregisteredProviderErrorCode
1028
- ? { signal: "unregistered_provider_error_code" as const }
1076
+ ? {
1077
+ signal: "unregistered_provider_error_code" as const,
1078
+ signalFix:
1079
+ "Declare this code (with status and retryable) in the operation's docs.errorCodes so it serves its intended status instead of 500.",
1080
+ }
1029
1081
  : {}),
1030
1082
  ...(error instanceof z.ZodError ? { issues: zodDetails(error) } : {}),
1031
1083
  });
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { PROVIDER_ERROR_SOURCES } from "../observability.js";
2
3
 
3
4
  import { HttpRetryPreset } from "../types.js";
4
5
 
@@ -27,6 +28,10 @@ export const ErrorEnvelopeSchema = z.object({
27
28
  message: z.string(),
28
29
  requestId: z.string().optional(),
29
30
  retryable: z.boolean(),
31
+ // Who stopped the request (honest-provider-error-contract); optional so
32
+ // older peers (stateful forwarding owners) remain parseable. Derived from
33
+ // the PROVIDER_ERROR_SOURCES registry so the enum cannot drift.
34
+ source: z.enum(PROVIDER_ERROR_SOURCES).optional(),
30
35
  fix: z.string().optional(),
31
36
  details: z.unknown().optional(),
32
37
  });
package/src/types.ts CHANGED
@@ -719,7 +719,7 @@ 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;
722
+ export const VALID_OPERATION_ERROR_STATUSES = [400, 401, 404, 409, 410, 422, 429, 500, 502, 503, 504] as const;
723
723
 
724
724
  export type ProviderErrorStatus = (typeof VALID_OPERATION_ERROR_STATUSES)[number];
725
725